diff --git a/src/AddressDriver.sol b/src/AddressDriver.sol index 23432d92..89ba6f1b 100644 --- a/src/AddressDriver.sol +++ b/src/AddressDriver.sol @@ -1,7 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.24; -import {AccountMetadata, Drips, StreamReceiver, IERC20, SplitsReceiver} from "./Drips.sol"; +import { + AccountMetadata, + Drips, + MaxEndHints, + StreamReceiver, + IERC20, + SplitsReceiver +} from "./Drips.sol"; import {Managed} from "./Managed.sol"; import {DriverTransferUtils} from "./DriverTransferUtils.sol"; @@ -95,29 +102,31 @@ contract AddressDriver is DriverTransferUtils, Managed { /// @param newReceivers The list of the streams receivers of the sender to be set. /// Must be sorted by the account IDs and then by the stream configurations, /// without identical elements and without 0 amtPerSecs. - /// @param maxEndHint1 An optional parameter allowing gas optimization, pass `0` to ignore it. - /// The first hint for finding the maximum end time when all streams stop due to funds - /// running out after the balance is updated and the new receivers list is applied. + /// @param maxEndHints An optional parameter allowing gas optimization. + /// To not use this feature pass an integer `0`, it represents a list of 8 zero value hints. + /// This argument is a list of hints for finding the timestamp when all streams stop + /// due to funds running out after the streams configuration is updated. /// Hints have no effect on the results of calling this function, except potentially saving gas. /// Hints are Unix timestamps used as the starting points for binary search for the time /// when funds run out in the range of timestamps from the current block's to `2^32`. - /// Hints lower than the current timestamp are ignored. - /// You can provide zero, one or two hints. The order of hints doesn't matter. + /// Hints lower than the current timestamp including the zero value hints are ignored. + /// If you provide fewer than 8 non-zero value hints make them the rightmost values to save gas. + /// It's the most beneficial to make the most risky and precise hints + /// the rightmost ones, but there's no strict ordering requirement. /// Hints are the most effective when one of them is lower than or equal to /// the last timestamp when funds are still streamed, and the other one is strictly larger - /// than that timestamp,the smaller the difference between such hints, the higher gas savings. + /// than that timestamp, the smaller the difference between such hints, the more gas is saved. /// The savings are the highest possible when one of the hints is equal to /// the last timestamp when funds are still streamed, and the other one is larger by 1. /// It's worth noting that the exact timestamp of the block in which this function is executed - /// may affect correctness of the hints, especially if they're precise. + /// may affect correctness of the hints, especially if they're precise, + /// which is why you may want to pass multiple hints with varying precision. /// Hints don't provide any benefits when balance is not enough to cover /// a single second of streaming or is enough to cover all streams until timestamp `2^32`. /// Even inaccurate hints can be useful, and providing a single hint - /// or two hints that don't enclose the time when funds run out can still save some gas. + /// or hints that don't enclose the time when funds run out can still save some gas. /// Providing poor hints that don't reduce the number of binary search steps /// may cause slightly higher gas usage than not providing any hints. - /// @param maxEndHint2 An optional parameter allowing gas optimization, pass `0` to ignore it. - /// The second hint for finding the maximum end time, see `maxEndHint1` docs for more details. /// @param transferTo The address to send funds to in case of decreasing balance /// @return realBalanceDelta The actually applied streams balance change. /// It's equal to the passed `balanceDelta`, unless it's negative @@ -127,9 +136,7 @@ contract AddressDriver is DriverTransferUtils, Managed { StreamReceiver[] calldata currReceivers, int128 balanceDelta, StreamReceiver[] calldata newReceivers, - // slither-disable-next-line similar-names - uint32 maxEndHint1, - uint32 maxEndHint2, + MaxEndHints maxEndHints, address transferTo ) public onlyProxy returns (int128 realBalanceDelta) { return _setStreamsAndTransfer( @@ -139,8 +146,7 @@ contract AddressDriver is DriverTransferUtils, Managed { currReceivers, balanceDelta, newReceivers, - maxEndHint1, - maxEndHint2, + maxEndHints, transferTo ); } diff --git a/src/Drips.sol b/src/Drips.sol index ab6f861b..47e62aa4 100644 --- a/src/Drips.sol +++ b/src/Drips.sol @@ -2,7 +2,13 @@ pragma solidity ^0.8.24; import { - Streams, StreamConfig, StreamsHistory, StreamConfigImpl, StreamReceiver + MaxEndHints, + MaxEndHintsImpl, + StreamConfig, + StreamConfigImpl, + Streams, + StreamsHistory, + StreamReceiver } from "./Streams.sol"; import {Managed} from "./Managed.sol"; import {Splits, SplitsReceiver} from "./Splits.sol"; @@ -650,29 +656,31 @@ contract Drips is Managed, Streams, Splits { /// @param newReceivers The list of the streams receivers of the account to be set. /// Must be sorted by the account IDs and then by the stream configurations, /// without identical elements and without 0 amtPerSecs. - /// @param maxEndHint1 An optional parameter allowing gas optimization, pass `0` to ignore it. - /// The first hint for finding the maximum end time when all streams stop due to funds - /// running out after the balance is updated and the new receivers list is applied. + /// @param maxEndHints An optional parameter allowing gas optimization. + /// To not use this feature pass an integer `0`, it represents a list of 8 zero value hints. + /// This argument is a list of hints for finding the timestamp when all streams stop + /// due to funds running out after the streams configuration is updated. /// Hints have no effect on the results of calling this function, except potentially saving gas. /// Hints are Unix timestamps used as the starting points for binary search for the time /// when funds run out in the range of timestamps from the current block's to `2^32`. - /// Hints lower than the current timestamp are ignored. - /// You can provide zero, one or two hints. The order of hints doesn't matter. + /// Hints lower than the current timestamp including the zero value hints are ignored. + /// If you provide fewer than 8 non-zero value hints make them the rightmost values to save gas. + /// It's the most beneficial to make the most risky and precise hints + /// the rightmost ones, but there's no strict ordering requirement. /// Hints are the most effective when one of them is lower than or equal to /// the last timestamp when funds are still streamed, and the other one is strictly larger - /// than that timestamp,the smaller the difference between such hints, the higher gas savings. + /// than that timestamp, the smaller the difference between such hints, the more gas is saved. /// The savings are the highest possible when one of the hints is equal to /// the last timestamp when funds are still streamed, and the other one is larger by 1. /// It's worth noting that the exact timestamp of the block in which this function is executed - /// may affect correctness of the hints, especially if they're precise. + /// may affect correctness of the hints, especially if they're precise, + /// which is why you may want to pass multiple hints with varying precision. /// Hints don't provide any benefits when balance is not enough to cover /// a single second of streaming or is enough to cover all streams until timestamp `2^32`. /// Even inaccurate hints can be useful, and providing a single hint - /// or two hints that don't enclose the time when funds run out can still save some gas. + /// or hints that don't enclose the time when funds run out can still save some gas. /// Providing poor hints that don't reduce the number of binary search steps /// may cause slightly higher gas usage than not providing any hints. - /// @param maxEndHint2 An optional parameter allowing gas optimization, pass `0` to ignore it. - /// The second hint for finding the maximum end time, see `maxEndHint1` docs for more details. /// @return realBalanceDelta The actually applied streams balance change. /// It's equal to the passed `balanceDelta`, unless it's negative /// and it gets capped at the current balance amount. @@ -683,13 +691,11 @@ contract Drips is Managed, Streams, Splits { StreamReceiver[] memory currReceivers, int128 balanceDelta, StreamReceiver[] memory newReceivers, - // slither-disable-next-line similar-names - uint32 maxEndHint1, - uint32 maxEndHint2 + MaxEndHints maxEndHints ) public onlyProxy onlyDriver(accountId) returns (int128 realBalanceDelta) { if (balanceDelta > 0) _increaseStreamsBalance(erc20, uint128(balanceDelta)); realBalanceDelta = Streams._setStreams( - accountId, erc20, currReceivers, balanceDelta, newReceivers, maxEndHint1, maxEndHint2 + accountId, erc20, currReceivers, balanceDelta, newReceivers, maxEndHints ); if (realBalanceDelta < 0) _decreaseStreamsBalance(erc20, uint128(-realBalanceDelta)); } diff --git a/src/DriverTransferUtils.sol b/src/DriverTransferUtils.sol index d19b906c..ba87f7af 100644 --- a/src/DriverTransferUtils.sol +++ b/src/DriverTransferUtils.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.24; -import {Drips, StreamReceiver, IERC20, SafeERC20} from "./Drips.sol"; +import {Drips, MaxEndHints, StreamReceiver, IERC20, SafeERC20} from "./Drips.sol"; import {ERC2771Context} from "openzeppelin-contracts/metatx/ERC2771Context.sol"; /// @notice ERC-20 token transfer utilities for drivers. @@ -77,29 +77,31 @@ abstract contract DriverTransferUtils is ERC2771Context { /// @param newReceivers The list of the streams receivers of the sender to be set. /// Must be sorted by the account IDs and then by the stream configurations, /// without identical elements and without 0 amtPerSecs. - /// @param maxEndHint1 An optional parameter allowing gas optimization, pass `0` to ignore it. - /// The first hint for finding the maximum end time when all streams stop due to funds - /// running out after the balance is updated and the new receivers list is applied. + /// @param maxEndHints An optional parameter allowing gas optimization. + /// To not use this feature pass an integer `0`, it represents a list of 8 zero value hints. + /// This argument is a list of hints for finding the timestamp when all streams stop + /// due to funds running out after the streams configuration is updated. /// Hints have no effect on the results of calling this function, except potentially saving gas. /// Hints are Unix timestamps used as the starting points for binary search for the time /// when funds run out in the range of timestamps from the current block's to `2^32`. - /// Hints lower than the current timestamp are ignored. - /// You can provide zero, one or two hints. The order of hints doesn't matter. + /// Hints lower than the current timestamp including the zero value hints are ignored. + /// If you provide fewer than 8 non-zero value hints make them the rightmost values to save gas. + /// It's the most beneficial to make the most risky and precise hints + /// the rightmost ones, but there's no strict ordering requirement. /// Hints are the most effective when one of them is lower than or equal to /// the last timestamp when funds are still streamed, and the other one is strictly larger - /// than that timestamp,the smaller the difference between such hints, the higher gas savings. + /// than that timestamp, the smaller the difference between such hints, the more gas is saved. /// The savings are the highest possible when one of the hints is equal to /// the last timestamp when funds are still streamed, and the other one is larger by 1. /// It's worth noting that the exact timestamp of the block in which this function is executed - /// may affect correctness of the hints, especially if they're precise. + /// may affect correctness of the hints, especially if they're precise, + /// which is why you may want to pass multiple hints with varying precision. /// Hints don't provide any benefits when balance is not enough to cover /// a single second of streaming or is enough to cover all streams until timestamp `2^32`. /// Even inaccurate hints can be useful, and providing a single hint - /// or two hints that don't enclose the time when funds run out can still save some gas. + /// or hints that don't enclose the time when funds run out can still save some gas. /// Providing poor hints that don't reduce the number of binary search steps /// may cause slightly higher gas usage than not providing any hints. - /// @param maxEndHint2 An optional parameter allowing gas optimization, pass `0` to ignore it. - /// The second hint for finding the maximum end time, see `maxEndHint1` docs for more details. /// @param transferTo The address to send funds to in case of decreasing balance /// @return realBalanceDelta The actually applied streams balance change. /// It's equal to the passed `balanceDelta`, unless it's negative @@ -111,14 +113,12 @@ abstract contract DriverTransferUtils is ERC2771Context { StreamReceiver[] calldata currReceivers, int128 balanceDelta, StreamReceiver[] calldata newReceivers, - // slither-disable-next-line similar-names - uint32 maxEndHint1, - uint32 maxEndHint2, + MaxEndHints maxEndHints, address transferTo ) internal returns (int128 realBalanceDelta) { if (balanceDelta > 0) _transferFromCaller(drips, erc20, uint128(balanceDelta)); realBalanceDelta = drips.setStreams( - accountId, erc20, currReceivers, balanceDelta, newReceivers, maxEndHint1, maxEndHint2 + accountId, erc20, currReceivers, balanceDelta, newReceivers, maxEndHints ); if (realBalanceDelta < 0) drips.withdraw(erc20, transferTo, uint128(-realBalanceDelta)); } diff --git a/src/NFTDriver.sol b/src/NFTDriver.sol index d35536fd..e76d0c23 100644 --- a/src/NFTDriver.sol +++ b/src/NFTDriver.sol @@ -2,7 +2,13 @@ pragma solidity ^0.8.24; import { - AccountMetadata, Drips, StreamReceiver, IERC20, SafeERC20, SplitsReceiver + AccountMetadata, + Drips, + MaxEndHints, + StreamReceiver, + IERC20, + SafeERC20, + SplitsReceiver } from "./Drips.sol"; import {DriverTransferUtils, ERC2771Context} from "./DriverTransferUtils.sol"; import {Managed} from "./Managed.sol"; @@ -262,29 +268,31 @@ contract NFTDriver is ERC721Burnable, DriverTransferUtils, Managed { /// @param newReceivers The list of the streams receivers of the sender to be set. /// Must be sorted by the account IDs and then by the stream configurations, /// without identical elements and without 0 amtPerSecs. - /// @param maxEndHint1 An optional parameter allowing gas optimization, pass `0` to ignore it. - /// The first hint for finding the maximum end time when all streams stop due to funds - /// running out after the balance is updated and the new receivers list is applied. + /// @param maxEndHints An optional parameter allowing gas optimization. + /// To not use this feature pass an integer `0`, it represents a list of 8 zero value hints. + /// This argument is a list of hints for finding the timestamp when all streams stop + /// due to funds running out after the streams configuration is updated. /// Hints have no effect on the results of calling this function, except potentially saving gas. /// Hints are Unix timestamps used as the starting points for binary search for the time /// when funds run out in the range of timestamps from the current block's to `2^32`. - /// Hints lower than the current timestamp are ignored. - /// You can provide zero, one or two hints. The order of hints doesn't matter. + /// Hints lower than the current timestamp including the zero value hints are ignored. + /// If you provide fewer than 8 non-zero value hints make them the rightmost values to save gas. + /// It's the most beneficial to make the most risky and precise hints + /// the rightmost ones, but there's no strict ordering requirement. /// Hints are the most effective when one of them is lower than or equal to /// the last timestamp when funds are still streamed, and the other one is strictly larger - /// than that timestamp,the smaller the difference between such hints, the higher gas savings. + /// than that timestamp, the smaller the difference between such hints, the more gas is saved. /// The savings are the highest possible when one of the hints is equal to /// the last timestamp when funds are still streamed, and the other one is larger by 1. /// It's worth noting that the exact timestamp of the block in which this function is executed - /// may affect correctness of the hints, especially if they're precise. + /// may affect correctness of the hints, especially if they're precise, + /// which is why you may want to pass multiple hints with varying precision. /// Hints don't provide any benefits when balance is not enough to cover /// a single second of streaming or is enough to cover all streams until timestamp `2^32`. /// Even inaccurate hints can be useful, and providing a single hint - /// or two hints that don't enclose the time when funds run out can still save some gas. + /// or hints that don't enclose the time when funds run out can still save some gas. /// Providing poor hints that don't reduce the number of binary search steps /// may cause slightly higher gas usage than not providing any hints. - /// @param maxEndHint2 An optional parameter allowing gas optimization, pass `0` to ignore it. - /// The second hint for finding the maximum end time, see `maxEndHint1` docs for more details. /// @param transferTo The address to send funds to in case of decreasing balance /// @return realBalanceDelta The actually applied streams balance change. /// It's equal to the passed `balanceDelta`, unless it's negative @@ -295,9 +303,7 @@ contract NFTDriver is ERC721Burnable, DriverTransferUtils, Managed { StreamReceiver[] calldata currReceivers, int128 balanceDelta, StreamReceiver[] calldata newReceivers, - // slither-disable-next-line similar-names - uint32 maxEndHint1, - uint32 maxEndHint2, + MaxEndHints maxEndHints, address transferTo ) public onlyProxy onlyApprovedOrOwner(tokenId) returns (int128 realBalanceDelta) { return _setStreamsAndTransfer( @@ -307,8 +313,7 @@ contract NFTDriver is ERC721Burnable, DriverTransferUtils, Managed { currReceivers, balanceDelta, newReceivers, - maxEndHint1, - maxEndHint2, + maxEndHints, transferTo ); } diff --git a/src/RepoDriver.sol b/src/RepoDriver.sol index c4520078..161a7e6f 100644 --- a/src/RepoDriver.sol +++ b/src/RepoDriver.sol @@ -2,7 +2,13 @@ pragma solidity ^0.8.24; import { - AccountMetadata, Drips, StreamReceiver, IERC20, SafeERC20, SplitsReceiver + AccountMetadata, + Drips, + MaxEndHints, + StreamReceiver, + IERC20, + SafeERC20, + SplitsReceiver } from "./Drips.sol"; import {DriverTransferUtils} from "./DriverTransferUtils.sol"; import {Managed} from "./Managed.sol"; @@ -457,29 +463,31 @@ contract RepoDriver is ERC677ReceiverInterface, DriverTransferUtils, Managed { /// @param newReceivers The list of the streams receivers of the sender to be set. /// Must be sorted by the account IDs and then by the stream configurations, /// without identical elements and without 0 amtPerSecs. - /// @param maxEndHint1 An optional parameter allowing gas optimization, pass `0` to ignore it. - /// The first hint for finding the maximum end time when all streams stop due to funds - /// running out after the balance is updated and the new receivers list is applied. + /// @param maxEndHints An optional parameter allowing gas optimization. + /// To not use this feature pass an integer `0`, it represents a list of 8 zero value hints. + /// This argument is a list of hints for finding the timestamp when all streams stop + /// due to funds running out after the streams configuration is updated. /// Hints have no effect on the results of calling this function, except potentially saving gas. /// Hints are Unix timestamps used as the starting points for binary search for the time /// when funds run out in the range of timestamps from the current block's to `2^32`. - /// Hints lower than the current timestamp are ignored. - /// You can provide zero, one or two hints. The order of hints doesn't matter. + /// Hints lower than the current timestamp including the zero value hints are ignored. + /// If you provide fewer than 8 non-zero value hints make them the rightmost values to save gas. + /// It's the most beneficial to make the most risky and precise hints + /// the rightmost ones, but there's no strict ordering requirement. /// Hints are the most effective when one of them is lower than or equal to /// the last timestamp when funds are still streamed, and the other one is strictly larger - /// than that timestamp,the smaller the difference between such hints, the higher gas savings. + /// than that timestamp, the smaller the difference between such hints, the more gas is saved. /// The savings are the highest possible when one of the hints is equal to /// the last timestamp when funds are still streamed, and the other one is larger by 1. /// It's worth noting that the exact timestamp of the block in which this function is executed - /// may affect correctness of the hints, especially if they're precise. + /// may affect correctness of the hints, especially if they're precise, + /// which is why you may want to pass multiple hints with varying precision. /// Hints don't provide any benefits when balance is not enough to cover /// a single second of streaming or is enough to cover all streams until timestamp `2^32`. /// Even inaccurate hints can be useful, and providing a single hint - /// or two hints that don't enclose the time when funds run out can still save some gas. + /// or hints that don't enclose the time when funds run out can still save some gas. /// Providing poor hints that don't reduce the number of binary search steps /// may cause slightly higher gas usage than not providing any hints. - /// @param maxEndHint2 An optional parameter allowing gas optimization, pass `0` to ignore it. - /// The second hint for finding the maximum end time, see `maxEndHint1` docs for more details. /// @param transferTo The address to send funds to in case of decreasing balance /// @return realBalanceDelta The actually applied streams balance change. /// It's equal to the passed `balanceDelta`, unless it's negative @@ -490,9 +498,7 @@ contract RepoDriver is ERC677ReceiverInterface, DriverTransferUtils, Managed { StreamReceiver[] calldata currReceivers, int128 balanceDelta, StreamReceiver[] calldata newReceivers, - // slither-disable-next-line similar-names - uint32 maxEndHint1, - uint32 maxEndHint2, + MaxEndHints maxEndHints, address transferTo ) public onlyProxy onlyOwner(accountId) returns (int128 realBalanceDelta) { return _setStreamsAndTransfer( @@ -502,8 +508,7 @@ contract RepoDriver is ERC677ReceiverInterface, DriverTransferUtils, Managed { currReceivers, balanceDelta, newReceivers, - maxEndHint1, - maxEndHint2, + maxEndHints, transferTo ); } diff --git a/src/Streams.sol b/src/Streams.sol index b62a1f79..72b4522c 100644 --- a/src/Streams.sol +++ b/src/Streams.sol @@ -144,6 +144,59 @@ library StreamConfigImpl { } } +/// @notice The list of 8 hints for max end time calculation. +/// They are constructed as a concatenation of 8 32-bit values: +/// `the leftmost hint (32 bits) | ... | the rightmost hint (32 bits) +type MaxEndHints is uint256; + +using MaxEndHintsImpl for MaxEndHints global; + +library MaxEndHintsImpl { + /// @notice Create a list of 8 zero value hints. + /// @return hints The list of hints. + // slither-disable-next-line dead-code + function create() internal pure returns (MaxEndHints hints) { + return MaxEndHints.wrap(0); + } + + /// @notice Add a hint to the list of hints as the rightmost and remove the leftmost. + /// @param hints The list of hints. + /// @param hint The added hint. + /// @return newHints The modified list of hints. + // slither-disable-next-line dead-code + function push(MaxEndHints hints, uint32 hint) internal pure returns (MaxEndHints newHints) { + // `hints` has value: + // `leftmost hint (32 bits) | other hints (224 bits)` + // By bit shifting we get value: + // `other hints (224 bits) | zeros (32 bits)` + // By bit masking we get value: + // `other hints (224 bits) | rightmost hint (32 bits)` + return MaxEndHints.wrap((MaxEndHints.unwrap(hints) << 32) | hint); + } + + /// @notice Remove and return the rightmost hint, and add the zero value hint as the leftmost. + /// @param hints The list of hints. + /// @return newHints The modified list of hints. + /// @return hint The removed hint. + function pop(MaxEndHints hints) internal pure returns (MaxEndHints newHints, uint32 hint) { + // `hints` has value: + // `other hints (224 bits) | rightmost hint (32 bits)` + // By bit shifting we get value: + // `zeros (32 bits) | other hints (224 bits)` + // By casting down we get value: + // `rightmost hint (32 bits)` + return + (MaxEndHints.wrap(MaxEndHints.unwrap(hints) >> 32), uint32(MaxEndHints.unwrap(hints))); + } + + /// @notice Check if the list contains any non-zero value hints. + /// @param hints The list of hints. + /// @return hasHints_ True if the list contains any non-zero value hints. + function hasHints(MaxEndHints hints) internal pure returns (bool hasHints_) { + return MaxEndHints.unwrap(hints) != 0; + } +} + /// @notice Streams can keep track of at most `type(int128).max` /// which is `2 ^ 127 - 1` units of each ERC-20 token. /// It's up to the caller to guarantee that this limit is never exceeded, @@ -683,29 +736,31 @@ abstract contract Streams { /// @param newReceivers The list of the streams receivers of the account to be set. /// Must be sorted by the account IDs and then by the stream configurations, /// without identical elements and without 0 amtPerSecs. - /// @param maxEndHint1 An optional parameter allowing gas optimization, pass `0` to ignore it. - /// The first hint for finding the maximum end time when all streams stop due to funds - /// running out after the balance is updated and the new receivers list is applied. + /// @param maxEndHints An optional parameter allowing gas optimization. + /// To not use this feature pass an integer `0`, it represents a list of 8 zero value hints. + /// This argument is a list of hints for finding the timestamp when all streams stop + /// due to funds running out after the streams configuration is updated. /// Hints have no effect on the results of calling this function, except potentially saving gas. /// Hints are Unix timestamps used as the starting points for binary search for the time /// when funds run out in the range of timestamps from the current block's to `2^32`. - /// Hints lower than the current timestamp are ignored. - /// You can provide zero, one or two hints. The order of hints doesn't matter. + /// Hints lower than the current timestamp including the zero value hints are ignored. + /// If you provide fewer than 8 non-zero value hints make them the rightmost values to save gas. + /// It's the most beneficial to make the most risky and precise hints + /// the rightmost ones, but there's no strict ordering requirement. /// Hints are the most effective when one of them is lower than or equal to /// the last timestamp when funds are still streamed, and the other one is strictly larger - /// than that timestamp,the smaller the difference between such hints, the higher gas savings. + /// than that timestamp, the smaller the difference between such hints, the more gas is saved. /// The savings are the highest possible when one of the hints is equal to /// the last timestamp when funds are still streamed, and the other one is larger by 1. /// It's worth noting that the exact timestamp of the block in which this function is executed - /// may affect correctness of the hints, especially if they're precise. + /// may affect correctness of the hints, especially if they're precise, + /// which is why you may want to pass multiple hints with varying precision. /// Hints don't provide any benefits when balance is not enough to cover /// a single second of streaming or is enough to cover all streams until timestamp `2^32`. /// Even inaccurate hints can be useful, and providing a single hint - /// or two hints that don't enclose the time when funds run out can still save some gas. + /// or hints that don't enclose the time when funds run out can still save some gas. /// Providing poor hints that don't reduce the number of binary search steps /// may cause slightly higher gas usage than not providing any hints. - /// @param maxEndHint2 An optional parameter allowing gas optimization, pass `0` to ignore it. - /// The second hint for finding the maximum end time, see `maxEndHint1` docs for more details. /// @return realBalanceDelta The actually applied streams balance change. /// It's equal to the passed `balanceDelta`, unless it's negative /// and it gets capped at the current balance amount. @@ -715,9 +770,7 @@ abstract contract Streams { StreamReceiver[] memory currReceivers, int128 balanceDelta, StreamReceiver[] memory newReceivers, - // slither-disable-next-line similar-names - uint32 maxEndHint1, - uint32 maxEndHint2 + MaxEndHints maxEndHints ) internal returns (int128 realBalanceDelta) { unchecked { StreamsState storage state = _streamsStorage().states[erc20][accountId]; @@ -740,7 +793,7 @@ abstract contract Streams { // This will not overflow if the requirement of tracking in the contract // no more than `_MAX_STREAMS_BALANCE` of each token is followed. newBalance = uint128(currBalance + realBalanceDelta); - newMaxEnd = _calcMaxEnd(newBalance, newReceivers, maxEndHint1, maxEndHint2); + newMaxEnd = _calcMaxEnd(newBalance, newReceivers, maxEndHints); _updateReceiverStates( _streamsStorage().states[erc20], currReceivers, @@ -787,16 +840,13 @@ abstract contract Streams { /// @param receivers The list of streams receivers. /// Must be sorted by the account IDs and then by the stream configurations, /// without identical elements and without 0 amtPerSecs. - /// @param hint1 The first hint for finding the maximum end time. - /// See `_setStreams` docs for `maxEndHint1` for more details. - /// @param hint2 The second hint for finding the maximum end time. - /// See `_setStreams` docs for `maxEndHint2` for more details. + /// @param maxEndHints The list of hints for finding the maximum end time. + /// See `_setStreams` docs for `maxEndHints` for more details. /// @return maxEnd The maximum end time of streaming. function _calcMaxEnd( uint128 balance, StreamReceiver[] memory receivers, - uint32 hint1, - uint32 hint2 + MaxEndHints maxEndHints ) private view returns (uint32 maxEnd) { (uint256[] memory configs, uint256 configsLen) = _buildConfigs(receivers); @@ -811,19 +861,14 @@ abstract contract Streams { return uint32(notEnoughEnd); } - if (hint1 > enoughEnd && hint1 < notEnoughEnd) { - if (_isBalanceEnough(balance, configs, configsLen, hint1)) { - enoughEnd = hint1; - } else { - notEnoughEnd = hint1; - } - } - - if (hint2 > enoughEnd && hint2 < notEnoughEnd) { - if (_isBalanceEnough(balance, configs, configsLen, hint2)) { - enoughEnd = hint2; + while (maxEndHints.hasHints()) { + uint32 hint; + (maxEndHints, hint) = maxEndHints.pop(); + if (hint <= enoughEnd || hint >= notEnoughEnd) continue; + if (_isBalanceEnough(balance, configs, configsLen, hint)) { + enoughEnd = hint; } else { - notEnoughEnd = hint2; + notEnoughEnd = hint; } } diff --git a/src/dataStore/AddressDriverDataProxy.sol b/src/dataStore/AddressDriverDataProxy.sol index d36db08e..501f96ca 100644 --- a/src/dataStore/AddressDriverDataProxy.sol +++ b/src/dataStore/AddressDriverDataProxy.sol @@ -4,7 +4,14 @@ pragma solidity ^0.8.24; import {DripsDataStore} from "./DripsDataStore.sol"; import {AddressDriver} from "../AddressDriver.sol"; import {Caller} from "../Caller.sol"; -import {AccountMetadata, Drips, StreamReceiver, IERC20, SplitsReceiver} from "../Drips.sol"; +import { + AccountMetadata, + Drips, + MaxEndHints, + StreamReceiver, + IERC20, + SplitsReceiver +} from "../Drips.sol"; import {Managed} from "../Managed.sol"; import {ERC2771Context} from "openzeppelin-contracts/metatx/ERC2771Context.sol"; @@ -64,29 +71,31 @@ contract AddressDriverDataProxy is ERC2771Context, Managed { /// the actual list must be stored in DripsDataStore. /// Must be sorted by the account IDs and then by the stream configurations, /// without identical elements and without 0 amtPerSecs. - /// @param maxEndHint1 An optional parameter allowing gas optimization, pass `0` to ignore it. - /// The first hint for finding the maximum end time when all streams stop due to funds - /// running out after the balance is updated and the new receivers list is applied. + /// @param maxEndHints An optional parameter allowing gas optimization. + /// To not use this feature pass an integer `0`, it represents a list of 8 zero value hints. + /// This argument is a list of hints for finding the timestamp when all streams stop + /// due to funds running out after the streams configuration is updated. /// Hints have no effect on the results of calling this function, except potentially saving gas. /// Hints are Unix timestamps used as the starting points for binary search for the time /// when funds run out in the range of timestamps from the current block's to `2^32`. - /// Hints lower than the current timestamp are ignored. - /// You can provide zero, one or two hints. The order of hints doesn't matter. + /// Hints lower than the current timestamp including the zero value hints are ignored. + /// If you provide fewer than 8 non-zero value hints make them the rightmost values to save gas. + /// It's the most beneficial to make the most risky and precise hints + /// the rightmost ones, but there's no strict ordering requirement. /// Hints are the most effective when one of them is lower than or equal to /// the last timestamp when funds are still streamed, and the other one is strictly larger - /// than that timestamp,the smaller the difference between such hints, the higher gas savings. + /// than that timestamp, the smaller the difference between such hints, the more gas is saved. /// The savings are the highest possible when one of the hints is equal to /// the last timestamp when funds are still streamed, and the other one is larger by 1. /// It's worth noting that the exact timestamp of the block in which this function is executed - /// may affect correctness of the hints, especially if they're precise. + /// may affect correctness of the hints, especially if they're precise, + /// which is why you may want to pass multiple hints with varying precision. /// Hints don't provide any benefits when balance is not enough to cover /// a single second of streaming or is enough to cover all streams until timestamp `2^32`. /// Even inaccurate hints can be useful, and providing a single hint - /// or two hints that don't enclose the time when funds run out can still save some gas. + /// or hints that don't enclose the time when funds run out can still save some gas. /// Providing poor hints that don't reduce the number of binary search steps /// may cause slightly higher gas usage than not providing any hints. - /// @param maxEndHint2 An optional parameter allowing gas optimization, pass `0` to ignore it. - /// The second hint for finding the maximum end time, see `maxEndHint1` docs for more details. /// @param transferTo The address to send funds to in case of decreasing balance /// @return realBalanceDelta The actually applied streams balance change. /// It's equal to the passed `balanceDelta`, unless it's negative @@ -95,9 +104,7 @@ contract AddressDriverDataProxy is ERC2771Context, Managed { IERC20 erc20, int128 balanceDelta, bytes32 newStreamsHash, - // slither-disable-next-line similar-names - uint32 maxEndHint1, - uint32 maxEndHint2, + MaxEndHints maxEndHints, address transferTo ) public onlyProxy returns (int128 realBalanceDelta) { uint256 accountId = addressDriver.calcAccountId(_msgSender()); @@ -110,8 +117,7 @@ contract AddressDriverDataProxy is ERC2771Context, Managed { dripsDataStore.loadStreams(currStreamsHash), balanceDelta, dripsDataStore.loadStreams(newStreamsHash), - maxEndHint1, - maxEndHint2, + maxEndHints, transferTo ) ); diff --git a/src/dataStore/NFTDriverDataProxy.sol b/src/dataStore/NFTDriverDataProxy.sol index 1b44b496..30097d69 100644 --- a/src/dataStore/NFTDriverDataProxy.sol +++ b/src/dataStore/NFTDriverDataProxy.sol @@ -3,7 +3,14 @@ pragma solidity ^0.8.24; import {DripsDataStore} from "./DripsDataStore.sol"; import {Caller} from "../Caller.sol"; -import {AccountMetadata, Drips, StreamReceiver, IERC20, SplitsReceiver} from "../Drips.sol"; +import { + AccountMetadata, + Drips, + MaxEndHints, + StreamReceiver, + IERC20, + SplitsReceiver +} from "../Drips.sol"; import {NFTDriver} from "../NFTDriver.sol"; import {Managed} from "../Managed.sol"; import {ERC2771Context} from "openzeppelin-contracts/metatx/ERC2771Context.sol"; @@ -150,29 +157,31 @@ contract NFTDriverDataProxy is ERC2771Context, Managed { /// the actual list must be stored in DripsDataStore. /// Must be sorted by the account IDs and then by the stream configurations, /// without identical elements and without 0 amtPerSecs. - /// @param maxEndHint1 An optional parameter allowing gas optimization, pass `0` to ignore it. - /// The first hint for finding the maximum end time when all streams stop due to funds - /// running out after the balance is updated and the new receivers list is applied. + /// @param maxEndHints An optional parameter allowing gas optimization. + /// To not use this feature pass an integer `0`, it represents a list of 8 zero value hints. + /// This argument is a list of hints for finding the timestamp when all streams stop + /// due to funds running out after the streams configuration is updated. /// Hints have no effect on the results of calling this function, except potentially saving gas. /// Hints are Unix timestamps used as the starting points for binary search for the time /// when funds run out in the range of timestamps from the current block's to `2^32`. - /// Hints lower than the current timestamp are ignored. - /// You can provide zero, one or two hints. The order of hints doesn't matter. + /// Hints lower than the current timestamp including the zero value hints are ignored. + /// If you provide fewer than 8 non-zero value hints make them the rightmost values to save gas. + /// It's the most beneficial to make the most risky and precise hints + /// the rightmost ones, but there's no strict ordering requirement. /// Hints are the most effective when one of them is lower than or equal to /// the last timestamp when funds are still streamed, and the other one is strictly larger - /// than that timestamp,the smaller the difference between such hints, the higher gas savings. + /// than that timestamp, the smaller the difference between such hints, the more gas is saved. /// The savings are the highest possible when one of the hints is equal to /// the last timestamp when funds are still streamed, and the other one is larger by 1. /// It's worth noting that the exact timestamp of the block in which this function is executed - /// may affect correctness of the hints, especially if they're precise. + /// may affect correctness of the hints, especially if they're precise, + /// which is why you may want to pass multiple hints with varying precision. /// Hints don't provide any benefits when balance is not enough to cover /// a single second of streaming or is enough to cover all streams until timestamp `2^32`. /// Even inaccurate hints can be useful, and providing a single hint - /// or two hints that don't enclose the time when funds run out can still save some gas. + /// or hints that don't enclose the time when funds run out can still save some gas. /// Providing poor hints that don't reduce the number of binary search steps /// may cause slightly higher gas usage than not providing any hints. - /// @param maxEndHint2 An optional parameter allowing gas optimization, pass `0` to ignore it. - /// The second hint for finding the maximum end time, see `maxEndHint1` docs for more details. /// @param transferTo The address to send funds to in case of decreasing balance /// @return realBalanceDelta The actually applied streams balance change. /// It's equal to the passed `balanceDelta`, unless it's negative @@ -182,9 +191,7 @@ contract NFTDriverDataProxy is ERC2771Context, Managed { IERC20 erc20, int128 balanceDelta, bytes32 newStreamsHash, - // slither-disable-next-line similar-names - uint32 maxEndHint1, - uint32 maxEndHint2, + MaxEndHints maxEndHints, address transferTo ) public onlyProxy returns (int128 realBalanceDelta) { // slither-disable-next-line unused-return @@ -197,8 +204,7 @@ contract NFTDriverDataProxy is ERC2771Context, Managed { dripsDataStore.loadStreams(currStreamsHash), balanceDelta, dripsDataStore.loadStreams(newStreamsHash), - maxEndHint1, - maxEndHint2, + maxEndHints, transferTo ) ); diff --git a/src/dataStore/RepoDriverDataProxy.sol b/src/dataStore/RepoDriverDataProxy.sol index 40b9fc71..95eebaab 100644 --- a/src/dataStore/RepoDriverDataProxy.sol +++ b/src/dataStore/RepoDriverDataProxy.sol @@ -3,7 +3,14 @@ pragma solidity ^0.8.24; import {DripsDataStore} from "./DripsDataStore.sol"; import {Caller} from "../Caller.sol"; -import {AccountMetadata, Drips, StreamReceiver, IERC20, SplitsReceiver} from "../Drips.sol"; +import { + AccountMetadata, + Drips, + MaxEndHints, + StreamReceiver, + IERC20, + SplitsReceiver +} from "../Drips.sol"; import {Managed} from "../Managed.sol"; import {RepoDriver} from "../RepoDriver.sol"; import {ERC2771Context} from "openzeppelin-contracts/metatx/ERC2771Context.sol"; @@ -67,29 +74,31 @@ contract RepoDriverDataProxy is ERC2771Context, Managed { /// the actual list must be stored in DripsDataStore. /// Must be sorted by the account IDs and then by the stream configurations, /// without identical elements and without 0 amtPerSecs. - /// @param maxEndHint1 An optional parameter allowing gas optimization, pass `0` to ignore it. - /// The first hint for finding the maximum end time when all streams stop due to funds - /// running out after the balance is updated and the new receivers list is applied. + /// @param maxEndHints An optional parameter allowing gas optimization. + /// To not use this feature pass an integer `0`, it represents a list of 8 zero value hints. + /// This argument is a list of hints for finding the timestamp when all streams stop + /// due to funds running out after the streams configuration is updated. /// Hints have no effect on the results of calling this function, except potentially saving gas. /// Hints are Unix timestamps used as the starting points for binary search for the time /// when funds run out in the range of timestamps from the current block's to `2^32`. - /// Hints lower than the current timestamp are ignored. - /// You can provide zero, one or two hints. The order of hints doesn't matter. + /// Hints lower than the current timestamp including the zero value hints are ignored. + /// If you provide fewer than 8 non-zero value hints make them the rightmost values to save gas. + /// It's the most beneficial to make the most risky and precise hints + /// the rightmost ones, but there's no strict ordering requirement. /// Hints are the most effective when one of them is lower than or equal to /// the last timestamp when funds are still streamed, and the other one is strictly larger - /// than that timestamp,the smaller the difference between such hints, the higher gas savings. + /// than that timestamp, the smaller the difference between such hints, the more gas is saved. /// The savings are the highest possible when one of the hints is equal to /// the last timestamp when funds are still streamed, and the other one is larger by 1. /// It's worth noting that the exact timestamp of the block in which this function is executed - /// may affect correctness of the hints, especially if they're precise. + /// may affect correctness of the hints, especially if they're precise, + /// which is why you may want to pass multiple hints with varying precision. /// Hints don't provide any benefits when balance is not enough to cover /// a single second of streaming or is enough to cover all streams until timestamp `2^32`. /// Even inaccurate hints can be useful, and providing a single hint - /// or two hints that don't enclose the time when funds run out can still save some gas. + /// or hints that don't enclose the time when funds run out can still save some gas. /// Providing poor hints that don't reduce the number of binary search steps /// may cause slightly higher gas usage than not providing any hints. - /// @param maxEndHint2 An optional parameter allowing gas optimization, pass `0` to ignore it. - /// The second hint for finding the maximum end time, see `maxEndHint1` docs for more details. /// @param transferTo The address to send funds to in case of decreasing balance /// @return realBalanceDelta The actually applied streams balance change. /// It's equal to the passed `balanceDelta`, unless it's negative @@ -99,9 +108,7 @@ contract RepoDriverDataProxy is ERC2771Context, Managed { IERC20 erc20, int128 balanceDelta, bytes32 newStreamsHash, - // slither-disable-next-line similar-names - uint32 maxEndHint1, - uint32 maxEndHint2, + MaxEndHints maxEndHints, address transferTo ) public onlyProxy returns (int128 realBalanceDelta) { // slither-disable-next-line unused-return @@ -114,8 +121,7 @@ contract RepoDriverDataProxy is ERC2771Context, Managed { dripsDataStore.loadStreams(currStreamsHash), balanceDelta, dripsDataStore.loadStreams(newStreamsHash), - maxEndHint1, - maxEndHint2, + maxEndHints, transferTo ) ); diff --git a/test/AddressDriver.t.sol b/test/AddressDriver.t.sol index 26b10637..65ca9e9c 100644 --- a/test/AddressDriver.t.sol +++ b/test/AddressDriver.t.sol @@ -6,6 +6,8 @@ import {AddressDriver} from "src/AddressDriver.sol"; import { AccountMetadata, Drips, + MaxEndHints, + MaxEndHintsImpl, StreamConfigImpl, StreamsHistory, StreamReceiver, @@ -28,6 +30,8 @@ contract AddressDriverTest is Test { address internal user = address(1); uint256 internal accountId; + MaxEndHints internal immutable noHints = MaxEndHintsImpl.create(); + function setUp() public { Drips dripsLogic = new Drips(10); drips = Drips(address(new ManagedProxy(dripsLogic, address(this)))); @@ -101,7 +105,7 @@ contract AddressDriverTest is Test { uint256 balance = erc20.balanceOf(address(this)); int128 realBalanceDelta = driver.setStreams( - erc20, new StreamReceiver[](0), int128(amt), receivers, 0, 0, address(this) + erc20, new StreamReceiver[](0), int128(amt), receivers, noHints, address(this) ); assertEq(erc20.balanceOf(address(this)), balance - amt, "Invalid balance after top-up"); @@ -115,7 +119,7 @@ contract AddressDriverTest is Test { balance = erc20.balanceOf(address(user)); realBalanceDelta = - driver.setStreams(erc20, receivers, -int128(amt), receivers, 0, 0, address(user)); + driver.setStreams(erc20, receivers, -int128(amt), receivers, noHints, address(user)); assertEq(erc20.balanceOf(address(user)), balance + amt, "Invalid balance after withdrawal"); assertEq(erc20.balanceOf(address(drips)), 0, "Invalid Drips balance after withdrawal"); @@ -127,11 +131,11 @@ contract AddressDriverTest is Test { function testSetStreamsDecreasingBalanceTransfersFundsToTheProvidedAddress() public { uint128 amt = 5; StreamReceiver[] memory receivers = new StreamReceiver[](0); - driver.setStreams(erc20, receivers, int128(amt), receivers, 0, 0, address(this)); + driver.setStreams(erc20, receivers, int128(amt), receivers, noHints, address(this)); address transferTo = address(1234); int128 realBalanceDelta = - driver.setStreams(erc20, receivers, -int128(amt), receivers, 0, 0, transferTo); + driver.setStreams(erc20, receivers, -int128(amt), receivers, noHints, transferTo); assertEq(erc20.balanceOf(transferTo), amt, "Invalid balance"); assertEq(erc20.balanceOf(address(drips)), 0, "Invalid Drips balance"); @@ -188,7 +192,7 @@ contract AddressDriverTest is Test { function testSetStreamsMustBeDelegated() public { notDelegatedReverts().setStreams( - erc20, new StreamReceiver[](0), 0, new StreamReceiver[](0), 0, 0, user + erc20, new StreamReceiver[](0), 0, new StreamReceiver[](0), noHints, user ); } diff --git a/test/Drips.t.sol b/test/Drips.t.sol index b10e2144..0a9912c8 100644 --- a/test/Drips.t.sol +++ b/test/Drips.t.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.24; import { AccountMetadata, Drips, + MaxEndHints, + MaxEndHintsImpl, Splits, SplitsReceiver, StreamConfigImpl, @@ -45,6 +47,8 @@ contract DripsTest is Test { uint256 internal receiver2; uint256 internal receiver3; + MaxEndHints internal immutable noHints = MaxEndHintsImpl.create(); + bytes internal constant ERROR_NOT_DRIVER = "Callable only by the driver"; bytes internal constant ERROR_BALANCE_TOO_HIGH = "Total balance too high"; bytes internal constant ERROR_ERC_20_BALANCE_TOO_LOW = "Token balance too low"; @@ -142,7 +146,7 @@ contract DripsTest is Test { if (balanceDelta > 0) transferToDrips(uint128(balanceDelta)); vm.prank(driver); int128 realBalanceDelta = - drips.setStreams(forAccount, erc20, currReceivers, balanceDelta, newReceivers, 0, 0); + drips.setStreams(forAccount, erc20, currReceivers, balanceDelta, newReceivers, noHints); if (balanceDelta < 0) withdraw(uint128(-balanceDelta)); storeStreams(forAccount, newReceivers); @@ -405,7 +409,7 @@ contract DripsTest is Test { vm.prank(driver); int128 realBalanceDelta = drips.setStreams( - accountId, erc20, receivers, -int128(streamsBalance) - 1, receivers, 0, 0 + accountId, erc20, receivers, -int128(streamsBalance) - 1, receivers, noHints ); withdraw(uint128(-realBalanceDelta)); @@ -618,7 +622,7 @@ contract DripsTest is Test { function testSetStreamsRevertsWhenNotCalledByTheDriver() public { vm.expectRevert(ERROR_NOT_DRIVER); - drips.setStreams(accountId, erc20, streamsReceivers(), 0, streamsReceivers(), 0, 0); + drips.setStreams(accountId, erc20, streamsReceivers(), 0, streamsReceivers(), noHints); } function testGiveRevertsWhenNotCalledByTheDriver() public { @@ -656,7 +660,7 @@ contract DripsTest is Test { transferToDrips(1); vm.prank(driver); vm.expectRevert(ERROR_BALANCE_TOO_HIGH); - drips.setStreams(accountId2, erc20, streamsReceivers(), 1, streamsReceivers(), 0, 0); + drips.setStreams(accountId2, erc20, streamsReceivers(), 1, streamsReceivers(), noHints); withdraw(1); setStreams(accountId1, maxBalance, maxBalance - 1, streamsReceivers()); @@ -670,7 +674,7 @@ contract DripsTest is Test { vm.prank(driver); vm.expectRevert(ERROR_ERC_20_BALANCE_TOO_LOW); - drips.setStreams(accountId, erc20, streamsReceivers(), 1, streamsReceivers(), 0, 0); + drips.setStreams(accountId, erc20, streamsReceivers(), 1, streamsReceivers(), noHints); setStreams(accountId, 2, 3, streamsReceivers()); } @@ -800,7 +804,9 @@ contract DripsTest is Test { } function testSetStreamsMustBeDelegated() public { - notDelegatedReverts().setStreams(0, erc20, streamsReceivers(), 0, streamsReceivers(), 0, 0); + notDelegatedReverts().setStreams( + 0, erc20, streamsReceivers(), 0, streamsReceivers(), noHints + ); } function testSetSplitsMustBeDelegated() public { diff --git a/test/DriverTransferUtils.t.sol b/test/DriverTransferUtils.t.sol index 701e4eef..b5cf1cbe 100644 --- a/test/DriverTransferUtils.t.sol +++ b/test/DriverTransferUtils.t.sol @@ -3,14 +3,7 @@ pragma solidity ^0.8.24; import {Caller} from "src/Caller.sol"; import {DriverTransferUtils} from "src/DriverTransferUtils.sol"; -import { - AccountMetadata, - Drips, - StreamConfigImpl, - StreamsHistory, - StreamReceiver, - SplitsReceiver -} from "src/Drips.sol"; +import {Drips, MaxEndHints, MaxEndHintsImpl, StreamReceiver, SplitsReceiver} from "src/Drips.sol"; import {ManagedProxy} from "src/Managed.sol"; import {Test} from "forge-std/Test.sol"; import { @@ -43,9 +36,7 @@ contract DummyDriver is DriverTransferUtils { StreamReceiver[] calldata currReceivers, int128 balanceDelta, StreamReceiver[] calldata newReceivers, - // slither-disable-next-line similar-names - uint32 maxEndHint1, - uint32 maxEndHint2, + MaxEndHints maxEndHints, address transferTo ) public returns (int128 realBalanceDelta) { return _setStreamsAndTransfer( @@ -55,8 +46,7 @@ contract DummyDriver is DriverTransferUtils { currReceivers, balanceDelta, newReceivers, - maxEndHint1, - maxEndHint2, + maxEndHints, transferTo ); } @@ -68,6 +58,8 @@ contract DriverTransferUtilsTest is Test { DummyDriver internal driver; IERC20 internal erc20; + MaxEndHints internal immutable noHints = MaxEndHintsImpl.create(); + uint256 accountId = 1; address userAddr = address(1234); @@ -87,14 +79,10 @@ contract DriverTransferUtilsTest is Test { return new StreamReceiver[](0); } - function noSplitsReceivers() public pure returns (SplitsReceiver[] memory) { - return new SplitsReceiver[](0); - } - function testCollectTransfersFundsToTheProvidedAddress() public { uint128 amt = 5; driver.give(accountId, accountId, erc20, amt); - drips.split(accountId, erc20, noSplitsReceivers()); + drips.split(accountId, erc20, new SplitsReceiver[](0)); uint128 collected = driver.collect(accountId, erc20, userAddr); @@ -134,7 +122,13 @@ contract DriverTransferUtilsTest is Test { uint256 balance = erc20.balanceOf(address(this)); int128 realBalanceDelta = driver.setStreams( - accountId, erc20, noStreamReceivers(), int128(amt), noStreamReceivers(), 0, 0, userAddr + accountId, + erc20, + noStreamReceivers(), + int128(amt), + noStreamReceivers(), + noHints, + userAddr ); assertEq(realBalanceDelta, int128(amt), "Invalid streams balance delta"); @@ -148,11 +142,23 @@ contract DriverTransferUtilsTest is Test { uint128 amt = 5; uint256 balance = erc20.balanceOf(address(this)); driver.setStreams( - accountId, erc20, noStreamReceivers(), int128(amt), noStreamReceivers(), 0, 0, userAddr + accountId, + erc20, + noStreamReceivers(), + int128(amt), + noStreamReceivers(), + noHints, + userAddr ); int128 realBalanceDelta = driver.setStreams( - accountId, erc20, noStreamReceivers(), -int128(amt), noStreamReceivers(), 0, 0, userAddr + accountId, + erc20, + noStreamReceivers(), + -int128(amt), + noStreamReceivers(), + noHints, + userAddr ); assertEq(realBalanceDelta, -int128(amt), "Invalid streams balance delta"); @@ -175,8 +181,7 @@ contract DriverTransferUtilsTest is Test { noStreamReceivers(), int128(amt), noStreamReceivers(), - 0, - 0, + noHints, userAddr ) ); diff --git a/test/Giver.t.sol b/test/Giver.t.sol index 14878294..6c7c24c7 100644 --- a/test/Giver.t.sol +++ b/test/Giver.t.sol @@ -2,7 +2,8 @@ pragma solidity ^0.8.24; import {Test} from "forge-std/Test.sol"; -import {AddressDriver, Drips, IERC20, StreamReceiver} from "src/AddressDriver.sol"; +import {AddressDriver} from "src/AddressDriver.sol"; +import {Drips, IERC20, MaxEndHintsImpl, StreamReceiver} from "src/Drips.sol"; import {Address, Giver, GiversRegistry} from "src/Giver.sol"; import {ManagedProxy} from "src/Managed.sol"; import { @@ -139,7 +140,12 @@ contract GiversRegistryTest is Test { function testGiveOverMaxBalance() public { erc20.approve(address(addressDriver), 15); addressDriver.setStreams( - erc20, new StreamReceiver[](0), 10, new StreamReceiver[](0), 0, 0, address(this) + erc20, + new StreamReceiver[](0), + 10, + new StreamReceiver[](0), + MaxEndHintsImpl.create(), + address(this) ); addressDriver.give(0, erc20, 5); give(drips.MAX_TOTAL_BALANCE(), drips.MAX_TOTAL_BALANCE() - 15); diff --git a/test/NFTDriver.t.sol b/test/NFTDriver.t.sol index 39ba91c1..a08fecf2 100644 --- a/test/NFTDriver.t.sol +++ b/test/NFTDriver.t.sol @@ -5,11 +5,13 @@ import {Caller} from "src/Caller.sol"; import {NFTDriver} from "src/NFTDriver.sol"; import { AccountMetadata, - StreamConfigImpl, Drips, - StreamsHistory, + MaxEndHints, + MaxEndHintsImpl, + SplitsReceiver, + StreamConfigImpl, StreamReceiver, - SplitsReceiver + StreamsHistory } from "src/Drips.sol"; import {ManagedProxy} from "src/Managed.sol"; import {Test} from "forge-std/Test.sol"; @@ -30,6 +32,8 @@ contract NFTDriverTest is Test { uint256 internal tokenId2; uint256 internal tokenIdUser; + MaxEndHints internal immutable noHints = MaxEndHintsImpl.create(); + bytes internal constant ERROR_NOT_OWNER = "ERC721: caller is not token owner or approved"; bytes internal constant ERROR_ALREADY_MINTED = "ERC721: token already minted"; @@ -223,7 +227,7 @@ contract NFTDriverTest is Test { uint256 balance = erc20.balanceOf(address(this)); int128 realBalanceDelta = driver.setStreams( - tokenId1, erc20, new StreamReceiver[](0), int128(amt), receivers, 0, 0, address(this) + tokenId1, erc20, new StreamReceiver[](0), int128(amt), receivers, noHints, address(this) ); assertEq(erc20.balanceOf(address(this)), balance - amt, "Invalid balance after top-up"); @@ -237,7 +241,7 @@ contract NFTDriverTest is Test { balance = erc20.balanceOf(address(user)); realBalanceDelta = driver.setStreams( - tokenId1, erc20, receivers, -int128(amt), receivers, 0, 0, address(user) + tokenId1, erc20, receivers, -int128(amt), receivers, noHints, address(user) ); assertEq(erc20.balanceOf(address(user)), balance + amt, "Invalid balance after withdrawal"); @@ -250,11 +254,12 @@ contract NFTDriverTest is Test { function testSetStreamsDecreasingBalanceTransfersFundsToTheProvidedAddress() public { uint128 amt = 5; StreamReceiver[] memory receivers = new StreamReceiver[](0); - driver.setStreams(tokenId, erc20, receivers, int128(amt), receivers, 0, 0, address(this)); + driver.setStreams(tokenId, erc20, receivers, int128(amt), receivers, noHints, address(this)); address transferTo = address(1234); - int128 realBalanceDelta = - driver.setStreams(tokenId, erc20, receivers, -int128(amt), receivers, 0, 0, transferTo); + int128 realBalanceDelta = driver.setStreams( + tokenId, erc20, receivers, -int128(amt), receivers, noHints, transferTo + ); assertEq(erc20.balanceOf(transferTo), amt, "Invalid balance"); assertEq(erc20.balanceOf(address(drips)), 0, "Invalid Drips balance"); @@ -266,7 +271,7 @@ contract NFTDriverTest is Test { function testSetStreamsRevertsWhenNotTokenHolder() public { StreamReceiver[] memory noReceivers = new StreamReceiver[](0); vm.expectRevert(ERROR_NOT_OWNER); - driver.setStreams(tokenIdUser, erc20, noReceivers, 0, noReceivers, 0, 0, address(this)); + driver.setStreams(tokenIdUser, erc20, noReceivers, 0, noReceivers, noHints, address(this)); } function testSetSplits() public { @@ -361,7 +366,7 @@ contract NFTDriverTest is Test { function testSetStreamsMustBeDelegated() public { notDelegatedReverts().setStreams( - 0, erc20, new StreamReceiver[](0), 0, new StreamReceiver[](0), 0, 0, user + 0, erc20, new StreamReceiver[](0), 0, new StreamReceiver[](0), noHints, user ); } diff --git a/test/RepoDriver.t.sol b/test/RepoDriver.t.sol index 848d693d..ea52c641 100644 --- a/test/RepoDriver.t.sol +++ b/test/RepoDriver.t.sol @@ -5,11 +5,13 @@ import {Caller} from "src/Caller.sol"; import {Forge, RepoDriver} from "src/RepoDriver.sol"; import { AccountMetadata, - StreamConfigImpl, Drips, - StreamsHistory, + MaxEndHints, + MaxEndHintsImpl, + SplitsReceiver, + StreamConfigImpl, StreamReceiver, - SplitsReceiver + StreamsHistory } from "src/Drips.sol"; import {ManagedProxy} from "src/Managed.sol"; import {BufferChainlink, CBORChainlink} from "chainlink/Chainlink.sol"; @@ -60,6 +62,8 @@ contract RepoDriverTest is Test { uint256 internal accountId2; uint256 internal accountIdUser; + MaxEndHints internal immutable noHints = MaxEndHintsImpl.create(); + bytes internal constant ERROR_NOT_OWNER = "Caller is not the account owner"; bytes internal constant ERROR_ALREADY_INITIALIZED = "Already initialized"; @@ -228,7 +232,7 @@ contract RepoDriverTest is Test { 2, buffer.buf ) - ) + ) ) ), "" @@ -575,7 +579,13 @@ contract RepoDriverTest is Test { StreamReceiver(accountId2, StreamConfigImpl.create(0, drips.minAmtPerSec(), 0, 0)); uint256 balance = erc20.balanceOf(address(this)); int128 realBalanceDelta = driver.setStreams( - accountId1, erc20, new StreamReceiver[](0), int128(amt), receivers, 0, 0, address(this) + accountId1, + erc20, + new StreamReceiver[](0), + int128(amt), + receivers, + noHints, + address(this) ); assertEq(erc20.balanceOf(address(this)), balance - amt, "Invalid balance after top-up"); assertEq(erc20.balanceOf(address(drips)), amt, "Invalid Drips balance after top-up"); @@ -587,7 +597,7 @@ contract RepoDriverTest is Test { // Withdraw balance = erc20.balanceOf(address(user)); realBalanceDelta = driver.setStreams( - accountId1, erc20, receivers, -int128(amt), receivers, 0, 0, address(user) + accountId1, erc20, receivers, -int128(amt), receivers, noHints, address(user) ); assertEq(erc20.balanceOf(address(user)), balance + amt, "Invalid balance after withdrawal"); assertEq(erc20.balanceOf(address(drips)), 0, "Invalid Drips balance after withdrawal"); @@ -599,10 +609,12 @@ contract RepoDriverTest is Test { function testSetStreamsDecreasingBalanceTransfersFundsToTheProvidedAddress() public { uint128 amt = 5; StreamReceiver[] memory receivers = new StreamReceiver[](0); - driver.setStreams(accountId, erc20, receivers, int128(amt), receivers, 0, 0, address(this)); + driver.setStreams( + accountId, erc20, receivers, int128(amt), receivers, noHints, address(this) + ); address transferTo = address(1234); int128 realBalanceDelta = driver.setStreams( - accountId, erc20, receivers, -int128(amt), receivers, 0, 0, transferTo + accountId, erc20, receivers, -int128(amt), receivers, noHints, transferTo ); assertEq(erc20.balanceOf(transferTo), amt, "Invalid balance"); assertEq(erc20.balanceOf(address(drips)), 0, "Invalid Drips balance"); @@ -614,7 +626,7 @@ contract RepoDriverTest is Test { function testSetStreamsRevertsWhenNotAccountOwner() public { StreamReceiver[] memory noReceivers = new StreamReceiver[](0); vm.expectRevert(ERROR_NOT_OWNER); - driver.setStreams(accountIdUser, erc20, noReceivers, 0, noReceivers, 0, 0, address(this)); + driver.setStreams(accountIdUser, erc20, noReceivers, 0, noReceivers, noHints, address(this)); } function testSetSplits() public { @@ -698,7 +710,7 @@ contract RepoDriverTest is Test { function testSetStreamsMustBeDelegated() public { notDelegatedReverts().setStreams( - 0, erc20, new StreamReceiver[](0), 0, new StreamReceiver[](0), 0, 0, user + 0, erc20, new StreamReceiver[](0), 0, new StreamReceiver[](0), noHints, user ); } diff --git a/test/Streams.t.sol b/test/Streams.t.sol index e840cf1b..a52a2e4f 100644 --- a/test/Streams.t.sol +++ b/test/Streams.t.sol @@ -4,11 +4,13 @@ pragma solidity ^0.8.24; import {Test} from "forge-std/Test.sol"; import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; import { - Streams, + MaxEndHints, + MaxEndHintsImpl, StreamConfig, - StreamsHistory, StreamConfigImpl, - StreamReceiver + StreamReceiver, + Streams, + StreamsHistory } from "src/Streams.sol"; contract PseudoRandomUtils { @@ -44,6 +46,8 @@ contract StreamsTest is Test, PseudoRandomUtils, Streams { bytes internal constant ERROR_HISTORY_INVALID = "Invalid streams history"; bytes internal constant ERROR_HISTORY_UNCLEAR = "Entry with hash and receivers"; + MaxEndHints internal immutable noHints = MaxEndHintsImpl.create(); + mapping(IERC20 erc20 => mapping(uint256 accountId => StreamReceiver[])) internal currReceiversStore; IERC20 internal defaultErc20 = IERC20(address(1)); @@ -282,6 +286,16 @@ contract StreamsTest is Test, PseudoRandomUtils, Streams { setStreams(accountId, balanceFrom, 0, loadCurrReceivers(accountId), 0); } + function createMaxEndHints(uint32 maxEndHint1, uint32 maxEndHint2) + internal + pure + returns (MaxEndHints maxEndHints) + { + maxEndHints = MaxEndHintsImpl.create(); + if (maxEndHint1 != 0) maxEndHints = maxEndHints.push(maxEndHint1); + if (maxEndHint2 != 0) maxEndHints = maxEndHints.push(maxEndHint2); + } + function setStreams( uint256 accountId, uint128 balanceFrom, @@ -289,7 +303,7 @@ contract StreamsTest is Test, PseudoRandomUtils, Streams { StreamReceiver[] memory newReceivers, uint256 expectedMaxEndFromNow ) internal { - setStreams(accountId, balanceFrom, balanceTo, newReceivers, 0, 0, expectedMaxEndFromNow); + setStreams(accountId, balanceFrom, balanceTo, newReceivers, noHints, expectedMaxEndFromNow); } function setStreams( @@ -297,21 +311,14 @@ contract StreamsTest is Test, PseudoRandomUtils, Streams { uint128 balanceFrom, uint128 balanceTo, StreamReceiver[] memory newReceivers, - uint32 maxEndHint1, - uint32 maxEndHint2, + MaxEndHints maxEndHints, uint256 expectedMaxEndFromNow ) internal { (, bytes32 oldHistoryHash,,,) = Streams._streamsState(accountId, erc20); int128 balanceDelta = int128(balanceTo) - int128(balanceFrom); int128 realBalanceDelta = Streams._setStreams( - accountId, - erc20, - loadCurrReceivers(accountId), - balanceDelta, - newReceivers, - maxEndHint1, - maxEndHint2 + accountId, erc20, loadCurrReceivers(accountId), balanceDelta, newReceivers, maxEndHints ); assertEq(realBalanceDelta, balanceDelta, "Invalid real balance delta"); @@ -407,7 +414,7 @@ contract StreamsTest is Test, PseudoRandomUtils, Streams { int128 balanceDelta, StreamReceiver[] memory newReceivers ) external { - Streams._setStreams(accountId, erc20, currReceivers, balanceDelta, newReceivers, 0, 0); + Streams._setStreams(accountId, erc20, currReceivers, balanceDelta, newReceivers, noHints); } function receiveStreams(uint256 accountId, uint128 expectedAmt) internal { @@ -581,6 +588,30 @@ contract StreamsTest is Test, PseudoRandomUtils, Streams { assertFalse(higherDuration.lt(config), "Duration lower"); } + function testMaxEndHintsStoresHints() public { + uint32 hint; + MaxEndHints hints = MaxEndHintsImpl.create(); + assertFalse(hints.hasHints(), "New hints not empty"); + + hints = hints.push(1); + assertTrue(hints.hasHints(), "Hints empty after push"); + + hints = hints.push(2); + assertTrue(hints.hasHints(), "Hints empty after 2 pushes"); + + (hints, hint) = hints.pop(); + assertEq(hint, 2, "Invalid hint 1"); + assertTrue(hints.hasHints(), "Hints empty after 1 pop"); + + (hints, hint) = hints.pop(); + assertEq(hint, 1, "Invalid hint 2"); + assertFalse(hints.hasHints(), "Hints not empty after 2 pops"); + + (hints, hint) = hints.pop(); + assertEq(hint, 0, "Invalid unpushed hint"); + assertFalse(hints.hasHints(), "Hints not empty after 3 pops"); + } + function testAllowsStreamingToASingleReceiver() public { setStreams(sender, 0, 100, recv(receiver, 1), 100); skip(15); @@ -1260,8 +1291,9 @@ contract StreamsTest is Test, PseudoRandomUtils, Streams { receivers[i] = recv(senderId + 1 + i, 1, 0, 0)[0]; } int128 amt = int128(int256((maxEnd - vm.getBlockTimestamp()) * count)); + MaxEndHints hints = createMaxEndHints(maxEndHint1, maxEndHint2); uint256 gas = gasleft(); - Streams._setStreams(senderId, erc20, recv(), amt, receivers, maxEndHint1, maxEndHint2); + Streams._setStreams(senderId, erc20, recv(), amt, receivers, hints); gas -= gasleft(); emit log_named_uint(string.concat("Gas used for ", testName), gas); } @@ -1353,7 +1385,7 @@ contract StreamsTest is Test, PseudoRandomUtils, Streams { StreamReceiver[] memory newReceivers = recv(); int128 realBalanceDelta = - Streams._setStreams(sender, erc20, receivers, type(int128).min, newReceivers, 0, 0); + Streams._setStreams(sender, erc20, receivers, type(int128).min, newReceivers, noHints); storeCurrReceivers(sender, newReceivers); assertBalance(sender, 0); assertEq(realBalanceDelta, -6, "Invalid real balance delta"); @@ -1517,7 +1549,7 @@ contract StreamsTest is Test, PseudoRandomUtils, Streams { StreamReceiver[] memory receivers = genRandomRecv(amountReceivers, maxAmtPerSec, maxStart, maxDuration); emit log_named_uint("setStreams.updateTime", vm.getBlockTimestamp()); - Streams._setStreams(sender, erc20, recv(), int128(maxCosts), receivers, 0, 0); + Streams._setStreams(sender, erc20, recv(), int128(maxCosts), receivers, noHints); (,, uint32 updateTime,, uint32 maxEnd) = Streams._streamsState(sender, erc20); @@ -1630,11 +1662,12 @@ contract StreamsTest is Test, PseudoRandomUtils, Streams { ) public { uint128 balanceBefore = sanitizeStreamsBalance(balanceRaw); StreamReceiver[] memory receivers = sanitizeReceivers(receiversRaw, receiversLengthRaw); - Streams._setStreams(senderId, usedErc20, recv(), int128(balanceBefore), receivers, 0, 0); + Streams._setStreams(senderId, usedErc20, recv(), int128(balanceBefore), receivers, noHints); skip(sanitizeStreamingTime(streamingTimeRaw, 100)); - int128 realBalanceDelta = - Streams._setStreams(senderId, usedErc20, receivers, type(int128).min, receivers, 0, 0); + int128 realBalanceDelta = Streams._setStreams( + senderId, usedErc20, receivers, type(int128).min, receivers, noHints + ); skipToCycleEnd(); uint256 balanceAfter = uint128(-realBalanceDelta); @@ -1658,17 +1691,18 @@ contract StreamsTest is Test, PseudoRandomUtils, Streams { ) public { uint128 balanceBefore = sanitizeStreamsBalance(balanceRaw); StreamReceiver[] memory receivers1 = sanitizeReceivers(receiversRaw1, receiversLengthRaw1); - Streams._setStreams(senderId, usedErc20, recv(), int128(balanceBefore), receivers1, 0, 0); + Streams._setStreams(senderId, usedErc20, recv(), int128(balanceBefore), receivers1, noHints); skip(sanitizeStreamingTime(streamingTimeRaw1, 50)); StreamReceiver[] memory receivers2 = sanitizeReceivers(receiversRaw2, receiversLengthRaw2); int128 realBalanceDelta = - Streams._setStreams(senderId, usedErc20, receivers1, 0, receivers2, 0, 0); + Streams._setStreams(senderId, usedErc20, receivers1, 0, receivers2, noHints); assertEq(realBalanceDelta, 0, "Zero balance delta changed balance"); skip(sanitizeStreamingTime(streamingTimeRaw2, 50)); - realBalanceDelta = - Streams._setStreams(senderId, usedErc20, receivers2, type(int128).min, receivers2, 0, 0); + realBalanceDelta = Streams._setStreams( + senderId, usedErc20, receivers2, type(int128).min, receivers2, noHints + ); skipToCycleEnd(); uint256 balanceAfter = uint128(-realBalanceDelta); @@ -1697,7 +1731,7 @@ contract StreamsTest is Test, PseudoRandomUtils, Streams { for (uint256 i = 0; i < senders.length; i++) { Sender memory snd = senders[i]; Streams._setStreams( - snd.accountId, usedErc20, recv(), int128(snd.balance), snd.receivers, 0, 0 + snd.accountId, usedErc20, recv(), int128(snd.balance), snd.receivers, noHints ); } @@ -1706,7 +1740,7 @@ contract StreamsTest is Test, PseudoRandomUtils, Streams { for (uint256 i = 0; i < senders.length; i++) { Sender memory snd = senders[i]; int128 realBalanceDelta = Streams._setStreams( - snd.accountId, usedErc20, snd.receivers, type(int128).min, snd.receivers, 0, 0 + snd.accountId, usedErc20, snd.receivers, type(int128).min, snd.receivers, noHints ); balanceAfter += uint128(-realBalanceDelta); } @@ -1716,6 +1750,29 @@ contract StreamsTest is Test, PseudoRandomUtils, Streams { assertEq(balanceAfter, balanceBefore, "Streamed funds don't add up"); } + function testAFullListOfMaxEndHintsDoesNotAffectMaxEnd() public { + MaxEndHints maxEndHints = MaxEndHintsImpl.create(); + // Not enough hints + maxEndHints = maxEndHints.push(26); + maxEndHints = maxEndHints.push(25); + maxEndHints = maxEndHints.push(24); + maxEndHints = maxEndHints.push(23); + // Enough hints + maxEndHints = maxEndHints.push(15); + maxEndHints = maxEndHints.push(16); + maxEndHints = maxEndHints.push(17); + maxEndHints = maxEndHints.push(18); + skipTo(10); + setStreams({ + accountId: sender, + balanceFrom: 0, + balanceTo: 10, + newReceivers: recv(receiver, 1), + maxEndHints: maxEndHints, + expectedMaxEndFromNow: 10 + }); + } + function testMaxEndHintsDoNotAffectMaxEnd() public { skipTo(10); setStreamsPermuteHints({ @@ -1787,8 +1844,9 @@ contract StreamsTest is Test, PseudoRandomUtils, Streams { ) internal { emit log_named_uint("Setting streams with hint 1", maxEndHint1); emit log_named_uint(" and hint 2", maxEndHint2); + MaxEndHints maxEndHints = createMaxEndHints(maxEndHint1, maxEndHint2); uint256 snapshot = vm.snapshot(); - setStreams(sender, 0, amt, receivers, maxEndHint1, maxEndHint2, expectedMaxEndFromNow); + setStreams(sender, 0, amt, receivers, maxEndHints, expectedMaxEndFromNow); vm.revertTo(snapshot); } diff --git a/test/dataStore/AddressDriverDataProxy.t.sol b/test/dataStore/AddressDriverDataProxy.t.sol index 60900934..a819cbc8 100644 --- a/test/dataStore/AddressDriverDataProxy.t.sol +++ b/test/dataStore/AddressDriverDataProxy.t.sol @@ -8,7 +8,13 @@ import { } from "src/dataStore/AddressDriverDataProxy.sol"; import {Call, Caller} from "src/Caller.sol"; import { - AccountMetadata, Drips, StreamConfigImpl, StreamReceiver, SplitsReceiver + AccountMetadata, + Drips, + MaxEndHints, + MaxEndHintsImpl, + StreamConfigImpl, + StreamReceiver, + SplitsReceiver } from "src/Drips.sol"; import {ManagedProxy} from "src/Managed.sol"; import {Test} from "forge-std/Test.sol"; @@ -28,6 +34,8 @@ contract AddressDriverDataProxyTest is Test { uint256 internal thisId; address internal user = address(1); + MaxEndHints internal immutable noHints = MaxEndHintsImpl.create(); + function setUp() public { Drips dripsLogic = new Drips(10); drips = Drips(address(new ManagedProxy(dripsLogic, address(this)))); @@ -65,7 +73,7 @@ contract AddressDriverDataProxyTest is Test { bytes32 hash = dripsDataStore.storeStreams(receivers); uint256 balance = erc20.balanceOf(address(this)); - int128 balanceDelta = dataProxy.setStreams(erc20, int128(amt), hash, 0, 0, address(this)); + int128 balanceDelta = dataProxy.setStreams(erc20, int128(amt), hash, noHints, address(this)); assertEq(erc20.balanceOf(address(this)), balance - amt, "Invalid balance after top-up"); assertEq(erc20.balanceOf(address(drips)), amt, "Invalid Drips balance after top-up"); @@ -77,7 +85,7 @@ contract AddressDriverDataProxyTest is Test { // Withdraw balance = erc20.balanceOf(user); - balanceDelta = dataProxy.setStreams(erc20, -int128(amt), 0, 0, 0, user); + balanceDelta = dataProxy.setStreams(erc20, -int128(amt), 0, noHints, user); assertEq(erc20.balanceOf(user), balance + amt, "Invalid balance after withdrawal"); assertEq(erc20.balanceOf(address(drips)), 0, "Invalid Drips balance after withdrawal"); @@ -93,7 +101,7 @@ contract AddressDriverDataProxyTest is Test { Call[] memory calls = new Call[](1); calls[0] = Call({ target: address(dataProxy), - data: abi.encodeCall(dataProxy.setStreams, (erc20, int128(amt), 0, 0, 0, address(this))), + data: abi.encodeCall(dataProxy.setStreams, (erc20, int128(amt), 0, noHints, address(this))), value: 0 }); @@ -157,7 +165,7 @@ contract AddressDriverDataProxyTest is Test { } function testSetStreamsMustBeDelegated() public { - notDelegatedReverts().setStreams(erc20, 0, 0, 0, 0, user); + notDelegatedReverts().setStreams(erc20, 0, 0, noHints, user); } function testSetSplitsMustBeDelegated() public { diff --git a/test/dataStore/DripsDataProxy.t.sol b/test/dataStore/DripsDataProxy.t.sol index 92c1a065..51de6a1f 100644 --- a/test/dataStore/DripsDataProxy.t.sol +++ b/test/dataStore/DripsDataProxy.t.sol @@ -3,7 +3,13 @@ pragma solidity ^0.8.24; import {DripsDataProxy, DripsDataStore} from "src/dataStore/DripsDataProxy.sol"; import { - Drips, StreamConfigImpl, StreamReceiver, StreamsHistory, SplitsReceiver + Drips, + MaxEndHints, + MaxEndHintsImpl, + StreamConfigImpl, + StreamReceiver, + StreamsHistory, + SplitsReceiver } from "src/Drips.sol"; import {ManagedProxy} from "src/Managed.sol"; import {Test} from "forge-std/Test.sol"; @@ -23,6 +29,8 @@ contract DripsDataProxyTest is Test { uint256 internal account = 1; uint256 internal receiver = 2; + MaxEndHints internal immutable noHints = MaxEndHintsImpl.create(); + function setUp() public { Drips dripsLogic = new Drips(10); drips = Drips(address(new ManagedProxy(dripsLogic, address(this)))); @@ -43,7 +51,7 @@ contract DripsDataProxyTest is Test { ); erc20.transfer(address(drips), 2); vm.prank(driver); - drips.setStreams(account, erc20, new StreamReceiver[](0), 2, streams, 0, 0); + drips.setStreams(account, erc20, new StreamReceiver[](0), 2, streams, noHints); // Create history (,, uint32 updateTime,, uint32 maxEnd) = drips.streamsState(account, erc20); @@ -102,7 +110,7 @@ contract DripsDataProxyTest is Test { dripsDataStore.storeStreams(streams); erc20.transfer(address(drips), 2); vm.prank(driver); - drips.setStreams(account, erc20, new StreamReceiver[](0), 2, streams, 0, 0); + drips.setStreams(account, erc20, new StreamReceiver[](0), 2, streams, noHints); uint256 balanceAt = dataProxy.balanceAt(account, erc20, uint32(vm.getBlockTimestamp() + 1)); assertEq(balanceAt, 1, "Invalid balance"); diff --git a/test/dataStore/NFTDriverDataProxy.t.sol b/test/dataStore/NFTDriverDataProxy.t.sol index 20e7d575..06bd528f 100644 --- a/test/dataStore/NFTDriverDataProxy.t.sol +++ b/test/dataStore/NFTDriverDataProxy.t.sol @@ -4,7 +4,13 @@ pragma solidity ^0.8.24; import {DripsDataStore, NFTDriver, NFTDriverDataProxy} from "src/dataStore/NFTDriverDataProxy.sol"; import {Call, Caller} from "src/Caller.sol"; import { - AccountMetadata, StreamConfigImpl, Drips, StreamReceiver, SplitsReceiver + AccountMetadata, + MaxEndHints, + MaxEndHintsImpl, + StreamConfigImpl, + Drips, + StreamReceiver, + SplitsReceiver } from "src/Drips.sol"; import {ManagedProxy} from "src/Managed.sol"; import {Test} from "forge-std/Test.sol"; @@ -25,6 +31,8 @@ contract NFTDriverDataProxyTest is Test { uint256 internal tokenId; bytes32 internal someMetadataHash; + MaxEndHints internal immutable noHints = MaxEndHintsImpl.create(); + function setUp() public { Drips dripsLogic = new Drips(10); drips = Drips(address(new ManagedProxy(dripsLogic, address(this)))); @@ -118,7 +126,7 @@ contract NFTDriverDataProxyTest is Test { uint256 balance = erc20.balanceOf(address(this)); int128 balanceDelta = - dataProxy.setStreams(tokenId, erc20, int128(amt), hash, 0, 0, address(this)); + dataProxy.setStreams(tokenId, erc20, int128(amt), hash, noHints, address(this)); assertEq(erc20.balanceOf(address(this)), balance - amt, "Invalid balance after top-up"); assertEq(erc20.balanceOf(address(drips)), amt, "Invalid Drips balance after top-up"); @@ -130,7 +138,7 @@ contract NFTDriverDataProxyTest is Test { // Withdraw balance = erc20.balanceOf(address(user)); - balanceDelta = dataProxy.setStreams(tokenId, erc20, -int128(amt), 0, 0, 0, address(user)); + balanceDelta = dataProxy.setStreams(tokenId, erc20, -int128(amt), 0, noHints, address(user)); assertEq(erc20.balanceOf(address(user)), balance + amt, "Invalid balance after withdrawal"); assertEq(erc20.balanceOf(address(drips)), 0, "Invalid Drips balance after withdrawal"); @@ -147,8 +155,8 @@ contract NFTDriverDataProxyTest is Test { calls[0] = Call({ target: address(dataProxy), data: abi.encodeCall( - dataProxy.setStreams, (tokenId, erc20, int128(amt), 0, 0, 0, address(this)) - ), + dataProxy.setStreams, (tokenId, erc20, int128(amt), 0, noHints, address(this)) + ), value: 0 }); @@ -222,7 +230,7 @@ contract NFTDriverDataProxyTest is Test { } function testSetStreamsMustBeDelegated() public { - notDelegatedReverts().setStreams(0, erc20, 0, 0, 0, 0, user); + notDelegatedReverts().setStreams(0, erc20, 0, 0, noHints, user); } function testSetSplitsMustBeDelegated() public { diff --git a/test/dataStore/RepoDriverDataProxy.t.sol b/test/dataStore/RepoDriverDataProxy.t.sol index 692d0a57..d33684d9 100644 --- a/test/dataStore/RepoDriverDataProxy.t.sol +++ b/test/dataStore/RepoDriverDataProxy.t.sol @@ -6,7 +6,13 @@ import { } from "src/dataStore/RepoDriverDataProxy.sol"; import {Call, Caller} from "src/Caller.sol"; import { - AccountMetadata, StreamConfigImpl, Drips, StreamReceiver, SplitsReceiver + AccountMetadata, + MaxEndHints, + MaxEndHintsImpl, + StreamConfigImpl, + Drips, + StreamReceiver, + SplitsReceiver } from "src/Drips.sol"; import {ManagedProxy} from "src/Managed.sol"; import {Forge} from "src/RepoDriver.sol"; @@ -35,6 +41,8 @@ contract RepoDriverDataProxyTest is Test { address internal user = address(1); uint256 internal accountId; + MaxEndHints internal immutable noHints = MaxEndHintsImpl.create(); + function setUp() public { Drips dripsLogic = new Drips(10); drips = Drips(address(new ManagedProxy(dripsLogic, address(this)))); @@ -80,7 +88,7 @@ contract RepoDriverDataProxyTest is Test { uint256 balance = erc20.balanceOf(address(this)); int128 balanceDelta = - dataProxy.setStreams(accountId, erc20, int128(amt), hash, 0, 0, address(this)); + dataProxy.setStreams(accountId, erc20, int128(amt), hash, noHints, address(this)); assertEq(erc20.balanceOf(address(this)), balance - amt, "Invalid balance after top-up"); assertEq(erc20.balanceOf(address(drips)), amt, "Invalid Drips balance after top-up"); @@ -92,7 +100,8 @@ contract RepoDriverDataProxyTest is Test { // Withdraw balance = erc20.balanceOf(address(user)); - balanceDelta = dataProxy.setStreams(accountId, erc20, -int128(amt), 0, 0, 0, address(user)); + balanceDelta = + dataProxy.setStreams(accountId, erc20, -int128(amt), 0, noHints, address(user)); assertEq(erc20.balanceOf(address(user)), balance + amt, "Invalid balance after withdrawal"); assertEq(erc20.balanceOf(address(drips)), 0, "Invalid Drips balance after withdrawal"); @@ -109,8 +118,8 @@ contract RepoDriverDataProxyTest is Test { calls[0] = Call({ target: address(dataProxy), data: abi.encodeCall( - dataProxy.setStreams, (accountId, erc20, int128(amt), 0, 0, 0, address(this)) - ), + dataProxy.setStreams, (accountId, erc20, int128(amt), 0, noHints, address(this)) + ), value: 0 }); @@ -174,7 +183,7 @@ contract RepoDriverDataProxyTest is Test { } function testSetStreamsMustBeDelegated() public { - notDelegatedReverts().setStreams(0, erc20, 0, 0, 0, 0, user); + notDelegatedReverts().setStreams(0, erc20, 0, 0, noHints, user); } function testSetSplitsMustBeDelegated() public {