From d684168221812a8041e3c839cb48cadee0c20ca1 Mon Sep 17 00:00:00 2001 From: jordaniza Date: Fri, 25 Oct 2024 14:12:59 +0400 Subject: [PATCH] refinements to test suite --- TOTAL_SUPPLY.md | 28 +- .../increasing/LinearIncreasingEscrow.sol | 352 +++++------ .../linear/LinearApplyTokenToGlobal.t.sol | 65 +- test/escrow/curve/linear/LinearBase.sol | 29 +- .../curve/linear/LinearCheckpoint.t.sol | 258 ++++---- .../curve/linear/LinearEscrowTime.t.sol | 142 +++++ .../curve/linear/LinearTokenCheckpoint.t.sol | 592 +++++------------- .../curve/linear/LinearVotingPower.t.sol | 241 +++++++ .../curve/linear/LinearWriteHistory.t.sol | 39 +- 9 files changed, 971 insertions(+), 775 deletions(-) create mode 100644 test/escrow/curve/linear/LinearEscrowTime.t.sol create mode 100644 test/escrow/curve/linear/LinearVotingPower.t.sol diff --git a/TOTAL_SUPPLY.md b/TOTAL_SUPPLY.md index 656a943..641d3ef 100644 --- a/TOTAL_SUPPLY.md +++ b/TOTAL_SUPPLY.md @@ -102,12 +102,30 @@ Getting ready for live Minimum +time wise - priotise for now + +1. change the token checkpoint to only take in a lock - done +2. add the remaining test functions to ensure we've covered ourselves + - Token unit - todo tonight + - elapsed thingy + - Warmup: big test -done + - Voting Power: this can be done in the big test as well done +3. Test the revert conditions with a second big test +4. check the manual checkpoint works as expected + +Then optimise can come tomorrow + 1. Fix all the tests -2. Add the binary search for total supply + - decide on implementation - leaving for now + - unit test some of the other functions - done + - adjust the token tests +2. Add the binary search for total supply - done 3. Run the regression test and adjust the escrow contract -4. Fix the bug with the warmup -5. Check all the reverts and fix the revert conditions -6. Test the manual checkpoint +4. Fix the bug with the warmup - needs testing +5. Check all the revert - done +6. fix the revert conditions +7. Test the manual checkpoint +8. Review all the comments Optimise @@ -125,3 +143,5 @@ Document 1. Visual Doc 2. README + +Qus: New locked start passed to the update - what if that changes? diff --git a/src/escrow/increasing/LinearIncreasingEscrow.sol b/src/escrow/increasing/LinearIncreasingEscrow.sol index 8d573b3..e200280 100644 --- a/src/escrow/increasing/LinearIncreasingEscrow.sol +++ b/src/escrow/increasing/LinearIncreasingEscrow.sol @@ -53,25 +53,36 @@ contract LinearIncreasingEscrow is /// @dev tokenId => tokenPointIntervals => TokenPoint /// @dev The Array is fixed so we can write to it in the future - /// This implementation means that very short intervals may be challenging mapping(uint256 => TokenPoint[1_000_000_000]) internal _tokenPointHistory; - /// ADDED v0.1.1 + /*////////////////////////////////////////////////////////////// + ADDED: 0.2.0 + //////////////////////////////////////////////////////////////*/ - mapping(uint => GlobalPoint) internal _pointHistory; + /// @dev Global state snapshots in the past + /// pointIndex => GlobalPoint + mapping(uint256 => GlobalPoint) internal _pointHistory; - uint256 internal _latestPointIndex; - - // changes are stored in fixed point + /// @dev Scheduled adjustments to the curve at points in the future + /// interval timestamp => [bias, slope, 0] mapping(uint48 => int256[3]) internal _scheduledCurveChanges; + /// @dev The latest global point index. + uint256 internal _latestPointIndex; + + /// @dev Written to when the first deposit is made in the future + /// this is used to determine when to begin writing global points uint48 internal _earliestScheduledChange; /// @notice emulation of array like structure starting at 1 index for global points + /// @return The state snapshot at the given index. + /// @dev Points are written at least weekly. function pointHistory(uint256 _index) external view returns (GlobalPoint memory) { return _pointHistory[_index]; } + /// @notice Returns the scheduled bias and slope changes at a given timestamp + /// @param _at the timestamp to check, can be in the future, but such values can change. function scheduledCurveChanges(uint48 _at) external view returns (int256[3] memory) { return [ SignedFixedPointMath.fromFP(_scheduledCurveChanges[_at][0]), @@ -80,10 +91,12 @@ contract LinearIncreasingEscrow is ]; } + /// @notice How many GlobalPoints have been written, starting at 1. function latestPointIndex() external view returns (uint) { return _latestPointIndex; } + /// @notice The amount of time locks can accumulate voting power for since the start. function maxTime() external view returns (uint48) { return _maxTime().toUint48(); } @@ -167,30 +180,27 @@ contract LinearIncreasingEscrow is return _getBias(timeElapsed, coefficients); } - /// @dev returns the bias ignoring negative values and not converting from fp + /// @dev returns the bias ignoring negative values, maximum bounding and fixed point conversion function _getBiasUnbound( uint256 timeElapsed, int256[3] memory coefficients - ) internal view returns (int256) { + ) internal pure returns (int256) { int256 linear = coefficients[1]; int256 const = coefficients[0]; - // bound the time elapsed to the maximum time - // TODO technically redundant if we use the bounding function and pass in time elapsed - uint256 MAX_TIME = _maxTime(); - timeElapsed = timeElapsed > MAX_TIME ? MAX_TIME : timeElapsed; - // convert the time to fixed point int256 t = SignedFixedPointMath.toFP(timeElapsed.toInt256()); - int256 bias = linear.mul(t).add(const); - return bias; + return linear.mul(t).add(const); } function _getBias( uint256 timeElapsed, int256[3] memory coefficients ) internal view returns (uint256) { + uint256 MAX_TIME = _maxTime(); + timeElapsed = timeElapsed > MAX_TIME ? MAX_TIME : timeElapsed; + int256 bias = _getBiasUnbound(timeElapsed, coefficients); // never return negative values @@ -210,16 +220,22 @@ contract LinearIncreasingEscrow is Warmup //////////////////////////////////////////////////////////////*/ + /// @notice Set the warmup period for the curve. Voting power accrues silently during this period. function setWarmupPeriod(uint48 _warmupPeriod) external auth(CURVE_ADMIN_ROLE) { warmupPeriod = _warmupPeriod; emit WarmupSet(_warmupPeriod); } - /// @notice Returns whether the NFT is warm based on the first point - function isWarm(uint256 tokenId) public view returns (bool) { - TokenPoint memory point = _tokenPointHistory[tokenId][1]; - if (point.bias == 0) return false; - else return block.timestamp > point.writtenTs + warmupPeriod; + /// @notice Returns whether the NFT is currently warm based on the first point + function isWarm(uint256 _tokenId) external view returns (bool) { + return isWarmAt(_tokenId, block.timestamp); + } + + /// @notice Returns whether the NFT is warm based on the first point, for a given timestamp + function isWarmAt(uint256 _tokenId, uint256 _timestamp) public view returns (bool) { + TokenPoint memory point = _tokenPointHistory[_tokenId][1]; + if (point.coefficients[0] == 0) return false; + else return _timestamp > point.writtenTs + warmupPeriod; } /*////////////////////////////////////////////////////////////// @@ -298,23 +314,26 @@ contract LinearIncreasingEscrow is return lower; } - function votingPowerAt(uint256 _tokenId, uint256 _t) external view returns (uint256) { - uint256 interval = _getPastTokenPointInterval(_tokenId, _t); - + /// @notice Calculate voting power at some point in the past for a given tokenId + /// @return votingPower The voting power at that time, if the first point is in warmup, returns 0 + function votingPowerAt(uint256 _tokenId, uint256 _timestamp) external view returns (uint256) { + uint256 interval = _getPastTokenPointInterval(_tokenId, _timestamp); // epoch 0 is an empty point if (interval == 0) return 0; - // check the warmup status of the token - if (!isWarm(_tokenId)) return 0; + // check the warmup status of the token (first point only) + if (!isWarmAt(_tokenId, _timestamp)) return 0; // fetch the start time of the lock uint start = IVotingEscrow(escrow).locked(_tokenId).start; // calculate the bounded elapsed time since the point, factoring in the original start TokenPoint memory lastPoint = _tokenPointHistory[_tokenId][interval]; - uint256 timeElapsed = _getBoundedElasedSinceLastPoint( + + uint256 timeElapsed = boundedTimeSinceCheckpoint( uint48(start), - lastPoint.checkpointTs + lastPoint.checkpointTs, + uint48(_timestamp) ); // the bias here is converted from fixed point @@ -325,7 +344,7 @@ contract LinearIncreasingEscrow is /// @param _timestamp Time to calculate the total voting power at /// @return totalSupply Total supply of voting power at that time /// @dev We have to walk forward from the last point to the timestamp because - /// we cannot guarantee that all checkpoints have been written between the last point and the timestamp + /// we cannot guarantee that allow checkpoints have been written between the last point and the timestamp /// covering scheduled changes. function supplyAt(uint256 _timestamp) external view returns (uint256 totalSupply) { // get the index of the last point before the timestamp @@ -333,22 +352,18 @@ contract LinearIncreasingEscrow is if (index == 0) return 0; GlobalPoint memory latestPoint = _pointHistory[index]; - uint48 latestCheckpoint = uint48(latestPoint.ts); uint48 interval = uint48(IClock(clock).checkpointInterval()); - // if we are at the block timestamp with the latest point, history has already been written - bool latestPointUpToDate = latestPoint.ts == _timestamp; - - if (!latestPointUpToDate) { - // step 1: round down to floor of interval ensures we align with schedulling + if (latestPoint.ts != _timestamp) { + // round down to floor of interval ensures we align with schedulling uint48 t_i = (latestCheckpoint / interval) * interval; for (uint256 i = 0; i < 255; ++i) { - // step 2: the first interval is always the next one after the last checkpoint + // the first interval is always the next one after the last checkpoint t_i += interval; - // bound to at least the timestamp + // max now if (t_i > _timestamp) t_i = uint48(_timestamp); // fetch the changes for this interval @@ -361,8 +376,7 @@ contract LinearIncreasingEscrow is _getBiasUnbound(t_i - latestPoint.ts, latestPoint.coefficients) + biasChange; - // here we add the net result of the coefficient changes to the slope - // which can be applied for the ensuring period + // here we add the slope changes for next period // this can be positive or negative depending on if new deposits outweigh tapering effects + withdrawals latestPoint.coefficients[1] += slopeChange; @@ -370,12 +384,8 @@ contract LinearIncreasingEscrow is if (latestPoint.coefficients[1] < 0) latestPoint.coefficients[1] = 0; if (latestPoint.coefficients[0] < 0) latestPoint.coefficients[0] = 0; - // update the timestamp ahead of either breaking or the next iteration + // keep going until we reach the timestamp latestPoint.ts = t_i; - - // write the point to storage if there are changes, otherwise continue - // interpolating in memory and can write to storage at the end - // otherwise we are as far as we can go so we break if (t_i == _timestamp) { break; } @@ -402,7 +412,6 @@ contract LinearIncreasingEscrow is } /// @dev manual checkpoint that can be called to ensure history is up to date - // TODO test this function _checkpoint() internal nonReentrant { (GlobalPoint memory latestPoint, uint256 currentIndex) = _populateHistory(); @@ -412,12 +421,6 @@ contract LinearIncreasingEscrow is } } - // we could have: - // globalCheckpoint - // tokenCheckpointUpdate - // tokenCheckpointInit - // tokenCheckpointReinit - /// @dev Main checkpointing function for token and global state /// @param _tokenId The NFT token ID /// @param _oldLocked The previous locked balance and start time @@ -427,10 +430,9 @@ contract LinearIncreasingEscrow is IVotingEscrow.LockedBalance memory _oldLocked, IVotingEscrow.LockedBalance memory _newLocked ) internal { - // write the token checkpoint + // write the token checkpoint, fetching the old and new points (TokenPoint memory oldTokenPoint, TokenPoint memory newTokenPoint) = _tokenCheckpoint( _tokenId, - _oldLocked, _newLocked ); @@ -442,12 +444,12 @@ contract LinearIncreasingEscrow is (GlobalPoint memory latestPoint, uint256 currentIndex) = _populateHistory(); // update the global with the latest token point - // it may be the case that the token is writing a scheduled change in which case there is no - // incremental change to the global state + // it may be the case that the token is writing a scheduled change + // meaning there is no current change to latest global point bool tokenHasUpdateNow = newTokenPoint.checkpointTs == latestPoint.ts; if (tokenHasUpdateNow) { latestPoint = _applyTokenUpdateToGlobal( - _newLocked.start, // TODO + _newLocked.start, oldTokenPoint, newTokenPoint, latestPoint @@ -457,7 +459,8 @@ contract LinearIncreasingEscrow is // if the currentIndex is unchanged, this means no extra state has been written globally // so no need to write if there are no changes from token + schedule if (currentIndex != _latestPointIndex || tokenHasUpdateNow) { - // index starts at 1 - so if there is an update we need to add it + // index starts at 1 - so if there are no global updates + // but there is a token update, write it to index 1 _latestPointIndex = currentIndex == 0 ? 1 : currentIndex; _pointHistory[currentIndex] = latestPoint; } @@ -501,55 +504,25 @@ contract LinearIncreasingEscrow is return true; } - /// @dev Ensure that when writing multiple points, we don't violate the invariant that no lock - /// can accumulate more than the maxTime amount of voting power. - /// @dev We assume Lock start dates cannot be retroactively changed - /// @param _lockStartTs The start of the original lock. - /// @param _checkpointTs The timestamp of the checkPoint. - /// @return boundedElapsed The total time elapsed since the checkpoint, - /// accounting for the original start date and the maximum. - function _getBoundedElasedSinceLastPoint( - uint48 _lockStartTs, - uint128 _checkpointTs - ) internal view returns (uint48 boundedElapsed) { - uint48 ts = uint48(block.timestamp); - // if the original lock or the checkpoint haven't started, return 0 - if (_lockStartTs > ts || _checkpointTs > ts) return 0; - - // calculate the max possible time based on the lock start - uint48 maxPossibleTs = _lockStartTs + uint48(_maxTime()); - - // bound the elased value to max of this - uint48 boundedTs = ts > maxPossibleTs ? maxPossibleTs : ts; - - // return elapsed seconds since then - return boundedTs - uint48(_checkpointTs); - } - /// @notice Record per-user data to checkpoints. Used by VotingEscrow system. /// @param _tokenId NFT token ID. - /// @param _newLocked New locked amount / end lock time for the tokenid + /// @param _lock The new locked balance and start time. + /// @dev The lock start can only be adjusted before the lock has started. function _tokenCheckpoint( uint256 _tokenId, - IVotingEscrow.LockedBalance memory /*_oldLocked */, - IVotingEscrow.LockedBalance memory _newLocked + IVotingEscrow.LockedBalance memory _lock ) internal returns (TokenPoint memory oldPoint, TokenPoint memory newPoint) { - uint newAmount = _newLocked.amount; - uint newStart = _newLocked.start; - - // in increasing curve, for new amounts we schedule the voting power - // to be created at the next checkpoint, this is not enforced in this function - // the writtenTs is used for warmups, cooldowns and for logging - // safe to cast as .start is 48 bit unsigned - // scheduling the ts in the future is allowed, but - // if the lock start is in the past we just write the checkpoint to now - // this would be the case if the lock has already started - // note that the schedulling function does not allow passing a lock with a new start - // date once the lock has started meaning you will always record at block.ts once - // the lock begins - newPoint.checkpointTs = block.timestamp < newStart - ? uint128(newStart) + uint lockAmount = _lock.amount; + uint lockStart = _lock.start; + + // lock start is frozen once it starts - + // so write to the future if schedulling a deposit + // else write to the current block timestamp + newPoint.checkpointTs = block.timestamp < lockStart + ? uint128(lockStart) : uint128(block.timestamp); + + // the writtenTs serves as a reference and is used for warmups and cooldowns newPoint.writtenTs = uint128(block.timestamp); // get the old point if it exists @@ -559,27 +532,20 @@ contract LinearIncreasingEscrow is // we can't write checkpoints out of order as it would interfere with searching if (oldPoint.checkpointTs > newPoint.checkpointTs) revert InvalidCheckpoint(); - // for all locks other than amount == 0 (an exit) - // we need to compute the coefficients and the bias - if (newAmount > 0) { - int256[3] memory coefficients = _getCoefficients(newAmount); + if (lockAmount > 0) { + int256[3] memory coefficients = _getCoefficients(lockAmount); // fetch the elapsed time since the lock has started - // this is predicated on the start date not being changeable if it's passed - // i.e. newLocked.start is the oldLocked.start once the lock has started - uint elapsed = block.timestamp < newStart ? 0 : block.timestamp - newStart; - // todo: understand why we can't use this - // uint elapsed = _getBoundedElasedSinceLastPoint(_newLocked.start, newPoint.checkpointTs); - - // problem we have now is that the coefficient[0] is later being used - // todo this is janky AF - newPoint.coefficients[1] = coefficients[1]; - // this bias is stored having been converted from fixed point - // be mindful about converting back - // newPoint.bias = _getBias(elapsed, coefficients); + // we rely on start date not being changeable if it's passed + uint elapsed = boundedTimeSinceLockStart(lockStart, block.timestamp); + + // If the lock has started, just write the initial amount + // else evaluate the bias based on the elapsed time + // and the coefficients computed from the lock amount newPoint.coefficients[0] = elapsed == 0 ? coefficients[0] : _getBiasUnbound(elapsed, coefficients); + newPoint.coefficients[1] = coefficients[1]; } // if we're writing to a new point, increment the interval @@ -592,7 +558,6 @@ contract LinearIncreasingEscrow is return (oldPoint, newPoint); } - /// @dev Writes future changes to the schedulling system. Old points that have yet to be written are replaced. /// @param _oldPoint The old point to be replaced, if it exists /// @param _newPoint The new point to be written, should always exist @@ -607,8 +572,6 @@ contract LinearIncreasingEscrow is ) internal { // check if there is any old schedule bool existingLock = _oldLocked.amount > 0; - - // max time is set during contract deploy, if its > uint48 someone didn't test properly uint48 max = uint48(_maxTime()); // if so we have to remove it @@ -622,8 +585,7 @@ contract LinearIncreasingEscrow is uint48 originalStart = _oldLocked.start; uint48 originalMax = originalStart + max; - // if before the start we need to remove the scheduled slope increase - // and the scheduled bias increase + // if before the start we need to remove the scheduled curve changes // strict equality is crucial as we will apply any immediate changes // directly to the global point in later functions if (block.timestamp < originalStart) { @@ -632,7 +594,6 @@ contract LinearIncreasingEscrow is } // If we're not yet at max, also remove the scheduled decrease - // (i.e. increase the slope) if (block.timestamp < originalMax) { _scheduledCurveChanges[originalMax][1] += _oldPoint.coefficients[1]; } @@ -642,25 +603,20 @@ contract LinearIncreasingEscrow is uint48 newStart = _newLocked.start; uint48 newMax = newStart + max; - // if before the start we need to add the scheduled slope increase - // and the scheduled bias increase - // strict equality is crucial as we will apply any immediate changes - // directly to the global point in later functions + // if before the start we need to add the scheduled changes if (block.timestamp < newStart) { - // directly to the global point in later functions if (block.timestamp < newStart) { _scheduledCurveChanges[newStart][0] += _newPoint.coefficients[0]; _scheduledCurveChanges[newStart][1] += _newPoint.coefficients[1]; // write the point where the populate history function should start tracking data from // technically speaking we should check if the old point needs to be moved forward - // if all the coefficients are zero. In practice this is unlikely to make much of a difference. - // unless someone is able to grief by locking very early then removing to much later. + // In practice this is unlikely to make much of a difference other than having some zero points if (_earliestScheduledChange == 0 || newStart < _earliestScheduledChange) { _earliestScheduledChange = newStart; } } - // If we're not yet at max, also add the scheduled decrease + // If we're not yet at max, also add the scheduled decrease to the slope if (block.timestamp < newMax) { _scheduledCurveChanges[newMax][1] -= _newPoint.coefficients[1]; } @@ -668,69 +624,62 @@ contract LinearIncreasingEscrow is /// @dev Fetches the latest global point from history or writes the first point if the earliest scheduled change has elapsed /// @return latestPoint This will either be the latest point in history, a new, empty point if no scheduled changes have elapsed, or the first scheduled change - /// if the earliest scheduled change has elapsed + /// @return latestIndex The index of the latest point in history, or nothing if there is no history function _getLatestGlobalPointOrWriteFirstPoint() internal - returns (GlobalPoint memory latestPoint) + returns (GlobalPoint memory latestPoint, uint256 latestIndex) { - // early return the point if we have it uint index = _latestPointIndex; - if (index > 0) return _pointHistory[index]; + if (index > 0) return (_pointHistory[index], index); - // determine if we have some existing state we need to start from + // check if a scheduled write has been set and has elapsed uint48 earliestTs = _earliestScheduledChange; - bool firstScheduledWrite = index == 0 && // if index == 1, we've got at least one point in history already - // earliest TS must have been set - earliestTs > 0 && - // the earliest scheduled change must have elapsed - earliestTs <= block.timestamp; + bool firstScheduledWrite = earliestTs > 0 && earliestTs <= block.timestamp; - // write the first point and return it + // if we have a scheduled point: write the first point to storage @ index 1 if (firstScheduledWrite) { latestPoint.ts = earliestTs; latestPoint.coefficients[0] = _scheduledCurveChanges[earliestTs][0]; latestPoint.coefficients[1] = _scheduledCurveChanges[earliestTs][1]; - // write operations to storage + index = 1; _latestPointIndex = 1; _pointHistory[1] = latestPoint; } - // otherwise return an empty point at the current ts + // otherwise point is empty but up to date w. no index else { latestPoint.ts = block.timestamp; } - return latestPoint; + return (latestPoint, index); } - /// @dev Backfills total supply history up to the present based on elapsed scheduled changes - /// @dev Will write to storage if there are changes in the past, otherwise will keep the array sparse to save gas - /// @return latestPoint The most recent global state checkpoint in memory. This point is not yet written to storage in case of token-level updates + /// @dev Backfills total supply history up to the present based on elapsed scheduled changes. + /// Minimum weekly intervals to avoid a sparse array that cannot be binary searched. + /// @return latestPoint The most recent global state checkpoint /// @return currentIndex Latest index + intervals iterated over since last state write + /// @dev if there is nothing will return the empty point @ now w. index = 0 function _populateHistory() internal returns (GlobalPoint memory latestPoint, uint256 currentIndex) { // fetch the latest point or write the first one if needed - latestPoint = _getLatestGlobalPointOrWriteFirstPoint(); + (latestPoint, currentIndex) = _getLatestGlobalPointOrWriteFirstPoint(); - // needs to go after writing the point - currentIndex = _latestPointIndex; uint48 latestCheckpoint = uint48(latestPoint.ts); uint48 interval = uint48(IClock(clock).checkpointInterval()); - // if we are at the block timestamp with the latest point, history has already been written - bool latestPointUpToDate = latestPoint.ts == block.timestamp; - - if (!latestPointUpToDate) { - // step 1: round down to floor of interval ensures we align with schedulling + // skip the loop if the latest point is up to date + if (latestPoint.ts != block.timestamp) { + // round down to floor so we align with schedulling checkpoints uint48 t_i = (latestCheckpoint / interval) * interval; for (uint256 i = 0; i < 255; ++i) { - // step 2: the first interval is always the next one after the last checkpoint + // first interval is always the next one after the last checkpoint + // so we double count mid week writes in the past t_i += interval; - // bound to at least the present + // bound to the present if (t_i > block.timestamp) t_i = uint48(block.timestamp); // fetch the changes for this interval @@ -743,8 +692,7 @@ contract LinearIncreasingEscrow is _getBiasUnbound(t_i - latestPoint.ts, latestPoint.coefficients) + biasChange; - // here we add the net result of the coefficient changes to the slope - // which can be applied for the ensuring period + // here we add the changes to the slope which can be applied next period // this can be positive or negative depending on if new deposits outweigh tapering effects + withdrawals latestPoint.coefficients[1] += slopeChange; @@ -752,20 +700,14 @@ contract LinearIncreasingEscrow is if (latestPoint.coefficients[1] < 0) latestPoint.coefficients[1] = 0; if (latestPoint.coefficients[0] < 0) latestPoint.coefficients[0] = 0; - // update the timestamp ahead of either breaking or the next iteration latestPoint.ts = t_i; currentIndex++; - // bool hasScheduledChange = (biasChange != 0 || slopeChange != 0); - // write the point to storage if there are changes, otherwise continue - // interpolating in memory and can write to storage at the end - // otherwise we are as far as we can go so we break + // if we are exactly on the boundary we don't write yet + // this means we can add the token-contribution later if (t_i == block.timestamp) { break; - } - // note: if we are exactly on the boundary we don't write yet - // this means we can add the token-contribution later - else { + } else { _pointHistory[currentIndex] = latestPoint; } } @@ -789,22 +731,26 @@ contract LinearIncreasingEscrow is if (_newPoint.checkpointTs != block.timestamp) revert TokenPointNotUpToDate(); if (_latestGlobalPoint.ts != block.timestamp) revert GlobalPointNotUpToDate(); - // evaluate the old curve up until now and remove its impact from the bias - // calculate bounded elapsed time since last point was written - uint48 elapsed = _getBoundedElasedSinceLastPoint(lockStart, _oldPoint.checkpointTs); - int256 oldUserBias = _getBiasUnbound(elapsed, _oldPoint.coefficients); - _latestGlobalPoint.coefficients[0] -= oldUserBias; + // evaluate the old curve up until now if exists and remove its impact from the bias + if (_oldPoint.checkpointTs != 0) { + uint48 elapsed = boundedTimeSinceCheckpoint( + lockStart, + _oldPoint.checkpointTs, + block.timestamp + ).toUint48(); - // if the new point is not an exit, then add it back in + int256 oldUserBias = _getBiasUnbound(elapsed, _oldPoint.coefficients); + _latestGlobalPoint.coefficients[0] -= oldUserBias; + } + + // if the new point is not an exit, then add it to global state if (_newPoint.coefficients[0] > 0) { - // Add the new user's bias back to the global bias - _latestGlobalPoint.coefficients[0] += int256(_newPoint.coefficients[0]); + _latestGlobalPoint.coefficients[0] += _newPoint.coefficients[0]; } // the immediate reduction is slope requires removing the old and adding the new - // only needs to be done if we are still accumulating voting power, else will double decrement - // TODO: can we simplify this? - if (lockStart > block.timestamp || block.timestamp - lockStart < _maxTime()) { + // only needs to be done if lock has started and we are still accumulating voting power + if (lockStart <= block.timestamp && block.timestamp - lockStart <= _maxTime()) { _latestGlobalPoint.coefficients[1] -= _oldPoint.coefficients[1]; _latestGlobalPoint.coefficients[1] += _newPoint.coefficients[1]; } @@ -816,6 +762,56 @@ contract LinearIncreasingEscrow is return _latestGlobalPoint; } + /*////////////////////////////////////////////////////////////// + CHECKPOINT TIME FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Gets the number of seconds since the lock has started, bounded by the maximum time. + /// @param _start The start time of the lock. + /// @param _timestamp The timestamp to evaluate over + function boundedTimeSinceLockStart( + uint256 _start, + uint256 _timestamp + ) public view returns (uint256) { + if (_timestamp < _start) return 0; + + uint256 rawElapsed = _timestamp - _start; + uint256 max = _maxTime(); + + if (rawElapsed > max) return max; + else return rawElapsed; + } + + /// @dev Ensure that when writing multiple points, we don't violate the invariant that no lock + /// can accumulate more than the maxTime amount of voting power. + /// @dev We assume Lock start dates cannot be retroactively changed + /// @param _start The start of the original lock. + /// @param _checkpointTs The timestamp of the checkPoint. + /// @param _timestamp The timestamp to evaluate over. + /// @return The total time elapsed since the checkpoint, + /// accounting for the original start date and the maximum. + function boundedTimeSinceCheckpoint( + uint256 _start, + uint128 _checkpointTs, + uint256 _timestamp + ) public view returns (uint256) { + if (_checkpointTs < _start) revert InvalidCheckpoint(); + + // if the original lock or the checkpoint haven't started, return 0 + if (_timestamp < _start || _timestamp < _checkpointTs) return 0; + + // calculate the max possible time based on the lock start + uint256 max = _maxTime(); + uint256 maxPossibleTs = _start + max; + + // bound the checkpoint to the max possible time + // and the current timestamp to the max possible time + uint256 effectiveCheckpoint = _checkpointTs > maxPossibleTs ? maxPossibleTs : _checkpointTs; + uint256 effectiveTimestamp = _timestamp > maxPossibleTs ? maxPossibleTs : _timestamp; + + return effectiveTimestamp - effectiveCheckpoint; + } + /*/////////////////////////////////////////////////////////////// UUPS Upgrade //////////////////////////////////////////////////////////////*/ diff --git a/test/escrow/curve/linear/LinearApplyTokenToGlobal.t.sol b/test/escrow/curve/linear/LinearApplyTokenToGlobal.t.sol index 7543784..e6c3a84 100644 --- a/test/escrow/curve/linear/LinearApplyTokenToGlobal.t.sol +++ b/test/escrow/curve/linear/LinearApplyTokenToGlobal.t.sol @@ -21,6 +21,22 @@ contract TestLinearIncreasingApplyTokenChange is LinearCurveBase { return _old; } + function testRevertsIfPointsArentInSync() public { + vm.warp(0); + TokenPoint memory oldPoint; + TokenPoint memory newPoint; + GlobalPoint memory globalPoint; + newPoint.checkpointTs = 1; + + vm.expectRevert(TokenPointNotUpToDate.selector); + curve.applyTokenUpdateToGlobal(0, oldPoint, newPoint, globalPoint); + + vm.warp(newPoint.checkpointTs); + + vm.expectRevert(GlobalPointNotUpToDate.selector); + curve.applyTokenUpdateToGlobal(0, oldPoint, newPoint, globalPoint); + } + // when run on an empty global point, adds the user's own point // deposit function testEmptyGlobalDeposit() public { @@ -32,9 +48,11 @@ contract TestLinearIncreasingApplyTokenChange is LinearCurveBase { TokenPoint memory newPoint = curve.previewPoint(10 ether); newPoint.checkpointTs = uint128(block.timestamp); - // write it - globalPoint = curve.applyTokenUpdateToGlobal(0, oldPoint, newPoint, globalPoint); + // assume the lock starts immediately + uint48 lockStart = uint48(block.timestamp); + + globalPoint = curve.applyTokenUpdateToGlobal(lockStart, oldPoint, newPoint, globalPoint); uint expectedBias = 10 ether; @@ -53,7 +71,10 @@ contract TestLinearIncreasingApplyTokenChange is LinearCurveBase { TokenPoint memory newPoint0 = curve.previewPoint(10 ether); newPoint0.checkpointTs = uint128(block.timestamp); - globalPoint = curve.applyTokenUpdateToGlobal(0, oldPoint, newPoint0, globalPoint); + // assume the lock starts immediately + uint48 lockStart = uint48(block.timestamp); + + globalPoint = curve.applyTokenUpdateToGlobal(lockStart, oldPoint, newPoint0, globalPoint); // copy the new to old point and redefine the new point oldPoint = _copyNewToOld(newPoint0, oldPoint); @@ -62,7 +83,7 @@ contract TestLinearIncreasingApplyTokenChange is LinearCurveBase { newPoint1.checkpointTs = uint128(block.timestamp); GlobalPoint memory newGlobalPoint = curve.applyTokenUpdateToGlobal( - 0, + lockStart, oldPoint, newPoint1, globalPoint @@ -86,13 +107,15 @@ contract TestLinearIncreasingApplyTokenChange is LinearCurveBase { globalPoint.coefficients[1] = curve.previewPoint(100 ether).coefficients[1]; TokenPoint memory oldPoint; // 0 - TokenPoint memory newPoint = curve.previewPoint(10 ether); newPoint.checkpointTs = uint128(block.timestamp); + // again this is the first lock + uint48 lockStart = uint48(block.timestamp); + int cachedSlope = globalPoint.coefficients[1]; - globalPoint = curve.applyTokenUpdateToGlobal(0, oldPoint, newPoint, globalPoint); + globalPoint = curve.applyTokenUpdateToGlobal(lockStart, oldPoint, newPoint, globalPoint); // expectation: bias && slope incremented assertEq(uint(globalPoint.coefficients[0]) / 1e18, 110 ether); @@ -101,43 +124,53 @@ contract TestLinearIncreasingApplyTokenChange is LinearCurveBase { } // change - elapsed + // test that if we have existing state and some time elapses + // the change is correctly applied function testChangeOnExistingGlobalStateElapsedTime() public { vm.warp(100); + // imagine the state is set with 100 ether total GlobalPoint memory globalPoint; globalPoint.ts = block.timestamp; - - // imagine the state is set with 100 ether total globalPoint.coefficients[0] = curve.previewPoint(100 ether).coefficients[0]; globalPoint.coefficients[1] = curve.previewPoint(100 ether).coefficients[1]; + // no existing point TokenPoint memory oldPoint; // 0 + // user makes a deposit at the same time as global for 10 eth TokenPoint memory newPoint0 = curve.previewPoint(10 ether); newPoint0.checkpointTs = uint128(block.timestamp); - globalPoint = curve.applyTokenUpdateToGlobal(0, oldPoint, newPoint0, globalPoint); - int cachedCoeff0 = globalPoint.coefficients[0]; + uint48 lockStart = uint48(block.timestamp); + + // apply the deposit of the user to the state + globalPoint = curve.applyTokenUpdateToGlobal(lockStart, oldPoint, newPoint0, globalPoint); + + // should be a global state of 110 eth + assertEq(uint(globalPoint.coefficients[0]) / 1e18, 110 ether); // copy the new to old point and redefine the new point oldPoint = _copyNewToOld(newPoint0, oldPoint); + // warp into the future, we're gonna write a new point over the top + // representing a change in the deposit vm.warp(200); - TokenPoint memory newPoint1 = curve.previewPoint(20 ether); - newPoint1.checkpointTs = uint128(block.timestamp); - - // define a new global point by evaluating it over the elapsed time + // our existing global point should have accrued 110 ether's worth of bias for 100 seconds globalPoint.coefficients[0] = curve.getBiasUnbound(100, globalPoint.coefficients); globalPoint.ts = block.timestamp; + // an entirely fresh, new point is written which should overrwrite the old + TokenPoint memory newPoint1 = curve.previewPoint(20 ether); + newPoint1.checkpointTs = uint128(block.timestamp); + GlobalPoint memory newGlobalPoint = curve.applyTokenUpdateToGlobal( - 0, + lockStart, oldPoint, newPoint1, globalPoint ); - // we would now expect that the new global point is: // 110 ether evaled over 100 seconds - (10 ether evaled over 100 seconds) + (20 ether) uint expectedCoeff0 = curve.getBias(100, 110 ether) - diff --git a/test/escrow/curve/linear/LinearBase.sol b/test/escrow/curve/linear/LinearBase.sol index 82f05bd..666f2c7 100644 --- a/test/escrow/curve/linear/LinearBase.sol +++ b/test/escrow/curve/linear/LinearBase.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.17; import {console2 as console} from "forge-std/console2.sol"; import {TestHelpers} from "@helpers/TestHelpers.sol"; -import {console2 as console} from "forge-std/console2.sol"; import {DaoUnauthorized} from "@aragon/osx/core/utils/auth.sol"; import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; @@ -20,6 +19,12 @@ contract MockEscrow { address public token; LinearIncreasingEscrow public curve; + mapping(uint256 => IVotingEscrow.LockedBalance) internal _locked; + + function setLockedBalance(uint256 _tokenId, IVotingEscrow.LockedBalance memory lock) external { + _locked[_tokenId] = lock; + } + function setCurve(LinearIncreasingEscrow _curve) external { curve = _curve; } @@ -31,16 +36,19 @@ contract MockEscrow { ) external { return curve.checkpoint(_tokenId, _oldLocked, _newLocked); } + + function locked(uint256 _tokenId) external view returns (IVotingEscrow.LockedBalance memory) { + return _locked[_tokenId]; + } } /// @dev expose internal functions for testing contract MockLinearIncreasingEscrow is LinearIncreasingEscrow { function tokenCheckpoint( uint256 _tokenId, - IVotingEscrow.LockedBalance memory _oldLocked, IVotingEscrow.LockedBalance memory _newLocked ) public returns (TokenPoint memory, TokenPoint memory) { - return _tokenCheckpoint(_tokenId, _oldLocked, _newLocked); + return _tokenCheckpoint(_tokenId, _newLocked); } function scheduleCurveChanges( @@ -65,7 +73,16 @@ contract MockLinearIncreasingEscrow is LinearIncreasingEscrow { _pointHistory[_index] = _latestPoint; } - function getLatestGlobalPointOrWriteFirstPoint() external returns (GlobalPoint memory) { + function writeNewTokenPoint( + uint256 _tokenId, + TokenPoint memory _point, + uint _interval + ) external { + tokenPointIntervals[_tokenId] = _interval; + _tokenPointHistory[_tokenId][_interval] = _point; + } + + function getLatestGlobalPointOrWriteFirstPoint() external returns (GlobalPoint memory, uint) { return _getLatestGlobalPointOrWriteFirstPoint(); } @@ -94,11 +111,11 @@ contract MockLinearIncreasingEscrow is LinearIncreasingEscrow { function getBiasUnbound( uint elapsed, int[3] memory coefficients - ) external view returns (int256) { + ) external pure returns (int256) { return _getBiasUnbound(elapsed, coefficients); } - function getBiasUnbound(uint elapsed, uint amount) external view returns (int256) { + function getBiasUnbound(uint elapsed, uint amount) external pure returns (int256) { return _getBiasUnbound(elapsed, _getCoefficients(amount)); } diff --git a/test/escrow/curve/linear/LinearCheckpoint.t.sol b/test/escrow/curve/linear/LinearCheckpoint.t.sol index 37447c3..0daec62 100644 --- a/test/escrow/curve/linear/LinearCheckpoint.t.sol +++ b/test/escrow/curve/linear/LinearCheckpoint.t.sol @@ -8,108 +8,6 @@ import {IVotingEscrowIncreasing, ILockedBalanceIncreasing} from "src/escrow/incr import {LinearCurveBase} from "./LinearBase.sol"; contract TestLinearIncreasingCheckpoint is LinearCurveBase { - function testDepositSingleUser() public { - // test 1 user and see that it maxes out - - vm.warp(1 weeks + 1 days); - - curve.unsafeCheckpoint( - 1, - LockedBalance(0, 0), - LockedBalance({start: 2 weeks, amount: 1000 ether}) - ); - - // wait until the end of the week - vm.warp(2 weeks); - - curve.unsafeManualCheckpoint(); - - assertEq(curve.latestPointIndex(), 1, "index should be 1"); - - vm.warp(2 weeks + curve.maxTime()); - - curve.unsafeManualCheckpoint(); - - assertEq( - curve.latestPointIndex(), - curve.maxTime() / 1 weeks + 1, - "index should be maxTime / 1 weeks, 1 indexed" - ); - - vm.warp(2 weeks + curve.maxTime() + 10 weeks); - - curve.unsafeManualCheckpoint(); - - assertEq( - curve.latestPointIndex(), - curve.maxTime() / 1 weeks + 11, - "index should be maxTime / 1 weeks, 1 indexed + 10" - ); - - // check that most of the array is sparse - for (uint i = 0; i < curve.maxTime() / 1 weeks + 11; i++) { - if (i == 1 || i == 2 || i == 105) { - continue; - } else { - assertEq(curve.pointHistory(i).ts, 0, "point should be empty"); - } - } - } - function testDepositTwoUsers() public { - // test 1 user and see that it maxes out - - vm.warp(1 weeks + 1 days); - - curve.unsafeCheckpoint( - 1, - LockedBalance(0, 0), - LockedBalance({start: 2 weeks, amount: 1000 ether}) - ); - - // wait until the end of the week - vm.warp(2 weeks); - - curve.unsafeManualCheckpoint(); - - assertEq(curve.latestPointIndex(), 1, "index should be 1"); - - vm.warp(2 weeks + 1 days); - - curve.unsafeCheckpoint( - 2, - LockedBalance(0, 0), - LockedBalance({start: 3 weeks, amount: 1000 ether}) - ); - - vm.warp(2 weeks + curve.maxTime()); - - curve.unsafeManualCheckpoint(); - - assertEq( - curve.latestPointIndex(), - curve.maxTime() / 1 weeks + 2, - "index should be maxTime / 1 weeks, 1 indexed" - ); - - vm.warp(2 weeks + curve.maxTime() + 10 weeks); - - curve.unsafeManualCheckpoint(); - - assertEq( - curve.latestPointIndex(), - curve.maxTime() / 1 weeks + 12, - "index should be maxTime / 1 weeks, 1 indexed + 10" - ); - - // check that most of the array is sparse - // for (uint i = 0; i < curve.maxTime() / 1 weeks + 11; i++) { - // if (i == 1 || i == 2 || i == 105) { - // continue; - // } else { - // assertEq(curve.pointHistory(i).ts, 0, "point should be empty"); - // } - // } - } // test a single deposit happening in the future // followed by manual checkpoint before and after the scheduled increase @@ -144,6 +42,9 @@ contract TestLinearIncreasingCheckpoint is LinearCurveBase { shaneLastLock = LockedBalance({start: nextInterval, amount: 1_000_000e18}); + // write the lock + escrow.setLockedBalance(shane, shaneLastLock); + curve.unsafeCheckpoint(shane, LockedBalance(0, 0), shaneLastLock); { @@ -193,6 +94,7 @@ contract TestLinearIncreasingCheckpoint is LinearCurveBase { ); mattLastLock = LockedBalance({start: nextInterval, amount: 500_000e18}); + escrow.setLockedBalance(matt, mattLastLock); { // assertions: @@ -200,7 +102,6 @@ contract TestLinearIncreasingCheckpoint is LinearCurveBase { int changeInSlope = curve.getCoefficients(500_000e18)[1]; uint expTotalVP = curve.getBias(3 days, 1_000_000e18); // current index is 2 - // todo: think about this as there should ideally be nothing written // if the only change is a schedulled one assertEq(curve.latestPointIndex(), 2, "index should be 2"); @@ -249,6 +150,7 @@ contract TestLinearIncreasingCheckpoint is LinearCurveBase { ); mattLastLock = LockedBalance({start: nextInterval, amount: 1_000_000e18}); + escrow.setLockedBalance(matt, mattLastLock); { // assertions: @@ -306,6 +208,7 @@ contract TestLinearIncreasingCheckpoint is LinearCurveBase { LockedBalance(0, 0), LockedBalance({start: nextInterval, amount: 1_000_000e18}) ); + escrow.setLockedBalance(phil, LockedBalance({start: nextInterval, amount: 1_000_000e18})); { // assertions: assertEq(curve.latestPointIndex(), 5, "index should be 5"); @@ -331,10 +234,11 @@ contract TestLinearIncreasingCheckpoint is LinearCurveBase { "bias should be the sum of 1m and 2m" ); - // point 4 should be skipped - // GlobalPoint memory p4 = curve.pointHistory(4); - // assertEq(p4.ts, 0, "point 4 should not exist"); - // + // point 4 should be at the week boundary + GlobalPoint memory p4 = curve.pointHistory(4); + + assertEq(p4.ts, 4 weeks, "point 4 should be at the week boundary"); + // point 5 should be written GlobalPoint memory p5 = curve.pointHistory(5); @@ -647,5 +551,145 @@ contract TestLinearIncreasingCheckpoint is LinearCurveBase { assertEq(totalVotingPower, 0, "total voting power w109 + 1 day"); } + + // let's query historic voting power + { + // for shane + { + // before 2 weeks - should be 0 + uint votingPower = curve.votingPowerAt(shane, 1 weeks + 6 days); + assertEq(votingPower, 0, "voting power should be 0 for shane at 1w6d"); + + // on week 2 - should be 1m + votingPower = curve.votingPowerAt(shane, 2 weeks); + assertEq(votingPower, 1_000_000e18, "voting power should be 1m for shane at 2w"); + + // on week 2.5 - should be 1m + 0.5w + votingPower = curve.votingPowerAt(shane, 2 weeks + 3 days); + assertEq( + votingPower, + curve.getBias(3 days, 1_000_000e18), + "voting power should be 1m + 0.5w for shane at 2w3d" + ); + + // week 5 - should be 0.5 run for 3 weeks + votingPower = curve.votingPowerAt(shane, 5 weeks); + assertEq( + votingPower, + curve.getBias(3 weeks, 500_000e18), + "voting power should be 0.5 run for 3 weeks" + ); + + // week 75 - 0.5 run for 73 weeks + votingPower = curve.votingPowerAt(shane, 75 weeks); + assertEq( + votingPower, + curve.getBias(73 weeks, 500_000e18), + "voting power should be 0.5 run for 73 weeks" + ); + + // maxxed out at 106 weeks + votingPower = curve.votingPowerAt(shane, 106 weeks); + assertEq( + votingPower, + curve.getBias(curve.maxTime(), 500_000e18), + "voting power should be maxxed out at 106 weeks" + ); + + // maxxed out 109 + 1 - 1 + votingPower = curve.votingPowerAt(shane, 109 weeks + 1 days - 1); + assertEq( + votingPower, + curve.getBias(curve.maxTime(), 500_000e18), + "voting power should be maxxed out at 109 + 1 - 1" + ); + + // zero at 109 + 1 + votingPower = curve.votingPowerAt(shane, 109 weeks + 1 days); + assertEq(votingPower, 0, "voting power should be 0"); + } + + // for matt + { + // before 3 weeks - should be 0 + uint votingPower = curve.votingPowerAt(matt, 2 weeks + 6 days); + assertEq(votingPower, 0, "voting power should be 0 for matt at 2w6d"); + + // on week 3 - should be 1m + votingPower = curve.votingPowerAt(matt, 3 weeks); + assertEq(votingPower, 1_000_000e18, "voting power should be 1m for matt at 3w"); + + // on week 3.5 - should be 1m + 0.5w + votingPower = curve.votingPowerAt(matt, 3 weeks + 3 days); + assertEq( + votingPower, + curve.getBias(3 days, 1_000_000e18), + "voting power should be 1m + 0.5w for matt at 3w3d" + ); + + // week 5 - should be 1m + 2w + votingPower = curve.votingPowerAt(matt, 5 weeks); + assertEq( + votingPower, + curve.getBias(2 weeks, 1_000_000e18), + "voting power should be 1m + 2w for matt at 5w" + ); + + // week 5.5 (3 days -1) - should be 1m + 2.5w + votingPower = curve.votingPowerAt(matt, 5 weeks + 3 days - 1); + assertEq( + votingPower, + curve.getBias(2 weeks + 3 days - 1, 1_000_000e18), + "voting power should be 1m + 2.5w for matt at 5w3d" + ); + + // week 5.5 (4 days) - should be 0 + votingPower = curve.votingPowerAt(matt, 5 weeks + 3 days); + assertEq(votingPower, 0, "voting power should be 0 for matt at 5w3d"); + } + + // for phil + { + // before 5 weeks - should be 0 + uint votingPower = curve.votingPowerAt(phil, 4 weeks + 6 days); + assertEq(votingPower, 0, "voting power should be 0 for phil at 4w6d"); + + // on week 5 - should be 0 as warming up + votingPower = curve.votingPowerAt(phil, 5 weeks); + assertEq(votingPower, 0, "voting power should be 0 for phil at 5w as warming up"); + + // at week 5 + 3 exactly still warming + votingPower = curve.votingPowerAt(phil, 5 weeks + 3 days); + assertEq(votingPower, 0, "voting power should be 0 for phil at 5w3d as warming up"); + + // +1s should be 5 w, 3d + 1 + votingPower = curve.votingPowerAt(phil, 5 weeks + 3 days + 1); + assertEq( + votingPower, + curve.getBias(3 days + 1, 1_000_000e18), + "voting power should be starting for phil at 5w3d + 1" + ); + + // on week 5 + 104 weeks - max + votingPower = curve.votingPowerAt(phil, 5 weeks + 104 weeks); + assertEq( + votingPower, + curve.getBias(curve.maxTime(), 1_000_000e18), + "voting power should be maxxed out for phil at 5w + 104w" + ); + + // on week 109 + 1 - 1 - max + votingPower = curve.votingPowerAt(phil, 109 weeks + 1 days - 1); + assertEq( + votingPower, + curve.getBias(curve.maxTime(), 1_000_000e18), + "voting power should be maxxed out for phil at 109 + 1 - 1" + ); + + // on week 109 + 1 - 0 - 0 + votingPower = curve.votingPowerAt(phil, 109 weeks + 1 days); + assertEq(votingPower, 0, "voting power should be 0 for phil at 109 + 1"); + } + } } } diff --git a/test/escrow/curve/linear/LinearEscrowTime.t.sol b/test/escrow/curve/linear/LinearEscrowTime.t.sol new file mode 100644 index 0000000..6440ed2 --- /dev/null +++ b/test/escrow/curve/linear/LinearEscrowTime.t.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {console2 as console} from "forge-std/console2.sol"; + +import {LinearIncreasingEscrow, IVotingEscrow, IEscrowCurve} from "src/escrow/increasing/LinearIncreasingEscrow.sol"; +import {IVotingEscrowIncreasing, ILockedBalanceIncreasing} from "src/escrow/increasing/interfaces/IVotingEscrowIncreasing.sol"; + +import {LinearCurveBase} from "./LinearBase.sol"; + +contract TestLinearIncreasingTime is LinearCurveBase { + // bounded since lock start + + // before the lock starts - return 0 + function testFuzzBeforeLockStartReturnZero(uint start, uint timestamp) public view { + vm.assume(start > timestamp); + assertEq(curve.boundedTimeSinceLockStart(start, timestamp), 0); + } + + // between lock starting and max time - return the correct amount + function testFuzzBetweenLockStartAndMaxTime(uint start, uint timestamp) public view { + vm.assume(start <= timestamp); + vm.assume(timestamp <= curve.maxTime()); + assertEq(curve.boundedTimeSinceLockStart(start, timestamp), timestamp - start); + } + + // after max time - return max + function testFuzzAfterMaxTime(uint start, uint timestamp) public view { + vm.assume(start <= timestamp); + vm.assume(timestamp - start >= curve.maxTime()); + assertEq(curve.boundedTimeSinceLockStart(start, timestamp), curve.maxTime()); + } + + // bounded since checkpoint + + // if checkpoint < start, revert + function testFuzzCheckpointBeforeStartRevert( + uint start, + uint128 checkpoint, + uint timestamp + ) public { + vm.assume(checkpoint < start); + vm.expectRevert(InvalidCheckpoint.selector); + curve.boundedTimeSinceCheckpoint(start, checkpoint, timestamp); + } + + // before the checkpoint - return 0 + function testFuzzBeforeCheckpointReturnZero( + uint start, + uint128 checkpoint, + uint timestamp + ) public view { + vm.assume(checkpoint >= start); + vm.assume(checkpoint > timestamp); + assertEq(curve.boundedTimeSinceCheckpoint(start, checkpoint, timestamp), 0); + } + + // before the lock starts - return 0 + function testFuzzBeforeLockStartReturnZero( + uint start, + uint128 checkpoint, + uint timestamp + ) public view { + vm.assume(checkpoint >= start); + vm.assume(start > timestamp); + assertEq(curve.boundedTimeSinceLockStart(start, timestamp), 0); + } + + // if checkpoint and start are the same and less than max time, return the same value + function testFuzzCheckpointAndStartSameAndLessThanMaxTime( + uint128 checkpoint, + uint timestamp + ) public view { + vm.assume(checkpoint <= timestamp); + vm.assume(timestamp <= curve.maxTime()); + uint start = checkpoint; + assertEq( + curve.boundedTimeSinceCheckpoint(start, checkpoint, timestamp), + timestamp - checkpoint + ); + assertEq( + curve.boundedTimeSinceLockStart(checkpoint, timestamp), + curve.boundedTimeSinceCheckpoint(start, checkpoint, timestamp) + ); + } + + // if checkpoint after start and before max time since start, return time since checkpoint + // bounding on uints to play nicely w. overflow + function testFuzzCheckpointAfterStartAndBeforeMaxTimeSinceStart( + uint120 start, + uint128 checkpoint, + uint120 timestamp + ) public view { + // start ----- checkpoint ----- timestamp ==== max + vm.assume(timestamp <= uint(start) + curve.maxTime()); + vm.assume(checkpoint > start); + vm.assume(timestamp >= checkpoint); + + assertEq( + curve.boundedTimeSinceCheckpoint(start, checkpoint, timestamp), + timestamp - checkpoint + ); + } + + // if checkpoint after start and after max time since start, return max time since start + function testFuzzCheckpointAfterMaxSinceStart( + uint120 start, + uint128 checkpoint, + uint120 timestamp + ) public view { + // start ----- checkpoint ==== max ----- timestamp + vm.assume(timestamp >= uint(start) + curve.maxTime()); + vm.assume(checkpoint > start); + vm.assume(timestamp >= checkpoint); + + uint secondsBetweenStartAndCheckpoint = checkpoint - start; + // assume the cp is not really far from the start + vm.assume(secondsBetweenStartAndCheckpoint <= curve.maxTime()); + + // therefore we'd assume the curve can keep increasing up to max - differential + uint maxTimeSubStartToCheckpoint = curve.maxTime() - secondsBetweenStartAndCheckpoint; + + assertEq( + curve.boundedTimeSinceCheckpoint(start, checkpoint, timestamp), + maxTimeSubStartToCheckpoint + ); + } + + // if checkpoint after max time since start, return 0 + function testFuzzCheckpointAfterMaxSinceStartReturnZero( + uint120 start, + uint128 checkpoint, + uint120 timestamp + ) public view { + // start ==== max ----- checkpoint ----- timestamp + vm.assume(checkpoint > start); + vm.assume(checkpoint >= uint(start) + curve.maxTime()); + vm.assume(timestamp >= checkpoint); + + assertEq(curve.boundedTimeSinceCheckpoint(start, checkpoint, timestamp), 0); + } +} diff --git a/test/escrow/curve/linear/LinearTokenCheckpoint.t.sol b/test/escrow/curve/linear/LinearTokenCheckpoint.t.sol index 709ec12..a840cc4 100644 --- a/test/escrow/curve/linear/LinearTokenCheckpoint.t.sol +++ b/test/escrow/curve/linear/LinearTokenCheckpoint.t.sol @@ -8,503 +8,193 @@ import {IVotingEscrowIncreasing, ILockedBalanceIncreasing} from "src/escrow/incr import {LinearCurveBase} from "./LinearBase.sol"; contract TestLinearIncreasingCurveTokenCheckpoint is LinearCurveBase { - /// @dev some asserts on the state of the passed fuzz variables - function _setValidateState( - uint256 _tokenId, - LockedBalance memory _oldLocked, - uint32 _warp - ) internal { - // Common fuzz assumptions - // start is not zero (this is a sentinel for no lock) - // tokenId = 0 is not a valid token - vm.assume(_oldLocked.start > 0 && _tokenId > 0); - // deposits in the past are not allowed - vm.assume(_oldLocked.start >= _warp); - // bound the start so it doesn't overflow - vm.assume(_oldLocked.start < type(uint48).max); - // bound the amount so it doesn't overflow - vm.assume(_oldLocked.amount <= 2 ** 127 - 1); - vm.assume(_oldLocked.amount > 0); - - // warp to whereever - vm.warp(_warp); - - // write the first point, will have id == 1 and a start of @ at least warp + 1 - curve.tokenCheckpoint(_tokenId, LockedBalance(0, 0), _oldLocked); - } + // new deposit - returns initial point starting in future and empty old, ti == 1 + function testTokenCheckpointNewDepositSchedule() public { + vm.warp(100); - function testFuzz_canWriteANewCheckpointWithCorrectParams( - uint256 _tokenId, - LockedBalance memory _newLocked, - uint32 _warp - ) public { - vm.assume(_newLocked.start > 0 && _tokenId > 0); - vm.warp(_warp); - vm.assume(_newLocked.start >= _warp); - // solmate not a fan of this - vm.assume(_newLocked.amount <= 2 ** 127); + LockedBalance memory lock = LockedBalance({start: 101, amount: 1e18}); - (TokenPoint memory oldPoint, TokenPoint memory newPoint) = curve.tokenCheckpoint( - _tokenId, - LockedBalance(0, 0), - _newLocked - ); + (TokenPoint memory oldPoint, TokenPoint memory newPoint) = curve.tokenCheckpoint(1, lock); + + assertEq(newPoint.coefficients[0] / 1e18, 1e18); + assertEq(newPoint.coefficients[1], curve.previewPoint(1e18).coefficients[1]); + assertEq(newPoint.checkpointTs, 101); + assertEq(newPoint.writtenTs, 100); - // the old point should be zero zero - // assertEq(oldPoint.bias, 0); + assertEq(oldPoint.coefficients[0], 0); + assertEq(oldPoint.coefficients[1], 0); assertEq(oldPoint.checkpointTs, 0); assertEq(oldPoint.writtenTs, 0); + + // token interval == 1 + assertEq(curve.tokenPointIntervals(1), 1); + assertEq(curve.tokenPointHistory(1, 1).checkpointTs, 101); + } + + // first write after start - returns point evaluated since start and empty old, ti == 1 + function testTokenCheckpointFirstWriteAfterStart() public { + vm.warp(200); + + LockedBalance memory lock = LockedBalance({start: 101, amount: 1e18}); + + (TokenPoint memory oldPoint, TokenPoint memory newPoint) = curve.tokenCheckpoint(1, lock); + + uint expectedBias = curve.getBias(block.timestamp - lock.start, 1e18); + + assertEq(uint(newPoint.coefficients[0]) / 1e18, expectedBias); + assertEq(newPoint.coefficients[1], curve.previewPoint(1e18).coefficients[1]); + assertEq(newPoint.checkpointTs, block.timestamp); + assertEq(newPoint.writtenTs, block.timestamp); + assertEq(oldPoint.coefficients[0], 0); assertEq(oldPoint.coefficients[1], 0); + assertEq(oldPoint.checkpointTs, 0); + assertEq(oldPoint.writtenTs, 0); - // new point should have the correct values - int256[3] memory coefficients = curve.getCoefficients(_newLocked.amount); + // token interval == 1 + assertEq(curve.tokenPointIntervals(1), 1); + assertEq(curve.tokenPointHistory(1, 1).checkpointTs, block.timestamp); + } - // assertEq(newPoint.bias, _newLocked.amount, "bias incorrect"); - assertEq(newPoint.checkpointTs, _newLocked.start, "checkpointTs incorrect"); - assertEq(newPoint.writtenTs, _warp, "writtenTs incorrect"); - assertEq(newPoint.coefficients[0] / 1e18, coefficients[0], "constant incorrect"); - assertEq(newPoint.coefficients[1] / 1e18, coefficients[1], "linear incorrect"); + // old point, reverts if the old cpTs > newcpTs + function testRevertIfNewBeforeOldPointSchedule() public { + vm.warp(100); + // checkpoint old point in future + LockedBalance memory oldLock = LockedBalance({start: 200, amount: 1e18}); - // token interval == 1 - assertEq(curve.tokenPointIntervals(_tokenId), 1, "token interval incorrect"); + curve.tokenCheckpoint(1, oldLock); - // token is recorded - bytes32 tokenPointHash = keccak256(abi.encode(newPoint)); - bytes32 historyHash = keccak256(abi.encode(curve.tokenPointHistory(_tokenId, 1))); - assertEq(tokenPointHash, historyHash, "token point not recorded correctly"); - } + // checkpoint new point less in the future + LockedBalance memory newLock = LockedBalance({start: 101, amount: 1e18}); - function testFuzz_case1_AmountSameStartSame( - uint256 _tokenId, - LockedBalance memory _oldLocked, - uint32 _warp - ) public { - // Initialize state - _setValidateState(_tokenId, _oldLocked, _warp); - - // Variables - TokenPoint memory oldPoint; - TokenPoint memory newPoint; - LockedBalance memory _newLocked = LockedBalance(_oldLocked.amount, _oldLocked.start); - - // Case 1: new point same - (oldPoint, newPoint) = curve.tokenCheckpoint(_tokenId, _oldLocked, _newLocked); - - // expect the point is overwritten but nothing actually changes - assertEq(curve.tokenPointIntervals(_tokenId), 1, "C1: token interval incorrect"); - // assertEq(newPoint.bias, _oldLocked.amount, "C1: bias incorrect"); - assertEq( - newPoint.checkpointTs, - _oldLocked.start, - "C1: checkpoindon't want to interrupt his vacation but if he needs us to step in tTs incorrect" - ); - assertEq(newPoint.writtenTs, _warp, "C1: writtenTs incorrect"); - assertEq(newPoint.coefficients[0], oldPoint.coefficients[0], "C1: constant incorrect"); - assertEq(newPoint.coefficients[1], oldPoint.coefficients[1], "C1: linear incorrect"); + vm.expectRevert(InvalidCheckpoint.selector); + curve.tokenCheckpoint(1, newLock); } - function testFuzz_case2_AmountGreaterSameStart( - uint256 _tokenId, - LockedBalance memory _oldLocked, - uint32 _warp - ) public { - // Initialize state - _setValidateState(_tokenId, _oldLocked, _warp); - - // Variables - TokenPoint memory oldPoint; - TokenPoint memory newPoint; - LockedBalance memory _newLocked = LockedBalance(_oldLocked.amount + 1, _oldLocked.start); - - // Case 2: amount > old point, start same - (oldPoint, newPoint) = curve.tokenCheckpoint(_tokenId, _oldLocked, _newLocked); - - // expectation: point overwritten but new curve - assertEq(curve.tokenPointIntervals(_tokenId), 1, "C2: token interval incorrect"); - // assertEq(newPoint.bias, _newLocked.amount, "C2: bias incorrect"); - assertEq(newPoint.checkpointTs, _oldLocked.start, "C2: checkpointTs incorrect"); - assertEq(newPoint.writtenTs, _warp, "C2: writtenTs incorrect"); - assertEq( - newPoint.coefficients[0] / 1e18, - curve.getCoefficients(_newLocked.amount)[0], - "C2: constant incorrect" - ); - assertEq( - newPoint.coefficients[1] / 1e18, - curve.getCoefficients(_newLocked.amount)[1], - "C2: linear incorrect" - ); - } + function testRevertIfNewBeforeOldInProgress() public { + vm.warp(100); + // checkpoint old in the future + LockedBalance memory oldLock = LockedBalance({start: 101, amount: 1e18}); - function testFuzz_case3_AmountGreaterStartBefore( - uint256 _tokenId, - LockedBalance memory _oldLocked, - uint32 _warp - ) public { - // Adjust assumptions for this case - vm.assume(_oldLocked.start > 1); // start > 1 to avoid underflow - _setValidateState(_tokenId, _oldLocked, _warp); - - // Variables - TokenPoint memory oldPoint; - TokenPoint memory newPoint; - // write a an amount greater 1 second before the previous lock - LockedBalance memory _newLocked = LockedBalance( - _oldLocked.amount + 1, - _oldLocked.start - 1 - ); + curve.tokenCheckpoint(1, oldLock); - // expectation - depends on the _warp - // if we now warp to after the start - - // we don't check lock times any more - // instead we check the start time of the points - // if the locks haven't started yet, we write the checkpoint in the future - // to the newStart. - // We then compare this to the old point and if the newPoint start date is BEFORE the old start - // then this reverts - // However if it's at the same time, there's no such revert - // This also holds if the lock start is in the past, it gets snapped to the current time - // hence we will jut overwrite the point - // anotehr option is that if the newLock.start is before the old point, we could reject it - // but that would mean you couldn't update the points. + // checkpoint new point now + LockedBalance memory newLock = LockedBalance({ + start: uint48(block.timestamp), + amount: 1e18 + }); vm.expectRevert(InvalidCheckpoint.selector); - (oldPoint, newPoint) = curve.tokenCheckpoint(_tokenId, _oldLocked, _newLocked); + curve.tokenCheckpoint(1, newLock); } - function testFuzz_case4_AmountGreaterStartAfter( - uint256 _tokenId, - LockedBalance memory _oldLocked, - uint32 _warp - ) public { - // Initialize state - _setValidateState(_tokenId, _oldLocked, _warp); - - // Variables - TokenPoint memory oldPoint; - TokenPoint memory newPoint; - LockedBalance memory _newLocked = LockedBalance( - _oldLocked.amount + 1, - _oldLocked.start + 1 - ); + // old point yet to start, new point yet to start, overwrites + function testOverwritePointIfBothScheduledAtSameTime() public { + vm.warp(100); - // Case 4: amount > old point, start after - (oldPoint, newPoint) = curve.tokenCheckpoint(_tokenId, _oldLocked, _newLocked); - - // expectation: new point written - assertEq(curve.tokenPointIntervals(_tokenId), 2, "C4: token interval incorrect"); - // assertEq(newPoint.bias, _newLocked.amount, "C4: bias incorrect"); - assertEq(newPoint.checkpointTs, _newLocked.start, "C4: checkpointTs incorrect"); - assertEq(newPoint.writtenTs, _warp, "C4: writtenTs incorrect"); - assertEq( - newPoint.coefficients[0] / 1e18, - curve.getCoefficients(_newLocked.amount)[0], - "C4: constant incorrect" - ); - assertEq( - newPoint.coefficients[1] / 1e18, - curve.getCoefficients(_newLocked.amount)[1], - "C4: linear incorrect" - ); - } + LockedBalance memory oldLock = LockedBalance({start: 101, amount: 1e18}); + curve.tokenCheckpoint(1, oldLock); - function testFuzz_case5_AmountLessSameStart( - uint256 _tokenId, - LockedBalance memory _oldLocked, - uint32 _warp - ) public { - // Initialize state - _setValidateState(_tokenId, _oldLocked, _warp); + LockedBalance memory newLock = LockedBalance({start: 101, amount: 2e18}); - vm.assume(_oldLocked.amount > 0); + (TokenPoint memory oldPoint, TokenPoint memory newPoint) = curve.tokenCheckpoint( + 1, + newLock + ); - // Variables - TokenPoint memory oldPoint; - TokenPoint memory newPoint; - LockedBalance memory _newLocked = LockedBalance(_oldLocked.amount - 1, _oldLocked.start); + assertEq(newPoint.coefficients[0] / 1e18, 2e18); + assertEq(newPoint.coefficients[1], curve.previewPoint(2e18).coefficients[1]); + assertEq(newPoint.checkpointTs, 101); + assertEq(newPoint.writtenTs, 100); - // Case 5: amount < old point, start same - console.log("oldLocked", _oldLocked.start, _oldLocked.amount); - console.log("newLocked", _newLocked.start, _newLocked.amount); + assertEq(oldPoint.coefficients[0] / 1e18, 1e18); + assertEq(oldPoint.coefficients[1], curve.previewPoint(1e18).coefficients[1]); + assertEq(oldPoint.checkpointTs, 101); + assertEq(oldPoint.writtenTs, 100); - // expectation: overwrite with a smaller value - (oldPoint, newPoint) = curve.tokenCheckpoint(_tokenId, _oldLocked, _newLocked); + // token interval == 1 + assertEq(curve.tokenPointIntervals(1), 1); + assertEq(curve.tokenPointHistory(1, 1).checkpointTs, 101); + assertEq(curve.tokenPointHistory(1, 1).coefficients[0] / 1e18, 2e18); } - function testFuzz_case6_AmountLessStartBefore( - uint256 _tokenId, - LockedBalance memory _oldLocked, - uint32 _warp - ) public { - // Initialize state - _setValidateState(_tokenId, _oldLocked, _warp); - - vm.assume(_oldLocked.start > 0); // start > 0 to avoid underflow - vm.assume(_oldLocked.amount > 0); - - // Variables - TokenPoint memory oldPoint; - TokenPoint memory newPoint; - LockedBalance memory _newLocked = LockedBalance( - _oldLocked.amount - 1, - _oldLocked.start - 1 - ); + // old point started, write a new point before the max, correctly writes w. correct bias + function testWriteNewPoint() public { + vm.warp(100); - // Case 6: amount less, start less - // expect can't do - vm.expectRevert(InvalidCheckpoint.selector); - (oldPoint, newPoint) = curve.tokenCheckpoint(_tokenId, _oldLocked, _newLocked); - } + // write a point + LockedBalance memory oldLock = LockedBalance({start: 200, amount: 1e18}); + curve.tokenCheckpoint(1, oldLock); - // Boilerplate for case 7: amount < old point, start after - function testFuzz_case7_AmountLessStartAfter( - uint256 _tokenId, - LockedBalance memory _oldLocked, - uint32 _warp - ) public { - // Initialize state - _setValidateState(_tokenId, _oldLocked, _warp); - - vm.assume(_oldLocked.amount > 0); - - // Variables - TokenPoint memory oldPoint; - TokenPoint memory newPoint; - LockedBalance memory _newLocked = LockedBalance( - _oldLocked.amount - 1, - _oldLocked.start + 1 - ); + // fast forward after starting + vm.warp(300); - // Case 7: amount less, start greater - // expect new point written and lower - (oldPoint, newPoint) = curve.tokenCheckpoint(_tokenId, _oldLocked, _newLocked); - - assertEq(curve.tokenPointIntervals(_tokenId), 2, "C7: token interval incorrect"); - // assertEq(newPoint.bias, _newLocked.amount, "C7: bias incorrect"); - assertEq(newPoint.checkpointTs, _newLocked.start, "C7: checkpointTs incorrect"); - assertEq(newPoint.writtenTs, _warp, "C7: writtenTs incorrect"); - assertEq( - newPoint.coefficients[0] / 1e18, - curve.getCoefficients(_newLocked.amount)[0], - "C7: constant incorrect" - ); - assertEq( - newPoint.coefficients[1] / 1e18, - curve.getCoefficients(_newLocked.amount)[1], - "C7: linear incorrect" + // write another point which is a reduction + LockedBalance memory reducedLock = LockedBalance({start: 200, amount: 0.5e18}); + (TokenPoint memory oldPoint, TokenPoint memory newPoint) = curve.tokenCheckpoint( + 1, + reducedLock ); - } - // Boilerplate for case 8: amount = old point, start before - function testFuzz_case8_AmountEqualStartBefore( - uint256 _tokenId, - LockedBalance memory _oldLocked, - uint32 _warp - ) public { - // Initialize state - _setValidateState(_tokenId, _oldLocked, _warp); + // check the bias and slope correct + uint expectedBias = curve.getBias(100, 0.5e18); + assertEq(uint(newPoint.coefficients[0]) / 1e18, expectedBias); + assertEq(newPoint.coefficients[1], curve.previewPoint(0.5e18).coefficients[1]); + assertEq(newPoint.checkpointTs, 300); + assertEq(newPoint.writtenTs, 300); - vm.assume(_oldLocked.start > 0); // start > 0 to avoid underflow + // token interval == 2 + assertEq(curve.tokenPointIntervals(1), 2); + assertEq(curve.tokenPointHistory(1, 2).checkpointTs, 300); + assertEq(uint(curve.tokenPointHistory(1, 2).coefficients[0]) / 1e18, expectedBias); - // Variables - TokenPoint memory oldPoint; - TokenPoint memory newPoint; - LockedBalance memory _newLocked = LockedBalance(_oldLocked.amount, _oldLocked.start - 1); + // exit - // start before so expect revert - vm.expectRevert(InvalidCheckpoint.selector); - (oldPoint, newPoint) = curve.tokenCheckpoint(_tokenId, _oldLocked, _newLocked); - } - - // Boilerplate for case 9: amount = old point, start after - function testFuzz_case9_AmountEqualStartAfter( - uint256 _tokenId, - LockedBalance memory _oldLocked, - uint32 _warp - ) public { - // Initialize state - _setValidateState(_tokenId, _oldLocked, _warp); - - // Variables - TokenPoint memory oldPoint; - TokenPoint memory newPoint; - LockedBalance memory _newLocked = LockedBalance(_oldLocked.amount, _oldLocked.start + 1); - - // Case 9: amount = old point, start after - // expect new point written - (oldPoint, newPoint) = curve.tokenCheckpoint(_tokenId, _oldLocked, _newLocked); - - assertEq(curve.tokenPointIntervals(_tokenId), 2, "C9: token interval incorrect"); - // assertEq(newPoint.bias, _newLocked.amount, "C9: bias incorrect"); - assertEq(newPoint.checkpointTs, _newLocked.start, "C9: checkpointTs incorrect"); - assertEq(newPoint.writtenTs, _warp, "C9: writtenTs incorrect"); - assertEq( - newPoint.coefficients[0] / 1e18, - curve.getCoefficients(_newLocked.amount)[0], - "C9: constant incorrect" + vm.warp(400); + LockedBalance memory exitLock = LockedBalance({start: 200, amount: 0}); + (TokenPoint memory exitOldPoint, TokenPoint memory exitPoint) = curve.tokenCheckpoint( + 1, + exitLock ); - assertEq( - newPoint.coefficients[1] / 1e18, - curve.getCoefficients(_newLocked.amount)[1], - "C9: linear incorrect" - ); - } - function testFuzz_newAmountAtDifferentBlockTimestampAfter( - uint256 _tokenId, - LockedBalance memory _oldLocked, - uint32 _warp, - uint32 _change, - uint128 _newAmount - ) public { - _setValidateState(_tokenId, _oldLocked, _warp); - vm.assume(_oldLocked.start < type(uint32).max); - - TokenPoint memory oldPoint; - TokenPoint memory newPoint; - - uint48 warpTime = uint48(_warp) + uint48(_change); - - vm.warp(warpTime); - - LockedBalance memory _newLocked = LockedBalance(_newAmount, _oldLocked.start); - (oldPoint, newPoint) = curve.tokenCheckpoint(_tokenId, _oldLocked, _newLocked); - - // expectation: new point written with new bias - assertEq(curve.tokenPointIntervals(_tokenId), 2, "C10: token interval incorrect"); - - uint elapsed = (_oldLocked.start > block.timestamp) - ? 0 - : block.timestamp - _oldLocked.start; - - uint newBias = curve.getBias(elapsed, _newAmount); - uint oldBias = curve.getBias(elapsed, _oldLocked.amount); - - // assertEq(newPoint.bias, newBias, "C10: new bias incorrect"); - - // if the new amount is the same, should be equivalent to the old lock - if (_newAmount == _oldLocked.amount) { - assertEq(newBias, oldBias, "C10: old and new bias should be the same"); - } - // if the new amount is less then the bias should be less than the equivalent - else if (_newAmount < _oldLocked.amount) { - assertLt(newBias, oldBias, "C10: new bias should be less than old bias"); - } - // if the new amount is greater then the bias should be greater than the equivalent - else { - assertGt(newBias, oldBias, "C10: new bias should be greater than old bias"); - } - - assertEq(newPoint.checkpointTs, _oldLocked.start, "C10: checkpointTs incorrect"); - assertEq(newPoint.writtenTs, warpTime, "C10: writtenTs incorrect"); - assertEq( - newPoint.coefficients[0] / 1e18, - curve.getCoefficients(_newLocked.amount)[0], - "C10: constant incorrect" - ); - assertEq( - newPoint.coefficients[1] / 1e18, - curve.getCoefficients(_newLocked.amount)[1], - "C10: linear incorrect" - ); - } + // check zero + assertEq(exitPoint.coefficients[0], 0); + assertEq(exitPoint.coefficients[1], 0); + assertEq(exitPoint.checkpointTs, 400); + assertEq(exitPoint.writtenTs, 400); - function testFuzz_newAmountAtDifferentBlockTimestampBefore( - uint256 _tokenId, - LockedBalance memory _oldLocked, - uint32 _warp, - uint32 _change, - uint128 _newAmount - ) public { - _setValidateState(_tokenId, _oldLocked, _warp); - vm.assume(_oldLocked.start < type(uint32).max); - - TokenPoint memory oldPoint; - TokenPoint memory newPoint; - - uint48 warpTime = uint48(_warp) + uint48(_change); - - // write the state prior to warping - LockedBalance memory _newLocked = LockedBalance(_newAmount, _oldLocked.start); - (oldPoint, newPoint) = curve.tokenCheckpoint(_tokenId, _oldLocked, _newLocked); - - vm.warp(warpTime); - - // expectation: new point written with new bias - assertEq(curve.tokenPointIntervals(_tokenId), 2, "C10: token interval incorrect"); - - uint elapsed = (_oldLocked.start > block.timestamp) - ? 0 - : block.timestamp - _oldLocked.start; - - uint newBias = curve.getBias(elapsed, _newAmount); - uint oldBias = curve.getBias(elapsed, _oldLocked.amount); - - // assertEq(newPoint.bias, newBias, "C10: new bias incorrect"); - - // if the new amount is the same, should be equivalent to the old lock - if (_newAmount == _oldLocked.amount) { - assertEq(newBias, oldBias, "C10: old and new bias should be the same"); - } - // if the new amount is less then the bias should be less than the equivalent - else if (_newAmount < _oldLocked.amount) { - assertLt(newBias, oldBias, "C10: new bias should be less than old bias"); - } - // if the new amount is greater then the bias should be greater than the equivalent - else { - assertGt(newBias, oldBias, "C10: new bias should be greater than old bias"); - } - - assertEq(newPoint.checkpointTs, _oldLocked.start, "C10: checkpointTs incorrect"); - assertEq(newPoint.writtenTs, warpTime, "C10: writtenTs incorrect"); - assertEq( - newPoint.coefficients[0] / 1e18, - curve.getCoefficients(_newLocked.amount)[0], - "C10: constant incorrect" - ); - assertEq( - newPoint.coefficients[1] / 1e18, - curve.getCoefficients(_newLocked.amount)[1], - "C10: linear incorrect" - ); + // token interval == 3 + assertEq(curve.tokenPointIntervals(1), 3); + assertEq(curve.tokenPointHistory(1, 3).checkpointTs, 400); + assertEq(exitPoint.coefficients[0], 0); } - function testFuzz_exit( - uint256 _tokenId, - LockedBalance memory _oldLocked, - uint32 _warp, - uint32 _exit - ) public { - // Initialize state - _setValidateState(_tokenId, _oldLocked, _warp); + // maxes out correctly single + function testMultipleCheckpointMaxesBias() public { + vm.warp(1 weeks); - vm.assume(_oldLocked.start < type(uint32).max); + uint48 start = 2 weeks; + // scheduled start + LockedBalance memory lock = LockedBalance({start: start, amount: 1e18}); + curve.tokenCheckpoint(1, lock); - // Variables - TokenPoint memory oldPoint; - TokenPoint memory newPoint; + // fast forward to max time + start + 10 weeks + vm.warp(start + curve.maxTime() + 10 weeks); - uint48 warpTime = uint48(_warp) + uint48(_exit); + // write a new point w. 50% + LockedBalance memory reducedLock = LockedBalance({start: start, amount: 0.5e18}); - vm.warp(warpTime); + (TokenPoint memory oldPoint, TokenPoint memory newPoint) = curve.tokenCheckpoint( + 1, + reducedLock + ); - LockedBalance memory _newLocked = LockedBalance(0, _oldLocked.start + _exit); - (oldPoint, newPoint) = curve.tokenCheckpoint(_tokenId, _oldLocked, _newLocked); + // expect the times to be correct but the bias should be the max of the 0.5e18 + uint expectedBias = curve.getBias(curve.maxTime(), 0.5e18); - // expectation: new point written and everything cleared out - assertEq( - curve.tokenPointIntervals(_tokenId), - _exit == 0 ? 1 : 2, - "exit: token interval incorrect" - ); - // assertEq(newPoint.bias, 0, "exit: bias incorrect"); - assertEq(newPoint.checkpointTs, _oldLocked.start + _exit, "exit: checkpointTs incorrect"); - assertEq(newPoint.writtenTs, warpTime, "exit: writtenTs incorrect"); - assertEq(newPoint.coefficients[0], 0, "exit: constant incorrect"); - assertEq(newPoint.coefficients[1], 0, "exit: linear incorrect"); + assertEq(uint(newPoint.coefficients[0]) / 1e18, expectedBias); + assertEq(newPoint.coefficients[1], curve.previewPoint(0.5e18).coefficients[1]); + assertEq(newPoint.checkpointTs, start + curve.maxTime() + 10 weeks); + assertEq(newPoint.writtenTs, start + curve.maxTime() + 10 weeks); } - - // think deeply about different TIMES of writing, not just checkpoints } diff --git a/test/escrow/curve/linear/LinearVotingPower.t.sol b/test/escrow/curve/linear/LinearVotingPower.t.sol new file mode 100644 index 0000000..be0236f --- /dev/null +++ b/test/escrow/curve/linear/LinearVotingPower.t.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {console2 as console} from "forge-std/console2.sol"; + +import {LinearIncreasingEscrow, IVotingEscrow, IEscrowCurve} from "src/escrow/increasing/LinearIncreasingEscrow.sol"; +import {IVotingEscrowIncreasing, ILockedBalanceIncreasing} from "src/escrow/increasing/interfaces/IVotingEscrowIncreasing.sol"; + +import {LinearCurveBase} from "./LinearBase.sol"; +contract TestLinearVotingPower is LinearCurveBase { + // returns zero by default + function testGetVotingPowerReturnsZero(uint _tokenId, uint _t) public view { + assertEq(curve.votingPowerAt(_tokenId, _t), 0); + } + + // for a token within warmup, returns zero + // set a random warmup + // write the point at ts = 1 + // move to 1 second before + // vp == 0 + function testGetVotingPowerReturnsZeroForWarmup(uint48 _warmup) public { + vm.assume(_warmup > 1); + vm.assume(_warmup < type(uint48).max); + + curve.setWarmupPeriod(_warmup); + + TokenPoint memory point = curve.previewPoint(10); // real point + point.writtenTs = 1; + point.checkpointTs = 1; + + // write a point + curve.writeNewTokenPoint(1, point, 1); + + // check the voting power + vm.warp(_warmup + point.writtenTs); + assertEq(curve.votingPowerAt(1, block.timestamp), 0); + + // after warmup, return gt 0 + vm.warp(_warmup + point.writtenTs + 1); + assertGt(curve.votingPowerAt(1, block.timestamp), 0); + } + + // maxes correctly + // set a lock start of 1 + // set point + // point voting power at max should be max && gt 0 + // at 2*max same + // with low amounts the amount doesn't increase + function testTokenMaxesAtMaxTime() public { + uint208 amount = 1e18; + uint128 start = 1; + uint max = start + curve.maxTime(); + + escrow.setLockedBalance(1, LockedBalance({start: uint48(start), amount: amount})); + + TokenPoint memory point = curve.previewPoint(amount); // real point + point.checkpointTs = start; + + // write a point + curve.writeNewTokenPoint(1, point, 1); + + // warp to max time + vm.warp(max - 1); + + uint votingPowerPreMax = curve.votingPowerAt(1, block.timestamp); + + vm.warp(max); + + uint votingPowerAtMax = curve.votingPowerAt(1, block.timestamp); + + vm.warp(max + 1); + + uint votingPowerPostMax = curve.votingPowerAt(1, block.timestamp); + + assertGt(votingPowerPreMax, 0); + assertGt(votingPowerAtMax, votingPowerPreMax); + assertEq(votingPowerAtMax, votingPowerPostMax); + } + + // for a token w. 2 points + + // if the first point is in warmup returns zero + function testIfFirstPointInWarmupReturnsZero() public { + uint208 amount = 1e18; + uint48 warmup = 100; + + curve.setWarmupPeriod(warmup); + + // write 2 points: p1 at 1, p2 after warmup + TokenPoint memory p1 = curve.previewPoint(amount); + TokenPoint memory p2 = curve.previewPoint(amount); + + p1.writtenTs = 1; + p1.checkpointTs = 1; + + p2.writtenTs = warmup + 1; + p2.checkpointTs = warmup + 1; + + curve.writeNewTokenPoint(1, p1, 1); + curve.writeNewTokenPoint(1, p2, 2); + + vm.warp(warmup); + assertEq(curve.votingPowerAt(1, block.timestamp), 0); + } + + // if the second point was written lt warmup seconds ago, but p1 outside warmup, returns postive + function testIfFirstPointOutsideWarmupSecondPointInsideWarmupReturnsPositive() public { + uint208 amount = 1e18; + uint48 warmup = 100; + + curve.setWarmupPeriod(warmup); + + // write 2 points: p1 at 1, p2 after warmup + TokenPoint memory p1 = curve.previewPoint(amount); + TokenPoint memory p2 = curve.previewPoint(amount); + + p1.writtenTs = 1; + p1.checkpointTs = 1; + + p2.writtenTs = warmup - 1; + p2.checkpointTs = warmup - 1; + + curve.writeNewTokenPoint(1, p1, 1); + curve.writeNewTokenPoint(1, p2, 2); + + vm.warp(warmup + p1.writtenTs + 1); + assertGt(curve.votingPowerAt(1, block.timestamp), 0); + } + + // for 2 points, correctly calulates the bias based on latest point + + // for 2 points, correctly maxes the bias at the original lock start + function test2PointsMaxesCorrectlySameAmount() public { + uint208 amount = 1e18; + uint48 start = 1; + uint48 max = start + curve.maxTime(); + + // original start is at 1 + escrow.setLockedBalance(1, LockedBalance({start: start, amount: amount})); + + TokenPoint memory p1 = curve.previewPoint(amount); // real point + TokenPoint memory p2 = curve.previewPoint(amount); // real point + + p1.checkpointTs = start; + p2.checkpointTs = 1000; + + // write a point + curve.writeNewTokenPoint(1, p1, 1); + curve.writeNewTokenPoint(1, p2, 2); + + // warp to max time + vm.warp(max - 1); + + uint votingPowerPreMax = curve.votingPowerAt(1, block.timestamp); + + vm.warp(max); + + uint votingPowerAtMax = curve.votingPowerAt(1, block.timestamp); + + vm.warp(max + 1); + + uint votingPowerPostMax = curve.votingPowerAt(1, block.timestamp); + + assertGt(votingPowerPreMax, 0); + assertGt(votingPowerAtMax, votingPowerPreMax, "votingPowerAtMax > votingPowerPreMax"); + assertEq(votingPowerAtMax, votingPowerPostMax, "votingPowerAtMax == votingPowerPostMax"); + } + + // in the event that you have 2 points but the second point + // is after the first, which is synced to the lock start: + // the max should be based on the lock start + // but the coefficients[0] of the second point is based on the + // evaluated bias of the first point at the second point's checkpoint + // this means second point should only keep accruing voting power + // for the duration of the lock start + // an example: + // I lock 100 tokens and they double after 2 years and don't keep increasing + // I drop to 50 tokens at half a year + // At t = 12 months - 1 second I'm at 150 tokens + // At t = 12 months I'm at: 75 tokens + // I don't then restart the max from 12 months, I start the max from + // the original lock start meaning I increase from 75 to 100 + // over the next 12 months + function test2PointsMaxesCorrectlyDiffAmount() public { + uint208 amount = 100e18; + uint amount2 = 50e18; + uint48 start = 1; + uint48 max = start + curve.maxTime(); // 2y + + // original start is at 1 + escrow.setLockedBalance(1, LockedBalance({start: start, amount: amount})); + + TokenPoint memory p1 = curve.previewPoint(amount); // real point + TokenPoint memory p2 = curve.previewPoint(amount2); // + + p1.checkpointTs = start; + p2.checkpointTs = start + 52 weeks; + + // overwrite the bias at the checkpoint with the bias evaluated at 52 weeks + p2.coefficients[0] = curve.getBiasUnbound(52 weeks, amount2); + + // write a point + curve.writeNewTokenPoint(1, p1, 1); + curve.writeNewTokenPoint(1, p2, 2); + + // warp to max time + vm.warp(max - 1); + + uint votingPowerPreMax = curve.votingPowerAt(1, block.timestamp); + + vm.warp(max); + + uint votingPowerAtMax = curve.votingPowerAt(1, block.timestamp); + + vm.warp(max + 1); + + uint votingPowerPostMax = curve.votingPowerAt(1, block.timestamp); + + assertGt(votingPowerPreMax, 0); + assertGt(votingPowerAtMax, votingPowerPreMax, "vp @ max > vp @ max - 1"); + assertEq(votingPowerPostMax, votingPowerAtMax, "vp @ max == vp @ max + 1"); + + assertEq( + votingPowerPostMax, + curve.getBias(curve.maxTime(), amount2), + "vp @ max == bias @ max for the lower amount" + ); + // check history is preserved + assertEq( + curve.votingPowerAt(1, start + 26 weeks), + curve.getBias(26 weeks, amount), + "vp @ 26 weeks" + ); + // after increase + assertEq( + curve.votingPowerAt(1, start + 75 weeks), + curve.getBias(75 weeks, amount2), + "vp @ 39 weeks" + ); + } +} diff --git a/test/escrow/curve/linear/LinearWriteHistory.t.sol b/test/escrow/curve/linear/LinearWriteHistory.t.sol index 16348dd..4ed1947 100644 --- a/test/escrow/curve/linear/LinearWriteHistory.t.sol +++ b/test/escrow/curve/linear/LinearWriteHistory.t.sol @@ -31,27 +31,32 @@ contract TestLinearIncreasingPopulateHistory is LinearCurveBase { } // in the case of no history we will return nothing unless exactly on the boundary + + // so, if function testNoHistorySingleSchedule(uint32 _warp) public { uint interval = clock.checkpointInterval(); vm.assume(_warp >= interval); + _warp = 3024000; vm.warp(_warp); - uint48 priorInterval = uint48(clock.epochNextCheckpointTs()) - uint48(clock.checkpointInterval()); // write a scheduled point curve.writeSchedule(priorInterval, [int256(1000), int256(2), int256(0)]); + // schedule the earliest point + curve.writeEarliestScheduleChange(priorInterval); + // populate the history (GlobalPoint memory point, uint index) = curve.populateHistory(); - // if we have a scheduled write exactly now, we should have the - // point written to memory but not storage as we have to add the user data later + // if we have a scheduled write exactly on the interval, we should have the + // point written to memory and storage if (priorInterval == _warp) { assertEq(point.coefficients[0], 1000, "coeff0 exact"); assertEq(point.coefficients[1], 2, "coeff1 exact"); assertEq(index, 1, "index exact"); - assertEq(curve.pointHistory(1).coefficients[0], 0, "ph exact"); + assertEq(curve.pointHistory(1).coefficients[0], 1000, "ph exact"); } // otherwise expect nothing else { @@ -65,8 +70,9 @@ contract TestLinearIncreasingPopulateHistory is LinearCurveBase { // test no prior history, no schedulling - returns CI 0, empty point with block.ts function testGetLatestPointNoPriorNoSchedule(uint32 warp) public { vm.warp(warp); - GlobalPoint memory point = curve.getLatestGlobalPointOrWriteFirstPoint(); + (GlobalPoint memory point, uint idx) = curve.getLatestGlobalPointOrWriteFirstPoint(); + assertEq(idx, 0, "idx"); assertEq(point.ts, block.timestamp, "ts"); assertEq(point.bias, 0, "bias"); assertEq(point.coefficients[0], 0, "coeff0"); @@ -81,8 +87,10 @@ contract TestLinearIncreasingPopulateHistory is LinearCurveBase { vm.assume(warp < type(uint48).max); vm.warp(warp); curve.writeEarliestScheduleChange(warp + 1); - GlobalPoint memory point = curve.getLatestGlobalPointOrWriteFirstPoint(); + (GlobalPoint memory point, uint idx) = curve.getLatestGlobalPointOrWriteFirstPoint(); + + assertEq(idx, 0, "idx"); assertEq(point.ts, block.timestamp, "ts"); assertEq(point.bias, 0, "bias"); assertEq(point.coefficients[0], 0, "coeff0"); @@ -99,8 +107,9 @@ contract TestLinearIncreasingPopulateHistory is LinearCurveBase { vm.warp(warp); curve.writeEarliestScheduleChange(warp - 1); curve.writeSchedule(warp - 1, [int(1), int(2), int(0)]); - GlobalPoint memory point = curve.getLatestGlobalPointOrWriteFirstPoint(); + (GlobalPoint memory point, uint idx) = curve.getLatestGlobalPointOrWriteFirstPoint(); + assertEq(idx, 1, "idx"); assertEq(point.ts, warp - 1, "ts"); assertEq(point.bias, 0, "bias"); // TODO assertEq(point.coefficients[0], 1, "coeff0"); @@ -120,8 +129,9 @@ contract TestLinearIncreasingPopulateHistory is LinearCurveBase { vm.assume(warp > 0); vm.warp(warp); curve.writeNewGlobalPoint(GlobalPoint(1, warp - 1, [int(1), int(2), int(0)]), 123); - GlobalPoint memory point = curve.getLatestGlobalPointOrWriteFirstPoint(); + (GlobalPoint memory point, uint idx) = curve.getLatestGlobalPointOrWriteFirstPoint(); + assertEq(idx, 123, "idx"); assertEq(point.ts, warp - 1, "ts"); assertEq(point.bias, 1, "bias"); assertEq(point.coefficients[0], 1, "coeff0"); @@ -465,10 +475,10 @@ contract TestLinearIncreasingPopulateHistory is LinearCurveBase { assertEq(p1.coefficients[0], globalPrev.coefficients[0]); assertEq(p1.coefficients[1], globalPrev.coefficients[1]); - // point 2 - 4 not written + // point 2 - 4 weekly points for (uint i = 2; i < 5; i++) { GlobalPoint memory p = curve.pointHistory(i); - assertEq(p.ts, 0); + assertEq(p.ts, i * 1 weeks); } // point 6 not written @@ -623,8 +633,11 @@ contract TestLinearIncreasingPopulateHistory is LinearCurveBase { "p2.coefficients[1]" ); - // point 3 is zero as sparse - assertEq(p3.ts, 0); + // point 3 is at the week boundary + assertEq(p3.ts, 3 weeks, "p3.ts != 3 weeks"); + // it should just be the continuation of the slope + uint expectedCoeff0p3 = curve.getBias(2 weeks, 10e18) + curve.getBias(1 weeks, 1e18); + assertEq(uint(p3.coefficients[0]) / 1e18, expectedCoeff0p3, "p3.coefficients[0]"); // point 4 should be written but the coefficients should now stop increasing assertEq(p4.ts, 4 weeks, "p4.ts != 4 weeks"); @@ -635,7 +648,7 @@ contract TestLinearIncreasingPopulateHistory is LinearCurveBase { uint expectedCoeff0p4 = curve.getBias(3 weeks, 10e18) + curve.getBias(2 weeks, 1e18); assertEq(uint(p4.coefficients[0]) / 1e18, expectedCoeff0p4, "p4.coefficients[0]"); - // point 5 should not be written as no changes + // point 5 should not be written as ahead of time assertEq(p5.ts, 0, "p5.ts != block.timestamp"); // last point in memory should be same as point 4