diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 429e284..7271c9d 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,4 +1,4 @@ -name: 90% Test Coverage +name: 100% Test Coverage on: ["push", "pull_request"] @@ -35,12 +35,12 @@ jobs: env: MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} run: | - forge coverage --report lcov && lcov --extract lcov.info -o lcov.info 'src/*' + forge coverage --nmc Invariant --report lcov && lcov --extract lcov.info -o lcov.info 'src/*' id: coverage - name: Report code coverage uses: zgosalvez/github-actions-report-lcov@v1.5.0 with: coverage-files: lcov.info - minimum-coverage: 90 + minimum-coverage: 100 github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/src/TwabController.sol b/src/TwabController.sol index 324a44a..3eb2788 100644 --- a/src/TwabController.sol +++ b/src/TwabController.sol @@ -254,7 +254,7 @@ contract TwabController { function getBalanceAt( address vault, address user, - uint32 periodEndOnOrAfterTime + uint256 periodEndOnOrAfterTime ) external view returns (uint256) { TwabLib.Account storage _account = userObservations[vault][user]; return @@ -275,7 +275,7 @@ contract TwabController { */ function getTotalSupplyAt( address vault, - uint32 periodEndOnOrAfterTime + uint256 periodEndOnOrAfterTime ) external view returns (uint256) { TwabLib.Account storage _account = totalSupplyObservations[vault]; return @@ -300,8 +300,8 @@ contract TwabController { function getTwabBetween( address vault, address user, - uint32 startTime, - uint32 endTime + uint256 startTime, + uint256 endTime ) external view returns (uint256) { TwabLib.Account storage _account = userObservations[vault][user]; // We snap the timestamps to the period end on or after the timestamp because the total supply records will be sparsely populated. @@ -327,8 +327,8 @@ contract TwabController { */ function getTotalSupplyTwabBetween( address vault, - uint32 startTime, - uint32 endTime + uint256 startTime, + uint256 endTime ) external view returns (uint256) { TwabLib.Account storage _account = totalSupplyObservations[vault]; // We snap the timestamps to the period end on or after the timestamp because the total supply records will be sparsely populated. @@ -349,7 +349,7 @@ contract TwabController { * @param _timestamp The timestamp to check * @return The end timestamp of the period that ends on or immediately after the given timestamp */ - function periodEndOnOrAfter(uint32 _timestamp) external view returns (uint32) { + function periodEndOnOrAfter(uint256 _timestamp) external view returns (uint256) { return _periodEndOnOrAfter(_timestamp); } @@ -358,18 +358,19 @@ contract TwabController { * @param _timestamp The timestamp to compute the period end time for * @return A period end time. */ - function _periodEndOnOrAfter(uint32 _timestamp) internal view returns (uint32) { + function _periodEndOnOrAfter(uint256 _timestamp) internal view returns (uint256) { if (_timestamp < PERIOD_OFFSET) { return PERIOD_OFFSET; } if ((_timestamp - PERIOD_OFFSET) % PERIOD_LENGTH == 0) { - return _timestamp; + return uint32(_timestamp); } + uint256 period = TwabLib.getTimestampPeriod(PERIOD_LENGTH, PERIOD_OFFSET, _timestamp); return TwabLib.getPeriodEndTime( PERIOD_LENGTH, PERIOD_OFFSET, - TwabLib.getTimestampPeriod(PERIOD_LENGTH, PERIOD_OFFSET, _timestamp) + period ); } @@ -434,7 +435,7 @@ contract TwabController { * @param time The timestamp to check * @return period The period the timestamp falls into */ - function getTimestampPeriod(uint32 time) external view returns (uint32) { + function getTimestampPeriod(uint256 time) external view returns (uint256) { return TwabLib.getTimestampPeriod(PERIOD_LENGTH, PERIOD_OFFSET, time); } @@ -443,7 +444,7 @@ contract TwabController { * @param time The timestamp to check * @return True if the given time is finalized, false if it's during the current overwrite period. */ - function hasFinalized(uint32 time) external view returns (bool) { + function hasFinalized(uint256 time) external view returns (bool) { return TwabLib.hasFinalized(PERIOD_LENGTH, PERIOD_OFFSET, time); } @@ -452,7 +453,7 @@ contract TwabController { * @dev The overwrite period is the period during which observations are collated. * @return period The timestamp at which the current overwrite period started. */ - function currentOverwritePeriodStartedAt() external view returns (uint32) { + function currentOverwritePeriodStartedAt() external view returns (uint256) { return TwabLib.currentOverwritePeriodStartedAt(PERIOD_LENGTH, PERIOD_OFFSET); } diff --git a/src/libraries/ObservationLib.sol b/src/libraries/ObservationLib.sol index 4f30eb1..7b59f8d 100644 --- a/src/libraries/ObservationLib.sol +++ b/src/libraries/ObservationLib.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.19; import "ring-buffer-lib/RingBufferLib.sol"; -import "./OverflowSafeComparatorLib.sol"; +// import "./OverflowSafeComparatorLib.sol"; /** * @dev Sets max ring buffer length in the Account.observations Observation list. @@ -22,8 +22,6 @@ uint16 constant MAX_CARDINALITY = 9600; // with min period of 1 hour, this allow * @author PoolTogether Inc. */ library ObservationLib { - using OverflowSafeComparatorLib for uint32; - /** * @notice Observation, which includes an amount and timestamp. * @param cumulativeBalance the cumulative time-weighted balance at `timestamp`. @@ -48,7 +46,6 @@ library ObservationLib { * @param _oldestObservationIndex Index of the oldest Observation. Left side of the circular buffer. * @param _target Timestamp at which we are searching the Observation. * @param _cardinality Cardinality of the circular buffer we are searching through. - * @param _currentTime Current timestamp. * @return beforeOrAt Observation recorded before, or at, the target. * @return beforeOrAtIndex Index of observation recorded before, or at, the target. * @return afterOrAt Observation recorded at, or after, the target. @@ -59,8 +56,7 @@ library ObservationLib { uint24 _newestObservationIndex, uint24 _oldestObservationIndex, uint32 _target, - uint16 _cardinality, - uint32 _currentTime + uint16 _cardinality ) internal view @@ -86,19 +82,13 @@ library ObservationLib { beforeOrAt = _observations[beforeOrAtIndex]; uint32 beforeOrAtTimestamp = beforeOrAt.timestamp; - // We've landed on an uninitialized timestamp, keep searching higher (more recently). - if (beforeOrAtTimestamp == 0) { - leftSide = uint16(RingBufferLib.nextIndex(leftSide, _cardinality)); - continue; - } - afterOrAtIndex = uint16(RingBufferLib.nextIndex(currentIndex, _cardinality)); afterOrAt = _observations[afterOrAtIndex]; - bool targetAfterOrAt = beforeOrAtTimestamp.lte(_target, _currentTime); + bool targetAfterOrAt = beforeOrAtTimestamp <= _target; // Check if we've found the corresponding Observation. - if (targetAfterOrAt && _target.lte(afterOrAt.timestamp, _currentTime)) { + if (targetAfterOrAt && _target <= afterOrAt.timestamp) { break; } diff --git a/src/libraries/OverflowSafeComparatorLib.sol b/src/libraries/OverflowSafeComparatorLib.sol deleted file mode 100644 index d8862bf..0000000 --- a/src/libraries/OverflowSafeComparatorLib.sol +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.19; - -/// @title OverflowSafeComparatorLib library to share comparator functions between contracts -/// @dev Code taken from Uniswap V3 Oracle.sol: https://github.com/Uniswap/v3-core/blob/3e88af408132fc957e3e406f65a0ce2b1ca06c3d/contracts/libraries/Oracle.sol -/// @author PoolTogether Inc. -library OverflowSafeComparatorLib { - /// @notice 32-bit timestamps comparator. - /// @dev safe for 0 or 1 overflows, `_a` and `_b` must be chronologically before or equal to time. - /// @param _a A comparison timestamp from which to determine the relative position of `_timestamp`. - /// @param _b Timestamp to compare against `_a`. - /// @param _timestamp A timestamp truncated to 32 bits. - /// @return bool Whether `_a` is chronologically < `_b`. - function lt(uint32 _a, uint32 _b, uint32 _timestamp) internal pure returns (bool) { - // No need to adjust if there hasn't been an overflow - if (_a <= _timestamp && _b <= _timestamp) return _a < _b; - - uint256 aAdjusted = _a > _timestamp ? _a : _a + 2 ** 32; - uint256 bAdjusted = _b > _timestamp ? _b : _b + 2 ** 32; - - return aAdjusted < bAdjusted; - } - - /// @notice 32-bit timestamps comparator. - /// @dev safe for 0 or 1 overflows, `_a` and `_b` must be chronologically before or equal to time. - /// @param _a A comparison timestamp from which to determine the relative position of `_timestamp`. - /// @param _b Timestamp to compare against `_a`. - /// @param _timestamp A timestamp truncated to 32 bits. - /// @return bool Whether `_a` is chronologically <= `_b`. - function lte(uint32 _a, uint32 _b, uint32 _timestamp) internal pure returns (bool) { - // No need to adjust if there hasn't been an overflow - if (_a <= _timestamp && _b <= _timestamp) return _a <= _b; - - uint256 aAdjusted = _a > _timestamp ? _a : _a + 2 ** 32; - uint256 bAdjusted = _b > _timestamp ? _b : _b + 2 ** 32; - - return aAdjusted <= bAdjusted; - } - - /// @notice 32-bit timestamp subtractor. - /// @dev safe for 0 or 1 overflows, where `_a` and `_b` must be chronologically before or equal to time - /// @param _a The subtraction left operand - /// @param _b The subtraction right operand - /// @param _timestamp The current time. Expected to be chronologically after both. - /// @return The difference between a and b, adjusted for overflow - function checkedSub(uint32 _a, uint32 _b, uint32 _timestamp) internal pure returns (uint32) { - // No need to adjust if there hasn't been an overflow - - if (_a <= _timestamp && _b <= _timestamp) return _a - _b; - - uint256 aAdjusted = _a > _timestamp ? _a : _a + 2 ** 32; - uint256 bAdjusted = _b > _timestamp ? _b : _b + 2 ** 32; - - return uint32(aAdjusted - bAdjusted); - } -} \ No newline at end of file diff --git a/src/libraries/TwabLib.sol b/src/libraries/TwabLib.sol index ee1fc82..a317dac 100644 --- a/src/libraries/TwabLib.sol +++ b/src/libraries/TwabLib.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import "forge-std/console2.sol"; - import "ring-buffer-lib/RingBufferLib.sol"; import { ObservationLib, MAX_CARDINALITY } from "./ObservationLib.sol"; +type PeriodOffsetRelativeTimestamp is uint32; + /// @notice Emitted when a balance is decreased by an amount that exceeds the amount available. /// @param balance The current balance of the account /// @param amount The amount being decreased from the account's balance @@ -32,7 +32,7 @@ error InvalidTimeRange(uint256 start, uint256 end); /// @notice Emitted when there is insufficient history to lookup a twab time range /// @param requestedTimestamp The timestamp requested /// @param oldestTimestamp The oldest timestamp that can be read -error InsufficientHistory(uint32 requestedTimestamp, uint32 oldestTimestamp); +error InsufficientHistory(PeriodOffsetRelativeTimestamp requestedTimestamp, PeriodOffsetRelativeTimestamp oldestTimestamp); /** * @title PoolTogether V5 TwabLib (Library) @@ -103,7 +103,8 @@ library TwabLib { ) { accountDetails = _account.details; - isObservationRecorded = _delegateAmount != uint96(0); + // record a new observation if the delegateAmount is non-zero and time has not overflowed. + isObservationRecorded = _delegateAmount != uint96(0) && (block.timestamp - PERIOD_OFFSET) < type(uint32).max; accountDetails.balance += _amount; accountDetails.delegateBalance += _delegateAmount; @@ -163,7 +164,8 @@ library TwabLib { ); } - isObservationRecorded = _delegateAmount != uint96(0); + // record a new observation if the delegateAmount is non-zero and time has not overflowed. + isObservationRecorded = _delegateAmount != uint96(0) && (block.timestamp - PERIOD_OFFSET) < type(uint32).max; unchecked { accountDetails.balance -= _amount; @@ -236,9 +238,17 @@ library TwabLib { uint32 PERIOD_OFFSET, ObservationLib.Observation[MAX_CARDINALITY] storage _observations, AccountDetails memory _accountDetails, - uint32 _targetTime + uint256 _targetTime ) internal view requireFinalized(PERIOD_LENGTH, PERIOD_OFFSET, _targetTime) returns (uint256) { - ObservationLib.Observation memory prevOrAtObservation = _getPreviousOrAtObservation(PERIOD_OFFSET, _observations, _accountDetails, _targetTime); + if (_targetTime < PERIOD_OFFSET) { + return 0; + } + uint256 offsetTargetTime = _targetTime - PERIOD_OFFSET; + // if this is for an overflowed time period, return 0 + if (offsetTargetTime > type(uint32).max) { + return 0; + } + ObservationLib.Observation memory prevOrAtObservation = _getPreviousOrAtObservation(_observations, _accountDetails, PeriodOffsetRelativeTimestamp.wrap(uint32(offsetTargetTime))); return prevOrAtObservation.balance; } @@ -258,39 +268,48 @@ library TwabLib { uint32 PERIOD_OFFSET, ObservationLib.Observation[MAX_CARDINALITY] storage _observations, AccountDetails memory _accountDetails, - uint32 _startTime, - uint32 _endTime + uint256 _startTime, + uint256 _endTime ) internal view requireFinalized(PERIOD_LENGTH, PERIOD_OFFSET, _endTime) returns (uint256) { - if (_startTime > _endTime) { + if (_endTime < _startTime) { revert InvalidTimeRange(_startTime, _endTime); } - ObservationLib.Observation memory endObservation = _getPreviousOrAtObservation(PERIOD_OFFSET, _observations, _accountDetails, _endTime); + uint256 offsetStartTime = _startTime - PERIOD_OFFSET; + uint256 offsetEndTime = _endTime - PERIOD_OFFSET; - if (_startTime == _endTime) { + // if the either time has overflowed, then return 0. + if (offsetStartTime > type(uint32).max || + offsetEndTime > type(uint32).max) { + return 0; + } + + ObservationLib.Observation memory endObservation = _getPreviousOrAtObservation(_observations, _accountDetails, PeriodOffsetRelativeTimestamp.wrap(uint32(offsetEndTime))); + + if (offsetStartTime == offsetEndTime) { return endObservation.balance; } - ObservationLib.Observation memory startObservation = _getPreviousOrAtObservation(PERIOD_OFFSET, _observations, _accountDetails, _startTime); + ObservationLib.Observation memory startObservation = _getPreviousOrAtObservation(_observations, _accountDetails, PeriodOffsetRelativeTimestamp.wrap(uint32(offsetStartTime))); - if (startObservation.timestamp != _startTime) { + if (startObservation.timestamp != offsetStartTime) { startObservation = _calculateTemporaryObservation( startObservation, - _startTime + PeriodOffsetRelativeTimestamp.wrap(uint32(offsetStartTime)) ); } - if (endObservation.timestamp != _endTime) { + if (endObservation.timestamp != offsetEndTime) { endObservation = _calculateTemporaryObservation( endObservation, - _endTime + PeriodOffsetRelativeTimestamp.wrap(uint32(offsetEndTime)) ); } // Difference in amount / time return (endObservation.cumulativeBalance - startObservation.cumulativeBalance) / - (_endTime - _startTime); + (offsetEndTime - offsetStartTime); } /** @@ -316,7 +335,7 @@ library TwabLib { AccountDetails memory newAccountDetails ) { - uint32 currentTime = uint32(block.timestamp); + PeriodOffsetRelativeTimestamp currentTime = PeriodOffsetRelativeTimestamp.wrap(uint32(block.timestamp - PERIOD_OFFSET)); uint16 nextIndex; ObservationLib.Observation memory newestObservation; @@ -349,7 +368,7 @@ library TwabLib { currentTime ), balance: _accountDetails.delegateBalance, - timestamp: currentTime + timestamp: PeriodOffsetRelativeTimestamp.unwrap(currentTime) }); // Write to storage @@ -365,13 +384,13 @@ library TwabLib { */ function _calculateTemporaryObservation( ObservationLib.Observation memory _observation, - uint32 _time + PeriodOffsetRelativeTimestamp _time ) private view returns (ObservationLib.Observation memory) { return ObservationLib.Observation({ cumulativeBalance: _extrapolateFromBalance(_observation, _time), balance: _observation.balance, - timestamp: _time + timestamp: PeriodOffsetRelativeTimestamp.unwrap(_time) }); } @@ -385,7 +404,7 @@ library TwabLib { * @param _accountDetails The account details to query with * @return index The index of the next observation slot to overwrite * @return newestObservation The newest observation in the circular buffer - * @return isNew Whether or not the observation is new + * @return isNew True if the observation slot is new, false if we're overwriting */ function _getNextObservationIndex( uint32 PERIOD_LENGTH, @@ -400,21 +419,16 @@ library TwabLib { uint16 newestIndex; (newestIndex, newestObservation) = getNewestObservation(_observations, _accountDetails); - // if we're in the same block, return - if (newestObservation.timestamp == block.timestamp) { - return (newestIndex, newestObservation, false); - } - - uint32 currentPeriod = getTimestampPeriod( + uint256 currentPeriod = getTimestampPeriod( PERIOD_LENGTH, PERIOD_OFFSET, block.timestamp ); - uint32 newestObservationPeriod = getTimestampPeriod( + uint256 newestObservationPeriod = getTimestampPeriod( PERIOD_LENGTH, PERIOD_OFFSET, - newestObservation.timestamp + PERIOD_OFFSET + uint256(newestObservation.timestamp) ); // Create a new Observation if it's the first period or the current time falls within a new period @@ -435,23 +449,31 @@ library TwabLib { function _currentOverwritePeriodStartedAt( uint32 PERIOD_LENGTH, uint32 PERIOD_OFFSET - ) private view returns (uint32) { - uint32 period = getTimestampPeriod(PERIOD_LENGTH, PERIOD_OFFSET, block.timestamp); + ) private view returns (uint256) { + uint256 period = getTimestampPeriod(PERIOD_LENGTH, PERIOD_OFFSET, block.timestamp); return getPeriodStartTime(PERIOD_LENGTH, PERIOD_OFFSET, period); } /** * @notice Calculates the next cumulative balance using a provided Observation and timestamp. * @param _observation The observation to extrapolate from - * @param _timestamp The timestamp to extrapolate to + * @param _offsetTimestamp The timestamp to extrapolate to * @return cumulativeBalance The cumulative balance at the timestamp */ function _extrapolateFromBalance( ObservationLib.Observation memory _observation, - uint32 _timestamp - ) private pure returns (uint128 cumulativeBalance) { + PeriodOffsetRelativeTimestamp _offsetTimestamp + ) private view returns (uint128) { // new cumulative balance = provided cumulative balance (or zero) + (current balance * elapsed seconds) - return _observation.cumulativeBalance + uint128(_observation.balance) * (_timestamp - _observation.timestamp); + uint128 cumulativeBalance; + uint32 deltaTime = PeriodOffsetRelativeTimestamp.unwrap(_offsetTimestamp) - _observation.timestamp; + unchecked { + cumulativeBalance = uint128( + uint256(_observation.cumulativeBalance) + uint256(_observation.balance) * deltaTime + ); + } + + return cumulativeBalance; } /** @@ -463,7 +485,7 @@ library TwabLib { function currentOverwritePeriodStartedAt( uint32 PERIOD_LENGTH, uint32 PERIOD_OFFSET - ) internal view returns (uint32) { + ) internal view returns (uint256) { return _currentOverwritePeriodStartedAt(PERIOD_LENGTH, PERIOD_OFFSET); } @@ -479,11 +501,11 @@ library TwabLib { uint32 PERIOD_LENGTH, uint32 PERIOD_OFFSET, uint256 _timestamp - ) internal pure returns (uint32) { + ) internal pure returns (uint256) { if (_timestamp <= PERIOD_OFFSET) { return 0; } - return uint32((_timestamp - PERIOD_OFFSET) / uint256(PERIOD_LENGTH)); + return (_timestamp - PERIOD_OFFSET) / uint256(PERIOD_LENGTH); } /** @@ -496,8 +518,8 @@ library TwabLib { function getPeriodStartTime( uint32 PERIOD_LENGTH, uint32 PERIOD_OFFSET, - uint32 _period - ) internal pure returns (uint32) { + uint256 _period + ) internal pure returns (uint256) { return _period * PERIOD_LENGTH + PERIOD_OFFSET; } @@ -511,8 +533,8 @@ library TwabLib { function getPeriodEndTime( uint32 PERIOD_LENGTH, uint32 PERIOD_OFFSET, - uint32 _period - ) internal pure returns (uint32) { + uint256 _period + ) internal pure returns (uint256) { return (_period + 1) * PERIOD_LENGTH + PERIOD_OFFSET; } @@ -529,13 +551,20 @@ library TwabLib { uint32 PERIOD_OFFSET, ObservationLib.Observation[MAX_CARDINALITY] storage _observations, AccountDetails memory _accountDetails, - uint32 _targetTime + uint256 _targetTime ) internal view returns (ObservationLib.Observation memory prevOrAtObservation) { + if (_targetTime < PERIOD_OFFSET) { + return ObservationLib.Observation({ cumulativeBalance: 0, balance: 0, timestamp: 0 }); + } + uint256 offsetTargetTime = _targetTime - PERIOD_OFFSET; + // if this is for an overflowed time period, return 0 + if (offsetTargetTime > type(uint32).max) { + return ObservationLib.Observation({ cumulativeBalance: 0, balance: 0, timestamp: type(uint32).max }); + } prevOrAtObservation = _getPreviousOrAtObservation( - PERIOD_OFFSET, _observations, _accountDetails, - _targetTime + PeriodOffsetRelativeTimestamp.wrap(uint32(offsetTargetTime)) ); } @@ -544,14 +573,13 @@ library TwabLib { * @dev If an observation is available at the target time, it is returned. Otherwise, the newest observation before the target time is returned. * @param _observations The circular buffer of observations * @param _accountDetails The account details to query with - * @param _targetTime The timestamp to look up + * @param _offsetTargetTime The timestamp to look up (offset by the period offset) * @return prevOrAtObservation The observation */ function _getPreviousOrAtObservation( - uint32 PERIOD_OFFSET, ObservationLib.Observation[MAX_CARDINALITY] storage _observations, AccountDetails memory _accountDetails, - uint32 _targetTime + PeriodOffsetRelativeTimestamp _offsetTargetTime ) private view @@ -559,7 +587,7 @@ library TwabLib { { // If there are no observations, return a zeroed observation if (_accountDetails.cardinality == 0) { - return ObservationLib.Observation({ cumulativeBalance: 0, balance: 0, timestamp: PERIOD_OFFSET }); + return ObservationLib.Observation({ cumulativeBalance: 0, balance: 0, timestamp: 0 }); } uint16 oldestTwabIndex; @@ -567,13 +595,13 @@ library TwabLib { (oldestTwabIndex, prevOrAtObservation) = getOldestObservation(_observations, _accountDetails); // if the requested time is older than the oldest observation - if (_targetTime < prevOrAtObservation.timestamp) { + if (PeriodOffsetRelativeTimestamp.unwrap(_offsetTargetTime) < prevOrAtObservation.timestamp) { // if the user didn't have any activity prior to the oldest observation, then we know they had a zero balance if (_accountDetails.cardinality < MAX_CARDINALITY) { - return ObservationLib.Observation({ cumulativeBalance: 0, balance: 0, timestamp: _targetTime }); + return ObservationLib.Observation({ cumulativeBalance: 0, balance: 0, timestamp: PeriodOffsetRelativeTimestamp.unwrap(_offsetTargetTime) }); } else { // if we are missing their history, we must revert - revert InsufficientHistory(_targetTime, prevOrAtObservation.timestamp); + revert InsufficientHistory(_offsetTargetTime, PeriodOffsetRelativeTimestamp.wrap(prevOrAtObservation.timestamp)); } } @@ -589,7 +617,7 @@ library TwabLib { (newestTwabIndex, afterOrAtObservation) = getNewestObservation(_observations, _accountDetails); // if the target time is at or after the newest, return it - if (_targetTime >= afterOrAtObservation.timestamp) { + if (PeriodOffsetRelativeTimestamp.unwrap(_offsetTargetTime) >= afterOrAtObservation.timestamp) { return afterOrAtObservation; } // if we know there is only 1 observation older than the newest @@ -603,13 +631,12 @@ library TwabLib { _observations, newestTwabIndex, oldestTwabIndex, - _targetTime, - _accountDetails.cardinality, - uint32(block.timestamp) + PeriodOffsetRelativeTimestamp.unwrap(_offsetTargetTime), + _accountDetails.cardinality ); // If the afterOrAt is at, we can skip a temporary Observation computation by returning it here - if (afterOrAtObservation.timestamp == _targetTime) { + if (afterOrAtObservation.timestamp == PeriodOffsetRelativeTimestamp.unwrap(_offsetTargetTime)) { return afterOrAtObservation; } @@ -627,7 +654,7 @@ library TwabLib { function hasFinalized( uint32 PERIOD_LENGTH, uint32 PERIOD_OFFSET, - uint32 _time + uint256 _time ) internal view returns (bool) { return _hasFinalized(PERIOD_LENGTH, PERIOD_OFFSET, _time); } @@ -643,7 +670,7 @@ library TwabLib { function _hasFinalized( uint32 PERIOD_LENGTH, uint32 PERIOD_OFFSET, - uint32 _time + uint256 _time ) private view returns (bool) { // It's safe if equal to the overwrite period start time, because the cumulative balance won't be impacted return _time <= _currentOverwritePeriodStartedAt(PERIOD_LENGTH, PERIOD_OFFSET); @@ -661,7 +688,7 @@ library TwabLib { uint256 _timestamp ) { // The current period can still be changed; so the start of the period marks the beginning of unsafe timestamps. - uint32 overwritePeriodStartTime = _currentOverwritePeriodStartedAt( + uint256 overwritePeriodStartTime = _currentOverwritePeriodStartedAt( PERIOD_LENGTH, PERIOD_OFFSET ); diff --git a/test/ObservationLib.t.sol b/test/ObservationLib.t.sol index 38894fa..7f8e5ad 100644 --- a/test/ObservationLib.t.sol +++ b/test/ObservationLib.t.sol @@ -41,8 +41,7 @@ contract ObservationLibTest is BaseTest { newestObservationIndex, oldestObservationIndex, 2, - cardinality, - time + cardinality ); assertEq(beforeOrAt.timestamp, 1); @@ -53,46 +52,13 @@ contract ObservationLibTest is BaseTest { newestObservationIndex, oldestObservationIndex, 5, - cardinality, - time + cardinality ); assertEq(beforeOrAt.timestamp, 5); assertEq(afterOrAt.timestamp, 6); } - function testBinarySearch_beforeOrAtIsZero() public { - observationLibMock.updateObservation( - 2, - ObservationLib.Observation({ timestamp: 10, balance: 0, cumulativeBalance: 0 }) - ); - observationLibMock.updateObservation( - 3, - ObservationLib.Observation({ timestamp: 20, balance: 0, cumulativeBalance: 0 }) - ); - - uint24 newestObservationIndex = 3; - uint24 oldestObservationIndex = 0; - uint32 target = 10; - uint16 cardinality = 4; - - ( - ObservationLib.Observation memory beforeOrAt, - uint16 beforeOrAtIndex, - ObservationLib.Observation memory afterOrAt, - uint16 afterOrAtIndex - ) = observationLibMock.binarySearch( - newestObservationIndex, - oldestObservationIndex, - target, - cardinality, - uint32(block.timestamp) - ); - - assertEq(beforeOrAt.timestamp, target); - assertEq(afterOrAt.timestamp, 20); - } - function testBinarySearch_HappyPath_afterOrAt() public { uint32[] memory t = new uint32[](6); t[0] = 1; @@ -116,8 +82,7 @@ contract ObservationLibTest is BaseTest { newestObservationIndex, oldestObservationIndex, target, - cardinality, - uint32(block.timestamp) + cardinality ); assertEq(beforeOrAt.timestamp, target - 1); @@ -138,8 +103,7 @@ contract ObservationLibTest is BaseTest { newestObservationIndex, oldestObservationIndex, target, - cardinality, - uint32(block.timestamp) + cardinality ); } @@ -161,8 +125,7 @@ contract ObservationLibTest is BaseTest { newestObservationIndex, oldestObservationIndex, target, - cardinality, - uint32(block.timestamp) + cardinality ); assertEq(beforeOrAt.timestamp, 10); @@ -183,8 +146,7 @@ contract ObservationLibTest is BaseTest { newestObservationIndex, oldestObservationIndex, target, - cardinality, - uint32(block.timestamp) + cardinality ); } @@ -207,8 +169,7 @@ contract ObservationLibTest is BaseTest { newestObservationIndex, oldestObservationIndex, target, - cardinality, - uint32(block.timestamp) + cardinality ); assertEq(beforeOrAt.timestamp, 10); @@ -234,8 +195,7 @@ contract ObservationLibTest is BaseTest { newestObservationIndex, oldestObservationIndex, target, - cardinality, - uint32(block.timestamp) + cardinality ); assertEq(beforeOrAt.timestamp, 10); @@ -261,8 +221,7 @@ contract ObservationLibTest is BaseTest { newestObservationIndex, oldestObservationIndex, target, - cardinality, - uint32(block.timestamp) + cardinality ); assertEq(beforeOrAt.timestamp, 10); @@ -289,8 +248,7 @@ contract ObservationLibTest is BaseTest { newestObservationIndex, oldestObservationIndex, target, - cardinality, - uint32(block.timestamp) + cardinality ); assertEq(beforeOrAt.timestamp, 10); @@ -317,8 +275,7 @@ contract ObservationLibTest is BaseTest { newestObservationIndex, oldestObservationIndex, target, - cardinality, - uint32(block.timestamp) + cardinality ); assertEq(beforeOrAt.timestamp, 20); @@ -345,8 +302,7 @@ contract ObservationLibTest is BaseTest { newestObservationIndex, oldestObservationIndex, target, - cardinality, - uint32(block.timestamp) + cardinality ); assertEq(beforeOrAt.timestamp, 20); diff --git a/test/TwabController.t.sol b/test/TwabController.t.sol index 9c8c279..b32ebb8 100644 --- a/test/TwabController.t.sol +++ b/test/TwabController.t.sol @@ -103,29 +103,29 @@ contract TwabControllerTest is BaseTest { } } - function testperiodEndOnOrAfter_beforeOffset() public { + function testPeriodEndOnOrAfter_beforeOffset() public { assertEq(twabController.periodEndOnOrAfter(PERIOD_OFFSET - 1), PERIOD_OFFSET); } - function testperiodEndOnOrAfter_atOffset() public { + function testPeriodEndOnOrAfter_atOffset() public { assertEq(twabController.periodEndOnOrAfter(PERIOD_OFFSET), PERIOD_OFFSET); } - function testperiodEndOnOrAfter_midPeriod() public { + function testPeriodEndOnOrAfter_midPeriod() public { assertEq( twabController.periodEndOnOrAfter(PERIOD_OFFSET + PERIOD_LENGTH / 2), PERIOD_OFFSET + PERIOD_LENGTH ); } - function testperiodEndOnOrAfter_firstPeriod() public { + function testPeriodEndOnOrAfter_firstPeriod() public { assertEq( twabController.periodEndOnOrAfter(PERIOD_OFFSET + PERIOD_LENGTH), PERIOD_OFFSET + PERIOD_LENGTH ); } - function testperiodEndOnOrAfter_secondPeriod() public { + function testPeriodEndOnOrAfter_secondPeriod() public { assertEq( twabController.periodEndOnOrAfter(PERIOD_OFFSET + PERIOD_LENGTH * 2), PERIOD_OFFSET + PERIOD_LENGTH * 2 @@ -300,6 +300,26 @@ contract TwabControllerTest is BaseTest { ); } + function testGetTimestampPeriod() public { + assertEq(twabController.getTimestampPeriod(PERIOD_OFFSET), 0); + assertEq(twabController.getTimestampPeriod(PERIOD_OFFSET-1), 0); + assertEq(twabController.getTimestampPeriod(PERIOD_OFFSET+PERIOD_LENGTH), 1); + } + + function testCurrentOverwritePeriodStartedAt_beginning() public { + assertEq(twabController.currentOverwritePeriodStartedAt(), PERIOD_OFFSET); + } + + function testCurrentOverwritePeriodStartedAt_partway() public { + vm.warp(PERIOD_OFFSET + PERIOD_LENGTH/2); + assertEq(twabController.currentOverwritePeriodStartedAt(), PERIOD_OFFSET); + } + + function testCurrentOverwritePeriodStartedAt_next() public { + vm.warp(PERIOD_OFFSET + PERIOD_LENGTH); + assertEq(twabController.currentOverwritePeriodStartedAt(), PERIOD_OFFSET + PERIOD_LENGTH); + } + /// It should not be possible for a twab measurement to change after the period has finalized. function testGetTotalSupplyTwabBetween_regression() public { uint32 secondPeriodStart = PERIOD_OFFSET + PERIOD_LENGTH; @@ -564,6 +584,23 @@ contract TwabControllerTest is BaseTest { assertEq(twabController.delegateBalanceOf(mockVault, SPONSORSHIP_ADDRESS), 0); } + function testMint_afterTimerange() public { + vm.startPrank(mockVault); + vm.warp(type(uint48).max); + twabController.mint(alice, 1000e18); + vm.warp(type(uint64).max); + assertEq(twabController.balanceOf(mockVault, alice), 1000e18); + assertEq(twabController.delegateBalanceOf(mockVault, alice), 1000e18); + assertEq(twabController.getBalanceAt(mockVault, alice, type(uint48).max), 0); + assertEq(twabController.getTotalSupplyAt(mockVault, type(uint48).max), 0); + + twabController.burn(alice, 1000e18); + assertEq(twabController.balanceOf(mockVault, alice), 0); + assertEq(twabController.delegateBalanceOf(mockVault, alice), 0); + + vm.stopPrank(); + } + function testMint() external { uint96 _amount = 1000e18; vm.expectEmit(true, true, false, true); @@ -576,7 +613,7 @@ contract TwabControllerTest is BaseTest { _amount, _amount, true, - ObservationLib.Observation({ cumulativeBalance: 0, balance: _amount, timestamp: uint32(block.timestamp) }) + ObservationLib.Observation({ cumulativeBalance: 0, balance: _amount, timestamp: uint32(block.timestamp) - PERIOD_OFFSET }) ); vm.expectEmit(true, false, false, true); @@ -588,7 +625,7 @@ contract TwabControllerTest is BaseTest { _amount, _amount, true, - ObservationLib.Observation({ cumulativeBalance: 0, balance: _amount, timestamp: uint32(block.timestamp) }) + ObservationLib.Observation({ cumulativeBalance: 0, balance: _amount, timestamp: uint32(block.timestamp) - PERIOD_OFFSET }) ); vm.startPrank(mockVault); @@ -620,7 +657,7 @@ contract TwabControllerTest is BaseTest { 0, 0, false, - ObservationLib.Observation({ cumulativeBalance: 0, balance: 0, timestamp: uint32(block.timestamp) }) + ObservationLib.Observation({ cumulativeBalance: 0, balance: 0, timestamp: uint32(block.timestamp) - PERIOD_OFFSET }) ); vm.expectEmit(true, false, false, true); @@ -632,7 +669,7 @@ contract TwabControllerTest is BaseTest { 0, 0, false, - ObservationLib.Observation({ cumulativeBalance: 0, balance: 0, timestamp: uint32(block.timestamp) }) + ObservationLib.Observation({ cumulativeBalance: 0, balance: 0, timestamp: uint32(block.timestamp) - PERIOD_OFFSET }) ); twabController.burn(alice, _amount); @@ -645,6 +682,36 @@ contract TwabControllerTest is BaseTest { vm.stopPrank(); } + function testMint_max() public { + vm.warp(100 days); + vm.startPrank(mockVault); + twabController.mint(alice, type(uint96).max); + uint256 currentTime = uint256(type(uint32).max) + 100 days; + console2.log("currentTime", currentTime); + // 4303607295 + vm.warp(currentTime); + twabController.burn(alice, 100); + // get the twab up until the very end (excluding last period so we don't have an endtime beyond the records) + assertEq(twabController.getTwabBetween(mockVault, alice, 100 days, uint256(type(uint32).max)+PERIOD_OFFSET-PERIOD_LENGTH), type(uint96).max); + vm.stopPrank(); + } + + + function testGetTwabBetween_max() public { + vm.warp(100 days); + vm.startPrank(mockVault); + twabController.mint(alice, type(uint96).max); + uint256 currentTime = uint256(type(uint32).max) + 100 days; + console2.log("currentTime", currentTime); + // 4303607295 + vm.warp(currentTime); + twabController.burn(alice, 100); + // get the twab up until the very end (excluding last period so we don't have an endtime beyond the records) + assertEq(twabController.getTwabBetween(mockVault, alice, 100 days, uint256(type(uint32).max)+PERIOD_OFFSET-PERIOD_LENGTH), type(uint96).max); + vm.stopPrank(); + } + + function testIsNewEvent() external { vm.startPrank(mockVault); @@ -659,7 +726,7 @@ contract TwabControllerTest is BaseTest { _amount, _amount, true, - ObservationLib.Observation({ cumulativeBalance: 0, balance: _amount, timestamp: uint32(block.timestamp) }) + ObservationLib.Observation({ cumulativeBalance: 0, balance: _amount, timestamp: uint32(block.timestamp) - PERIOD_OFFSET }) ); vm.expectEmit(true, false, false, true); @@ -671,7 +738,7 @@ contract TwabControllerTest is BaseTest { _amount, _amount, true, - ObservationLib.Observation({ cumulativeBalance: 0, balance: _amount, timestamp: uint32(block.timestamp) }) + ObservationLib.Observation({ cumulativeBalance: 0, balance: _amount, timestamp: uint32(block.timestamp) - PERIOD_OFFSET }) ); twabController.mint(alice, _amount); @@ -685,7 +752,7 @@ contract TwabControllerTest is BaseTest { 0, 0, false, - ObservationLib.Observation({ cumulativeBalance: 0, balance: 0, timestamp: uint32(block.timestamp) }) + ObservationLib.Observation({ cumulativeBalance: 0, balance: 0, timestamp: uint32(block.timestamp) - PERIOD_OFFSET }) ); vm.expectEmit(true, false, false, true); @@ -697,7 +764,7 @@ contract TwabControllerTest is BaseTest { 0, 0, false, - ObservationLib.Observation({ cumulativeBalance: 0, balance: 0, timestamp: uint32(block.timestamp) }) + ObservationLib.Observation({ cumulativeBalance: 0, balance: 0, timestamp: uint32(block.timestamp) - PERIOD_OFFSET }) ); twabController.burn(alice, _amount); vm.stopPrank(); @@ -913,7 +980,7 @@ contract TwabControllerTest is BaseTest { TwabLib.Account memory account = twabController.getAccount(vault, user); assertEq( account.observations[observation.expectedIndex].timestamp, - observation.timestamp, + observation.timestamp - PERIOD_OFFSET, "timestamp" ); assertEq( @@ -939,10 +1006,10 @@ contract TwabControllerTest is BaseTest { vm.warp(observation.timestamp); twabController.burn(user, observation.amount); TwabLib.Account memory account = twabController.getAccount(vault, user); - assertEq(account.observations[observation.expectedIndex].timestamp, observation.timestamp); - assertEq(account.observations[observation.expectedIndex + 1].timestamp, 0); - assertEq(account.details.nextObservationIndex, observation.expectedIndex + 1); - assertEq(account.details.cardinality, observation.expectedIndex + 1); + assertEq(account.observations[observation.expectedIndex].timestamp, observation.timestamp - PERIOD_OFFSET, "burnAndValidate timestamp"); + assertEq(account.observations[observation.expectedIndex + 1].timestamp, 0, "burnAndValidate next timestamp"); + assertEq(account.details.nextObservationIndex, observation.expectedIndex + 1, "burnAndValidate next observation index"); + assertEq(account.details.cardinality, observation.expectedIndex + 1, "burnAndValidate cardinality"); vm.stopPrank(); } @@ -1028,7 +1095,7 @@ contract TwabControllerTest is BaseTest { 86500e18, "second obs has 1 period of prev bal" ); - assertEq(account.observations[1].timestamp, t1 + 100, "second obs has 1 period of prev bal"); + assertEq(account.observations[1].timestamp, t1 + 100 - PERIOD_OFFSET, "second obs has 1 period of prev bal"); assertEq( account.observations[2].cumulativeBalance, @@ -1037,7 +1104,7 @@ contract TwabControllerTest is BaseTest { ); assertEq( account.observations[2].timestamp, - t2 + (periodTenth * 9), + t2 + (periodTenth * 9) - PERIOD_OFFSET, "third period has last timestamp" ); @@ -1081,28 +1148,28 @@ contract TwabControllerTest is BaseTest { assertEq( newestObservation.timestamp, - PERIOD_OFFSET + (PERIOD_LENGTH * (MAX_CARDINALITY)), + (PERIOD_LENGTH * (MAX_CARDINALITY)), "newest timestamp" ); assertEq(newestIndex, 0, "newest index"); (oldestIndex, oldestObservation) = twabController.getOldestObservation(mockVault, alice); - assertEq(oldestObservation.timestamp, PERIOD_OFFSET + PERIOD_LENGTH, "oldest timestamp"); + assertEq(oldestObservation.timestamp, PERIOD_LENGTH, "oldest timestamp"); assertEq(oldestIndex, 1, "oldest index"); - vm.warp(newestObservation.timestamp + PERIOD_LENGTH); + vm.warp(newestObservation.timestamp + PERIOD_LENGTH + PERIOD_OFFSET); uint256 aliceTwab = twabController.getTwabBetween( mockVault, alice, - oldestObservation.timestamp, - newestObservation.timestamp + oldestObservation.timestamp + PERIOD_OFFSET, + newestObservation.timestamp + PERIOD_OFFSET ); uint256 totalSupplyTwab = twabController.getTotalSupplyTwabBetween( mockVault, - oldestObservation.timestamp, - newestObservation.timestamp + oldestObservation.timestamp + PERIOD_OFFSET, + newestObservation.timestamp + PERIOD_OFFSET ); assertEq(aliceTwab, totalSupplyTwab); @@ -1138,12 +1205,12 @@ contract TwabControllerTest is BaseTest { (newestIndex, newestObservation) = twabController.getNewestTotalSupplyObservation(mockVault); - assertEq(newestObservation.timestamp, PERIOD_OFFSET + (PERIOD_LENGTH * (MAX_CARDINALITY))); + assertEq(newestObservation.timestamp, (PERIOD_LENGTH * (MAX_CARDINALITY))); assertEq(newestIndex, 0); (oldestIndex, oldestObservation) = twabController.getOldestObservation(mockVault, alice); - assertEq(oldestObservation.timestamp, PERIOD_OFFSET + PERIOD_LENGTH); + assertEq(oldestObservation.timestamp, PERIOD_LENGTH); assertEq(oldestIndex, 1); vm.stopPrank(); @@ -1204,7 +1271,7 @@ contract TwabControllerTest is BaseTest { drawEnd + PERIOD_LENGTH * 2 ); - assertEq(manipulatedDrawBalance, actualDrawBalance); + assertEq(manipulatedDrawBalance, actualDrawBalance, "draw balance"); } function testImmediateWithdrawalMitigation() external { diff --git a/test/TwabLib.t.sol b/test/TwabLib.t.sol index 09b3646..9abe345 100644 --- a/test/TwabLib.t.sol +++ b/test/TwabLib.t.sol @@ -3,7 +3,14 @@ pragma solidity ^0.8.19; import { console2 } from "forge-std/console2.sol"; -import { TwabLib, BalanceLTAmount, DelegateBalanceLTAmount, TimestampNotFinalized, InsufficientHistory } from "../src/libraries/TwabLib.sol"; +import { + TwabLib, + BalanceLTAmount, + DelegateBalanceLTAmount, + TimestampNotFinalized, + InsufficientHistory, + InvalidTimeRange +} from "../src/libraries/TwabLib.sol"; import { ObservationLib, MAX_CARDINALITY } from "../src/libraries/ObservationLib.sol"; import { BaseTest } from "./utils/BaseTest.sol"; @@ -83,7 +90,7 @@ contract TwabLibTest is BaseTest { assertEq(accountDetails.nextObservationIndex, 1); assertEq(accountDetails.cardinality, 1); assertEq(_observation.cumulativeBalance, 0); - assertEq(_observation.timestamp, _currentTimestamp); + assertEq(_observation.timestamp, _currentTimestamp - PERIOD_OFFSET); assertTrue(_isNew); assertTrue(_isRecorded); @@ -111,11 +118,11 @@ contract TwabLibTest is BaseTest { assertEq(accountDetails.balance, 0); assertEq(accountDetails.delegateBalance, _totalAmount); - assertEq(accountDetails.nextObservationIndex, 1); - assertEq(accountDetails.cardinality, 1); + assertEq(accountDetails.nextObservationIndex, 1, "next observation index"); + assertEq(accountDetails.cardinality, 1, "cardinality"); assertEq(_observation.cumulativeBalance, 0); assertTrue(_isRecorded); - assertEq(_observation.timestamp, _currentTimestamp); + assertEq(_observation.timestamp, _currentTimestamp - PERIOD_OFFSET); assertFalse(_isNew); vm.warp(_currentTimestamp + PERIOD_LENGTH); @@ -142,7 +149,7 @@ contract TwabLibTest is BaseTest { assertEq(accountDetails.nextObservationIndex, 1); assertEq(accountDetails.cardinality, 1); assertEq(_observation1.cumulativeBalance, 0); - assertEq(_observation1.timestamp, _initialTimestamp); + assertEq(_observation1.timestamp, _initialTimestamp - PERIOD_OFFSET); assertTrue(_isNew); vm.warp(_initialTimestamp + PERIOD_LENGTH); @@ -166,13 +173,25 @@ contract TwabLibTest is BaseTest { _observation2.timestamp - _observation1.timestamp ) ); - assertEq(_observation2.timestamp, _secondTimestamp); + assertEq(_observation2.timestamp, _secondTimestamp - PERIOD_OFFSET); assertTrue(_isNew); vm.warp(_secondTimestamp + PERIOD_LENGTH); assertEq(twabLibMock.getBalanceAt(_secondTimestamp), _totalAmount); } + function testFlashIncreaseDecrease() public { + vm.warp(PERIOD_OFFSET); + twabLibMock.flashBalance(1000e18, 1000e18); + twabLibMock.flashBalance(1000e18, 1000e18); + TwabLib.AccountDetails memory details = twabLibMock.getAccountDetails(); + assertEq(details.balance, 0); + assertEq(details.delegateBalance, 0); + assertEq(details.cardinality, 1); + vm.warp(PERIOD_OFFSET + PERIOD_LENGTH); + assertEq(twabLibMock.getTwabBetween(PERIOD_OFFSET, PERIOD_OFFSET + PERIOD_LENGTH), 0); + } + /* ============ decreaseBalances ============ */ function testDecreaseBalanceHappyPath() public { @@ -233,7 +252,7 @@ contract TwabLibTest is BaseTest { _computeCumulativeBalance(0, _amount, DRAW_LENGTH), "cumulative balance remains the same" ); - assertEq(_observation.timestamp, _secondTimestamp, "observation timestamp is correct"); + assertEq(_observation.timestamp, _secondTimestamp - PERIOD_OFFSET, "observation timestamp is correct"); assertTrue(_isNew, "was a new observation"); assertEq( @@ -266,7 +285,7 @@ contract TwabLibTest is BaseTest { assertEq(accountDetails.nextObservationIndex, 1); assertEq(accountDetails.cardinality, 1); assertEq(_observation.cumulativeBalance, 0); - assertEq(_observation.timestamp, _initialTimestamp); + assertEq(_observation.timestamp, _initialTimestamp - PERIOD_OFFSET); assertTrue(_isNew); vm.warp(_secondTimestamp); @@ -318,7 +337,7 @@ contract TwabLibTest is BaseTest { assertEq(accountDetails.nextObservationIndex, 1); assertEq(accountDetails.cardinality, 1); assertEq(_observation.cumulativeBalance, 0); - assertEq(_observation.timestamp, _initialTimestamp); + assertEq(_observation.timestamp, _initialTimestamp - PERIOD_OFFSET); assertTrue(_isNew); vm.warp(_secondTimestamp); @@ -334,7 +353,7 @@ contract TwabLibTest is BaseTest { assertEq(accountDetails.nextObservationIndex, 2); assertEq(accountDetails.cardinality, 2); assertEq(_observation2.cumulativeBalance, _computeCumulativeBalance(0, _amount, DRAW_LENGTH)); - assertEq(_observation2.timestamp, _secondTimestamp); + assertEq(_observation2.timestamp, _secondTimestamp - PERIOD_OFFSET); assertTrue(_isNew); vm.warp(_thirdTimestamp); @@ -352,7 +371,7 @@ contract TwabLibTest is BaseTest { _observation.cumulativeBalance, _computeCumulativeBalance(_observation2.cumulativeBalance, _halfAmount, DRAW_LENGTH) ); - assertEq(_observation.timestamp, _thirdTimestamp); + assertEq(_observation.timestamp, _thirdTimestamp - PERIOD_OFFSET); assertTrue(_isNew); } @@ -401,7 +420,7 @@ contract TwabLibTest is BaseTest { assertEq(_oldestIndex, 0); assertEq(_oldestObservation.cumulativeBalance, 0); - assertEq(_oldestObservation.timestamp, _initialTimestamp); + assertEq(_oldestObservation.timestamp, _initialTimestamp - PERIOD_OFFSET); assertEq(_newestIndex, 2); assertEq( _newestObservation.cumulativeBalance, @@ -411,7 +430,7 @@ contract TwabLibTest is BaseTest { _newestObservation.timestamp - _secondNewestObservation.timestamp ) ); - assertEq(_newestObservation.timestamp, _thirdTimestamp); + assertEq(_newestObservation.timestamp, _thirdTimestamp - PERIOD_OFFSET); } /* ============ getTwabBetween ============ */ @@ -431,6 +450,24 @@ contract TwabLibTest is BaseTest { twabLibMock.increaseBalances(0, amount); } + function testGetTwabBetween_InvalidTimeRange() public { + vm.warp(PERIOD_OFFSET); + vm.expectRevert(abi.encodeWithSelector(InvalidTimeRange.selector, 1000, 100)); + twabLibMock.getTwabBetween(1000, 100); + } + + function testGetTwabBetween_startAfterTimerange() public { + vm.warp(2*uint(type(uint32).max)); + uint256 startTime = PERIOD_OFFSET+uint(type(uint32).max)+1; + assertEq(twabLibMock.getTwabBetween(startTime, startTime + PERIOD_LENGTH), 0); + } + + function testGetTwabBetween_endAfterTimerange() public { + vm.warp(2*uint(type(uint32).max)); + uint256 startTime = PERIOD_OFFSET+uint(type(uint32).max); + assertEq(twabLibMock.getTwabBetween(startTime, startTime + PERIOD_LENGTH), 0); + } + function testGetTwabBetween_start_and_end_same_time() public { vm.warp(PERIOD_OFFSET); twabLibMock.increaseBalances(0, 1000e18); @@ -687,13 +724,25 @@ contract TwabLibTest is BaseTest { assertEq(twabLibMock.getBalanceAt(PERIOD_OFFSET + PERIOD_LENGTH), 2e18); } + function testGetBalanceAt_endOfTimerange() public { + twabLibMock.increaseBalances(0, 1e18); + vm.warp(type(uint48).max); + assertEq(twabLibMock.getBalanceAt(PERIOD_OFFSET + uint(type(uint32).max)), 1e18); + } + + function testGetBalanceAt_outOfTimerange() public { + twabLibMock.increaseBalances(0, 1e18); + vm.warp(type(uint48).max); + assertEq(twabLibMock.getBalanceAt(PERIOD_OFFSET + uint(type(uint32).max) + 1), 0); + } + function testGetBalanceAt_InsufficientHistory() public { fillObservationsBuffer(); vm.expectRevert( abi.encodeWithSelector( InsufficientHistory.selector, - PERIOD_OFFSET, - PERIOD_OFFSET + PERIOD_LENGTH + 0, + PERIOD_LENGTH ) ); twabLibMock.getBalanceAt(PERIOD_OFFSET); @@ -825,24 +874,33 @@ contract TwabLibTest is BaseTest { function testGetPreviousOrAtObservation_single_before() public { uint32 t0 = PERIOD_OFFSET; twabLibMock.increaseBalances(0, 1e18); - assertEq(twabLibMock.getPreviousOrAtObservation(t0 - 1).timestamp, t0 - 1); + assertEq(twabLibMock.getPreviousOrAtObservation(t0 - 1).timestamp, 0); } function testGetPreviousOrAtObservation_single_at() public { uint32 t0 = PERIOD_OFFSET; twabLibMock.increaseBalances(0, 1e18); ObservationLib.Observation memory obs = twabLibMock.getPreviousOrAtObservation(t0); - assertEq(obs.timestamp, t0); + assertEq(obs.timestamp, 0); } function testGetPreviousOrAtObservation_single_after() public { uint32 t0 = PERIOD_OFFSET; twabLibMock.increaseBalances(0, 1e18); ObservationLib.Observation memory obs = twabLibMock.getPreviousOrAtObservation(t0 + 1); - assertEq(obs.timestamp, t0); + assertEq(obs.timestamp, 0); + } + + function testGetPreviousOrAtObservation_after_lastTime() public { + twabLibMock.increaseBalances(0, 1e18); + vm.warp(PERIOD_OFFSET+uint256(type(uint32).max)+1); + ObservationLib.Observation memory obs = twabLibMock.getPreviousOrAtObservation(block.timestamp); + assertEq(obs.balance, 0); + assertEq(obs.cumulativeBalance, 0); + assertEq(obs.timestamp, type(uint32).max); } - function testGetPreviousOrAtObservation() public { + function testGetPreviousOrAtObservation_complex() public { uint32 t0 = PERIOD_OFFSET; uint32 t1 = PERIOD_OFFSET + PERIOD_LENGTH; uint32 t2 = PERIOD_OFFSET + (PERIOD_LENGTH * 2); @@ -853,15 +911,15 @@ contract TwabLibTest is BaseTest { twabLibMock.increaseBalances(1, 1); // Get observation at timestamp before first observation - assertEq(twabLibMock.getPreviousOrAtObservation(t0 - 1 seconds).timestamp, t0 - 1); + assertEq(twabLibMock.getPreviousOrAtObservation(t0 - 1 seconds).timestamp, 0); // Get observation at first timestamp prevOrAtObservation = twabLibMock.getPreviousOrAtObservation(t0); - assertEq(prevOrAtObservation.timestamp, t0); + assertEq(prevOrAtObservation.timestamp, 0); // Get observation after first timestamp prevOrAtObservation = twabLibMock.getPreviousOrAtObservation(t0 + 1 seconds); - assertEq(prevOrAtObservation.timestamp, t0); + assertEq(prevOrAtObservation.timestamp, 0); vm.warp(t1); twabLibMock.increaseBalances(1, 1); @@ -872,47 +930,47 @@ contract TwabLibTest is BaseTest { // Get observation at timestamp before first observation prevOrAtObservation = twabLibMock.getPreviousOrAtObservation(t0 - 1 seconds); - assertEq(prevOrAtObservation.timestamp, t0 - 1 seconds, "before first period"); + assertEq(prevOrAtObservation.timestamp, 0, "before first period"); // Get observation at first timestamp prevOrAtObservation = twabLibMock.getPreviousOrAtObservation(t0); - assertEq(prevOrAtObservation.timestamp, t0, "start of first period"); + assertEq(prevOrAtObservation.timestamp, 0, "start of first period"); // Get observation between first and second timestamp prevOrAtObservation = twabLibMock.getPreviousOrAtObservation(t1 - 1 seconds); - assertEq(prevOrAtObservation.timestamp, t0, "end of first period"); + assertEq(prevOrAtObservation.timestamp, 0, "end of first period"); // Get observation at second timestamp prevOrAtObservation = twabLibMock.getPreviousOrAtObservation(t1); - assertEq(prevOrAtObservation.timestamp, t1); + assertEq(prevOrAtObservation.timestamp, PERIOD_LENGTH); // Get observation between second and third timestamp prevOrAtObservation = twabLibMock.getPreviousOrAtObservation(t1 + 1 seconds); - assertEq(prevOrAtObservation.timestamp, t1); + assertEq(prevOrAtObservation.timestamp, PERIOD_LENGTH); // Get observation between second and third timestamp prevOrAtObservation = twabLibMock.getPreviousOrAtObservation(t2 - 1 seconds); - assertEq(prevOrAtObservation.timestamp, t1); + assertEq(prevOrAtObservation.timestamp, PERIOD_LENGTH); // Get observation at third timestamp prevOrAtObservation = twabLibMock.getPreviousOrAtObservation(t2); - assertEq(prevOrAtObservation.timestamp, t2); + assertEq(prevOrAtObservation.timestamp, PERIOD_LENGTH*2); // Get observation between third and fourth timestamp prevOrAtObservation = twabLibMock.getPreviousOrAtObservation(t2 + 1 seconds); - assertEq(prevOrAtObservation.timestamp, t2); + assertEq(prevOrAtObservation.timestamp, PERIOD_LENGTH*2); // Get observation between third and fourth timestamp prevOrAtObservation = twabLibMock.getPreviousOrAtObservation(t3 - 1 seconds); - assertEq(prevOrAtObservation.timestamp, t2); + assertEq(prevOrAtObservation.timestamp, PERIOD_LENGTH*2); // Get observation at fourth timestamp prevOrAtObservation = twabLibMock.getPreviousOrAtObservation(t3); - assertEq(prevOrAtObservation.timestamp, t3); + assertEq(prevOrAtObservation.timestamp, PERIOD_LENGTH*3); // Get observation after fourth timestamp prevOrAtObservation = twabLibMock.getPreviousOrAtObservation(t3 + 1 seconds); - assertEq(prevOrAtObservation.timestamp, t3); + assertEq(prevOrAtObservation.timestamp, PERIOD_LENGTH*3); } function testGetPreviousOrAtObservation_EmptyBuffer() public { @@ -920,7 +978,7 @@ contract TwabLibTest is BaseTest { PERIOD_OFFSET ); - assertEq(prevOrAtObservation.timestamp, PERIOD_OFFSET); + assertEq(prevOrAtObservation.timestamp, 0); } function testGetPreviousOrAtObservation_FullBuffer() public { @@ -931,8 +989,8 @@ contract TwabLibTest is BaseTest { vm.expectRevert( abi.encodeWithSelector( InsufficientHistory.selector, - PERIOD_OFFSET, - PERIOD_OFFSET + PERIOD_LENGTH + 0, + PERIOD_LENGTH ) ); twabLibMock.getPreviousOrAtObservation(PERIOD_OFFSET); @@ -941,41 +999,41 @@ contract TwabLibTest is BaseTest { (, ObservationLib.Observation memory oldestObservation) = twabLibMock.getOldestObservation(); // Get at oldest observation - prevOrAtObservation = twabLibMock.getPreviousOrAtObservation(oldestObservation.timestamp); + prevOrAtObservation = twabLibMock.getPreviousOrAtObservation(oldestObservation.timestamp + PERIOD_OFFSET); assertEq(prevOrAtObservation.timestamp, oldestObservation.timestamp); - assertGe(prevOrAtObservation.timestamp, PERIOD_OFFSET); + assertGe(prevOrAtObservation.timestamp, 0); // Get after oldest observation prevOrAtObservation = twabLibMock.getPreviousOrAtObservation( - oldestObservation.timestamp + 1 seconds + oldestObservation.timestamp + 1 seconds + PERIOD_OFFSET ); assertEq(prevOrAtObservation.timestamp, oldestObservation.timestamp); - assertGe(prevOrAtObservation.timestamp, PERIOD_OFFSET); + assertGe(prevOrAtObservation.timestamp, 0); // Get observation somewhere in the middle. uint32 t = PERIOD_OFFSET + (PERIOD_LENGTH * 100); prevOrAtObservation = twabLibMock.getPreviousOrAtObservation(t); - assertEq(prevOrAtObservation.timestamp, t); - assertGe(prevOrAtObservation.timestamp, PERIOD_OFFSET); + assertEq(prevOrAtObservation.timestamp, t - PERIOD_OFFSET); + assertGe(prevOrAtObservation.timestamp, 0); // Get observation right before somewhere in the middle. t -= 1 seconds; prevOrAtObservation = twabLibMock.getPreviousOrAtObservation(t); - assertEq(prevOrAtObservation.timestamp, PERIOD_OFFSET + (PERIOD_LENGTH * 99)); - assertGe(prevOrAtObservation.timestamp, PERIOD_OFFSET); + assertEq(prevOrAtObservation.timestamp, (PERIOD_LENGTH * 99)); + assertGe(prevOrAtObservation.timestamp, 0); // Get newest observation. (, ObservationLib.Observation memory newestObservation) = twabLibMock.getNewestObservation(); - prevOrAtObservation = twabLibMock.getPreviousOrAtObservation(newestObservation.timestamp); + prevOrAtObservation = twabLibMock.getPreviousOrAtObservation(newestObservation.timestamp + PERIOD_OFFSET); assertEq(prevOrAtObservation.timestamp, newestObservation.timestamp); - assertGe(prevOrAtObservation.timestamp, PERIOD_OFFSET); + assertGe(prevOrAtObservation.timestamp, 0); // Get before newest observation. prevOrAtObservation = twabLibMock.getPreviousOrAtObservation( - newestObservation.timestamp - 1 seconds + newestObservation.timestamp - 1 seconds + PERIOD_OFFSET ); assertEq(prevOrAtObservation.timestamp, newestObservation.timestamp - PERIOD_LENGTH); - assertGe(prevOrAtObservation.timestamp, PERIOD_OFFSET); + assertGe(prevOrAtObservation.timestamp, 0); } // ================== getNextOrNewestObservation ================== diff --git a/test/invariants/handlers/TwabControllerHandler.sol b/test/invariants/handlers/TwabControllerHandler.sol index a369a36..29eeede 100644 --- a/test/invariants/handlers/TwabControllerHandler.sol +++ b/test/invariants/handlers/TwabControllerHandler.sol @@ -279,7 +279,7 @@ contract TwabControllerHandler is CommonBase, StdCheats, StdUtils { } // Check if each period end timestamp is safe for this vault - uint32 newestPeriod = twabController.getTimestampPeriod(newestObservation.timestamp); + uint256 newestPeriod = twabController.getTimestampPeriod(newestObservation.timestamp); for (uint32 p_i = 1; p_i < newestPeriod; ++p_i) { uint32 timestamp = PERIOD_OFFSET + (p_i * PERIOD_LENGTH); isVaultsSafe = isVaultsSafe && twabController.hasFinalized(timestamp); diff --git a/test/mocks/ObservationLibMock.sol b/test/mocks/ObservationLibMock.sol index abd20c3..41255cc 100644 --- a/test/mocks/ObservationLibMock.sol +++ b/test/mocks/ObservationLibMock.sol @@ -43,7 +43,6 @@ contract ObservationLibMock { * @param _oldestObservationIndex Index of the oldest Observation. Left side of the circular buffer. * @param _target Timestamp at which we are searching the Observation. * @param _cardinality Cardinality of the circular buffer we are searching through. - * @param _currentTime Current time * @return Observation recorded before, or at, the target. * @return Observation recorded at, or after, the target. */ @@ -51,8 +50,7 @@ contract ObservationLibMock { uint24 _newestObservationIndex, uint24 _oldestObservationIndex, uint32 _target, - uint16 _cardinality, - uint32 _currentTime + uint16 _cardinality ) external view @@ -68,8 +66,7 @@ contract ObservationLibMock { _newestObservationIndex, _oldestObservationIndex, _target, - _cardinality, - _currentTime + _cardinality ); return (beforeOrAt, beforeOrAtIndex, afterOrAt, afterOrAtIndex); } diff --git a/test/mocks/TwabLibMock.sol b/test/mocks/TwabLibMock.sol index 48870c9..ffbdc6b 100644 --- a/test/mocks/TwabLibMock.sol +++ b/test/mocks/TwabLibMock.sol @@ -12,6 +12,14 @@ contract TwabLibMock { using TwabLib for ObservationLib.Observation[MAX_CARDINALITY]; TwabLib.Account public account; + function flashBalance( + uint96 _amount, + uint96 _delegateAmount + ) external { + TwabLib.increaseBalances(PERIOD_LENGTH, PERIOD_OFFSET, account, _amount, _delegateAmount); + TwabLib.decreaseBalances(PERIOD_LENGTH, PERIOD_OFFSET, account, _amount, _delegateAmount, "decrease"); + } + function increaseBalances( uint96 _amount, uint96 _delegateAmount @@ -54,7 +62,7 @@ contract TwabLibMock { return (observation, isNewObservation, isObservationRecorded, accountDetails); } - function getTwabBetween(uint32 _startTime, uint32 _endTime) external view returns (uint256) { + function getTwabBetween(uint256 _startTime, uint256 _endTime) external view returns (uint256) { uint256 averageBalance = TwabLib.getTwabBetween( PERIOD_LENGTH, PERIOD_OFFSET, @@ -67,7 +75,7 @@ contract TwabLibMock { } function getPreviousOrAtObservation( - uint32 _targetTime + uint256 _targetTime ) external view returns (ObservationLib.Observation memory) { ObservationLib.Observation memory prevOrAtObservation = TwabLib.getPreviousOrAtObservation( PERIOD_OFFSET, @@ -102,7 +110,7 @@ contract TwabLibMock { return (index, observation); } - function getBalanceAt(uint32 _targetTime) external view returns (uint256) { + function getBalanceAt(uint256 _targetTime) external view returns (uint256) { uint256 balance = TwabLib.getBalanceAt( PERIOD_LENGTH, PERIOD_OFFSET, @@ -121,30 +129,30 @@ contract TwabLibMock { return account.details; } - function getTimestampPeriod(uint32 _timestamp) external view returns (uint32) { - uint32 timestamp = TwabLib.getTimestampPeriod(PERIOD_LENGTH, PERIOD_OFFSET, _timestamp); + function getTimestampPeriod(uint32 _timestamp) external view returns (uint256) { + uint256 timestamp = TwabLib.getTimestampPeriod(PERIOD_LENGTH, PERIOD_OFFSET, _timestamp); return timestamp; } - function getPeriodStartTime(uint32 _period) external view returns (uint32) { - uint32 start = TwabLib.getPeriodStartTime(PERIOD_LENGTH, PERIOD_OFFSET, _period); + function getPeriodStartTime(uint256 _period) external view returns (uint256) { + uint256 start = TwabLib.getPeriodStartTime(PERIOD_LENGTH, PERIOD_OFFSET, _period); return start; } - function getPeriodEndTime(uint32 _period) external view returns (uint32) { - uint32 end = TwabLib.getPeriodEndTime(PERIOD_LENGTH, PERIOD_OFFSET, _period); + function getPeriodEndTime(uint256 _period) external view returns (uint256) { + uint256 end = TwabLib.getPeriodEndTime(PERIOD_LENGTH, PERIOD_OFFSET, _period); return end; } function currentOverwritePeriodStartedAt( uint32 _PERIOD_LENGTH, uint32 _PERIOD_OFFSET - ) external view returns (uint32) { - uint32 start = TwabLib.currentOverwritePeriodStartedAt(_PERIOD_LENGTH, _PERIOD_OFFSET); + ) external view returns (uint256) { + uint256 start = TwabLib.currentOverwritePeriodStartedAt(_PERIOD_LENGTH, _PERIOD_OFFSET); return start; } - function hasFinalized(uint32 _timestamp) external view returns (bool) { + function hasFinalized(uint256 _timestamp) external view returns (bool) { bool isSafe = TwabLib.hasFinalized(PERIOD_LENGTH, PERIOD_OFFSET, _timestamp); return isSafe; }