From fd92a3289f1af0062752fa0d0adad1f4e212ae4e Mon Sep 17 00:00:00 2001 From: jordaniza Date: Sun, 20 Oct 2024 22:33:34 +0400 Subject: [PATCH] close but some logic to check --- .../increasing/LinearIncreasingEscrow.sol | 99 +++-- .../linear/LinearApplyTokenToGlobal.t.sol | 169 ++++++++ test/escrow/curve/linear/LinearBase.sol | 32 ++ .../curve/linear/LinearCheckpoint.t.sol | 362 ++++++++++++++++++ test/python/global_state.py | 79 ++++ 5 files changed, 706 insertions(+), 35 deletions(-) create mode 100644 test/escrow/curve/linear/LinearCheckpoint.t.sol create mode 100644 test/python/global_state.py diff --git a/src/escrow/increasing/LinearIncreasingEscrow.sol b/src/escrow/increasing/LinearIncreasingEscrow.sol index a81459d..5ab746c 100644 --- a/src/escrow/increasing/LinearIncreasingEscrow.sol +++ b/src/escrow/increasing/LinearIncreasingEscrow.sol @@ -62,16 +62,26 @@ contract LinearIncreasingEscrow is uint256 internal _latestPointIndex; + // changes are stored in fixed point mapping(uint48 => int256[3]) internal _scheduledCurveChanges; uint48 internal _earliestScheduledChange; - function pointHistory(uint256 _loc) external view returns (GlobalPoint memory) { - return _pointHistory[_loc]; + /// @notice emulation of array like structure starting at 1 index for global points + function pointHistory(uint256 _index) external view returns (GlobalPoint memory) { + return _pointHistory[_index]; } function scheduledCurveChanges(uint48 _at) external view returns (int256[3] memory) { - return _scheduledCurveChanges[_at]; + return [ + SignedFixedPointMath.fromFP(_scheduledCurveChanges[_at][0]), + SignedFixedPointMath.fromFP(_scheduledCurveChanges[_at][1]), + 0 + ]; + } + + function latestPointIndex() external view returns (uint) { + return _latestPointIndex; } function maxTime() external view returns (uint48) { @@ -265,6 +275,7 @@ contract LinearIncreasingEscrow is } return lower; } + function votingPowerAt(uint256 _tokenId, uint256 _t) external view returns (uint256) { uint256 interval = _getPastTokenPointInterval(_tokenId, _t); @@ -300,9 +311,22 @@ contract LinearIncreasingEscrow is IVotingEscrow.LockedBalance memory _newLocked ) external nonReentrant { if (msg.sender != escrow) revert OnlyEscrow(); + if (_tokenId == 0) revert InvalidTokenId(); + if (!validateLockedBalances(_oldLocked, _newLocked)) revert("Invalid Locked Balances"); _checkpoint(_tokenId, _oldLocked, _newLocked); } + /// @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(); + + if (currentIndex != _latestPointIndex) { + _latestPointIndex = currentIndex == 0 ? 1 : currentIndex; + _pointHistory[currentIndex] = latestPoint; + } + } + /// @dev Main checkpointing function for token and global state /// @param _tokenId The NFT token ID /// @param _oldLocked The previous locked balance and start time @@ -312,9 +336,6 @@ contract LinearIncreasingEscrow is IVotingEscrow.LockedBalance memory _oldLocked, IVotingEscrow.LockedBalance memory _newLocked ) internal { - if (_tokenId == 0) revert InvalidTokenId(); - if (!validateLockedBalances(_oldLocked, _newLocked)) revert("Invalid Locked Balances"); - // write the token checkpoint (TokenPoint memory oldTokenPoint, TokenPoint memory newTokenPoint) = _tokenCheckpoint( _tokenId, @@ -335,7 +356,7 @@ contract LinearIncreasingEscrow is bool tokenHasUpdateNow = newTokenPoint.checkpointTs == latestPoint.ts; if (tokenHasUpdateNow) { latestPoint = _applyTokenUpdateToGlobal( - _newLocked.start, + _newLocked.start, // TODO oldTokenPoint, newTokenPoint, latestPoint @@ -506,9 +527,8 @@ contract LinearIncreasingEscrow is internal returns (GlobalPoint memory latestPoint) { - uint index = _latestPointIndex; - // early return the point if we have it + uint index = _latestPointIndex; if (index > 0) return _pointHistory[index]; // determine if we have some existing state we need to start from @@ -571,6 +591,12 @@ contract LinearIncreasingEscrow is int biasChange = _scheduledCurveChanges[t_i][0]; int slopeChange = _scheduledCurveChanges[t_i][1]; + console.log("biasChange", biasChange / 1e18); + console.log("slopeChange", slopeChange / 1e18); + console.log("idx number", currentIndex); + console.log("prev-coeff", latestPoint.coefficients[0] / 1e36); + console.log("prev-slope", latestPoint.coefficients[1] / 1e18); + // we create a new "curve" by defining the coefficients starting from time t_i // our constant is the y intercept at t_i and is found by evalutating the curve between the last point and t_i latestPoint.coefficients[0] = @@ -582,6 +608,11 @@ contract LinearIncreasingEscrow is // this can be positive or negative depending on if new deposits outweigh tapering effects + withdrawals latestPoint.coefficients[1] += slopeChange; + console.log("new coeff", latestPoint.coefficients[0] / 1e36); + console.log("new slope", latestPoint.coefficients[1] / 1e18); + + console.log(""); + // the slope itself can't be < 0 so we bound it if (latestPoint.coefficients[1] < 0) { latestPoint.coefficients[1] = 0; @@ -596,7 +627,6 @@ contract LinearIncreasingEscrow is // 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 @@ -613,6 +643,10 @@ contract LinearIncreasingEscrow is } } + // issue here is that this will always return a new index if called mid interval + // meaning we will always write a new point even if there is no change that couldn't + // have been interpolated + console.log("returning currentIndex", currentIndex); return (latestPoint, currentIndex); } @@ -628,35 +662,30 @@ contract LinearIncreasingEscrow is TokenPoint memory _newPoint, GlobalPoint memory _latestGlobalPoint ) internal view returns (GlobalPoint memory) { - // this change should only be applied to the present and the latest point should be up to date - if (_newPoint.checkpointTs != block.timestamp) revert("point not up to date"); - if (_latestGlobalPoint.ts != block.timestamp) revert("point not up to date"); - - // what do we want to happen here? - - // 4 cases: - // 1. new deposit. In which case we just add the bias and coeff on to the latest point at time t - // 2. increasing. The slope and bias are changing. The voting power of the user at time t is the bias evaluated between the last point and now - // which then need - // 3. decreasing. I think this is the same just in reverse - // 4. exit. decreasing taken ad infinitum - - // evaluate the old curve up until now - uint256 timeElapsed = block.timestamp - _oldPoint.checkpointTs; - // note: this must get maxed out from the original start date - // will revert if lt 0 - uint256 oldUserBias = _getBiasUnbound(timeElapsed, _oldPoint.coefficients).toUint256(); - - // Subtract the old user's bias from the global bias at this point - _latestGlobalPoint.coefficients[0] -= int256(oldUserBias); - - // User is reducing, not exiting - if (_newPoint.bias > 0) { + if (_newPoint.checkpointTs != block.timestamp) revert("token point not up to date"); + if (_latestGlobalPoint.ts != block.timestamp) revert("global point not up to date"); + + // if there is something to be replaced (old point has data) + uint oldCp = _oldPoint.checkpointTs; + uint elapsed = 0; + if (oldCp != 0 && block.timestamp > oldCp) { + elapsed = block.timestamp - oldCp; + } + + // evaluate the old curve up until now and remove its impact from the bias + // TODO bounding to zero + int256 oldUserBias = _getBiasUnbound(elapsed, _oldPoint.coefficients); + console.log("Old user bias", oldUserBias); + _latestGlobalPoint.coefficients[0] -= oldUserBias; + + // if the new point is not an exit, then add it back in + if (_newPoint.coefficients[0] > 0) { // Add the new user's bias back to the global bias - _latestGlobalPoint.coefficients[0] += int256(_newPoint.bias); + _latestGlobalPoint.coefficients[0] += int256(_newPoint.coefficients[0]); } // the immediate reduction is slope requires removing the old and adding the new + // this could involve zero writes _latestGlobalPoint.coefficients[1] -= _oldPoint.coefficients[1]; _latestGlobalPoint.coefficients[1] += _newPoint.coefficients[1]; diff --git a/test/escrow/curve/linear/LinearApplyTokenToGlobal.t.sol b/test/escrow/curve/linear/LinearApplyTokenToGlobal.t.sol index e69de29..7543784 100644 --- a/test/escrow/curve/linear/LinearApplyTokenToGlobal.t.sol +++ b/test/escrow/curve/linear/LinearApplyTokenToGlobal.t.sol @@ -0,0 +1,169 @@ +// 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 TestLinearIncreasingApplyTokenChange is LinearCurveBase { + function _copyNewToOld( + TokenPoint memory _new, + TokenPoint memory _old + ) internal pure returns (TokenPoint memory copied) { + _old.coefficients[0] = _new.coefficients[0]; + _old.coefficients[1] = _new.coefficients[1]; + _old.writtenTs = _new.writtenTs; + _old.checkpointTs = _new.checkpointTs; + _old.bias = _new.bias; + return _old; + } + + // when run on an empty global point, adds the user's own point + // deposit + function testEmptyGlobalDeposit() public { + vm.warp(100); + GlobalPoint memory globalPoint; + globalPoint.ts = block.timestamp; + + TokenPoint memory oldPoint; + + TokenPoint memory newPoint = curve.previewPoint(10 ether); + newPoint.checkpointTs = uint128(block.timestamp); + // write it + + globalPoint = curve.applyTokenUpdateToGlobal(0, oldPoint, newPoint, globalPoint); + + uint expectedBias = 10 ether; + + assertEq(uint(globalPoint.coefficients[0]) / 1e18, expectedBias); + assertEq(globalPoint.ts, block.timestamp); + assertEq(globalPoint.coefficients[1], newPoint.coefficients[1]); + } + + // change - same block + function testEmptyGlobalChangeSameBlock(uint128 _newBalance) public { + vm.warp(100); + GlobalPoint memory globalPoint; + globalPoint.ts = block.timestamp; + + TokenPoint memory oldPoint; + + TokenPoint memory newPoint0 = curve.previewPoint(10 ether); + newPoint0.checkpointTs = uint128(block.timestamp); + globalPoint = curve.applyTokenUpdateToGlobal(0, oldPoint, newPoint0, globalPoint); + + // copy the new to old point and redefine the new point + oldPoint = _copyNewToOld(newPoint0, oldPoint); + + TokenPoint memory newPoint1 = curve.previewPoint(_newBalance); + newPoint1.checkpointTs = uint128(block.timestamp); + + GlobalPoint memory newGlobalPoint = curve.applyTokenUpdateToGlobal( + 0, + oldPoint, + newPoint1, + globalPoint + ); + + assertEq(uint(newGlobalPoint.coefficients[0]) / 1e18, _newBalance); + assertEq(newGlobalPoint.ts, block.timestamp); + assertEq(newGlobalPoint.coefficients[1], newPoint1.coefficients[1]); + } + + // when run on an existing global point, increments the global point + // deposit + function testDepositOnExistingGlobalState() public { + vm.warp(100); + + 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]; + + TokenPoint memory oldPoint; // 0 + + TokenPoint memory newPoint = curve.previewPoint(10 ether); + newPoint.checkpointTs = uint128(block.timestamp); + + int cachedSlope = globalPoint.coefficients[1]; + + globalPoint = curve.applyTokenUpdateToGlobal(0, oldPoint, newPoint, globalPoint); + + // expectation: bias && slope incremented + assertEq(uint(globalPoint.coefficients[0]) / 1e18, 110 ether); + assertEq(globalPoint.ts, block.timestamp); + assertEq(globalPoint.coefficients[1], cachedSlope + newPoint.coefficients[1]); + } + + // change - elapsed + function testChangeOnExistingGlobalStateElapsedTime() public { + vm.warp(100); + + 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]; + + TokenPoint memory oldPoint; // 0 + + TokenPoint memory newPoint0 = curve.previewPoint(10 ether); + newPoint0.checkpointTs = uint128(block.timestamp); + + globalPoint = curve.applyTokenUpdateToGlobal(0, oldPoint, newPoint0, globalPoint); + int cachedCoeff0 = globalPoint.coefficients[0]; + + // copy the new to old point and redefine the new point + oldPoint = _copyNewToOld(newPoint0, oldPoint); + + 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 + globalPoint.coefficients[0] = curve.getBiasUnbound(100, globalPoint.coefficients); + globalPoint.ts = block.timestamp; + + GlobalPoint memory newGlobalPoint = curve.applyTokenUpdateToGlobal( + 0, + 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) - + curve.getBias(100, 10 ether) + + 20 ether; + int expectedCoeff1 = globalPoint.coefficients[1] - + newPoint0.coefficients[1] + + newPoint1.coefficients[1]; + + assertEq(uint(newGlobalPoint.coefficients[0]) / 1e18, uint(expectedCoeff0)); + assertEq(newGlobalPoint.ts, block.timestamp); + assertEq(newGlobalPoint.coefficients[1], expectedCoeff1); + } + + // decrease + // exit + + // same block change + // deposit + // increase + // decrease + // exit + + // TODO a few extra tests here + + // change - future? + + // correctly maxes out based on the lockStart +} diff --git a/test/escrow/curve/linear/LinearBase.sol b/test/escrow/curve/linear/LinearBase.sol index e4ee506..d4e6ecb 100644 --- a/test/escrow/curve/linear/LinearBase.sol +++ b/test/escrow/curve/linear/LinearBase.sol @@ -81,6 +81,38 @@ contract MockLinearIncreasingEscrow is LinearIncreasingEscrow { point.coefficients = _getCoefficients(amount); point.bias = _getBias(0, point.coefficients); } + + function applyTokenUpdateToGlobal( + uint lockStart, + TokenPoint memory _oldPoint, + TokenPoint memory _newPoint, + GlobalPoint memory _latestGlobalPoint + ) external view returns (GlobalPoint memory) { + return _applyTokenUpdateToGlobal(lockStart, _oldPoint, _newPoint, _latestGlobalPoint); + } + + function getBiasUnbound( + uint elapsed, + int[3] memory coefficients + ) external view returns (int256) { + return _getBiasUnbound(elapsed, coefficients); + } + + function getBiasUnbound(uint elapsed, uint amount) external view returns (int256) { + return _getBiasUnbound(elapsed, _getCoefficients(amount)); + } + + function unsafeCheckpoint( + uint256 _tokenId, + IVotingEscrow.LockedBalance memory _oldLocked, + IVotingEscrow.LockedBalance memory _newLocked + ) external { + return _checkpoint(_tokenId, _oldLocked, _newLocked); + } + + function unsafeManualCheckpoint() external { + return _checkpoint(); + } } contract LinearCurveBase is TestHelpers, ILockedBalanceIncreasing, IEscrowCurveEventsErrorsStorage { diff --git a/test/escrow/curve/linear/LinearCheckpoint.t.sol b/test/escrow/curve/linear/LinearCheckpoint.t.sol new file mode 100644 index 0000000..f639eda --- /dev/null +++ b/test/escrow/curve/linear/LinearCheckpoint.t.sol @@ -0,0 +1,362 @@ +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 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 + + // test a multi user case: + // 3 users random smashes of populate history + // user 1 deposits 1m ether a couple days into week 1 + // call just after E0W + // user 2 deposits 0.5m ether a couple days into week 2, then changes mind to 1m + // user 3 deposits 1m ether exactly on week 5 + // user 1 reduces later in the same block to 0.5m + // user 2 exits mid week + // user 1 reduces later mid week + // wait until end of max period + // user 1 exits at week interval + // user 3 exits same time + function testCheckpoint() public { + uint shane = 1; + uint matt = 2; + uint phil = 3; + + vm.warp(1 weeks + 1 days); + + // get the next cp interval + uint48 nextInterval = uint48(clock.epochNextCheckpointTs()); + + curve.unsafeCheckpoint( + shane, + LockedBalance(0, 0), + LockedBalance({start: nextInterval, amount: 1_000_000e18}) + ); + + { + int slope = curve.getCoefficients(1_000_000e18)[1]; + + // assertions: + // current index still 0 + // scheduled curve changes written correctly to the future + assertEq(curve.latestPointIndex(), 0, "index should be zero"); + assertEq(curve.scheduledCurveChanges(nextInterval)[0], 1_000_000e18); + assertEq(curve.scheduledCurveChanges(nextInterval)[1], slope); + + // and in the future + assertEq(curve.scheduledCurveChanges(nextInterval + curve.maxTime())[0], 0); + assertEq(curve.scheduledCurveChanges(nextInterval + curve.maxTime())[1], -slope); + } + + // move a couple days forward and check a manual populate doesn't work + vm.warp(1 weeks + 5 days); + curve.unsafeManualCheckpoint(); + + { + int slope = curve.getCoefficients(1_000_000e18)[1]; + + // assertions: + // current index still 0 + // scheduled curve changes written correctly to the future + assertEq(curve.latestPointIndex(), 0, "index should be zero"); + assertEq(curve.scheduledCurveChanges(nextInterval)[0], 1_000_000e18); + assertEq(curve.scheduledCurveChanges(nextInterval)[1], slope); + + // and in the future + assertEq(curve.scheduledCurveChanges(nextInterval + curve.maxTime())[0], 0); + assertEq(curve.scheduledCurveChanges(nextInterval + curve.maxTime())[1], -slope); + } + + // cross the start date and do the second deposit + vm.warp(2 weeks + 3 days); + + uint48 prevInterval = nextInterval; + nextInterval = uint48(clock.epochNextCheckpointTs()); + + curve.unsafeCheckpoint( + matt, + LockedBalance(0, 0), + LockedBalance({start: nextInterval, amount: 500_000e18}) + ); + + { + // assertions: + int baseSlope = curve.getCoefficients(1_000_000e18)[1]; + 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"); + + // the first point has been applied at the prev interval + GlobalPoint memory p0 = curve.pointHistory(0); + assertEq(p0.ts, 0, "should not have written to 0th index"); + + GlobalPoint memory p1 = curve.pointHistory(1); + assertEq(p1.ts, prevInterval, "should have written to 1st index"); + + assertEq( + p1.coefficients[0] / 1e18, + 1_000_000e18, + "should have written bias to 1st index" + ); + assertEq( + p1.coefficients[1] / 1e18, + baseSlope, + "should have written slope to 1st index" + ); + + GlobalPoint memory p2 = curve.pointHistory(2); + + // expect the coeff1 is the same and the bias is interpolated over 3 days + assertEq(p2.ts, block.timestamp, "point 2 should be at current time"); + assertEq(p2.coefficients[1] / 1e18, baseSlope, "slope should be the same"); + assertEq(uint(p2.coefficients[0]) / 1e18, expTotalVP, "bias should be interpolated"); + + // scheduled increases at the end of the week + assertEq(curve.scheduledCurveChanges(nextInterval)[0], 500_000e18); + assertEq(curve.scheduledCurveChanges(nextInterval)[1], changeInSlope); + + // scheduled decreases at the maxTime + assertEq(curve.scheduledCurveChanges(nextInterval + curve.maxTime())[0], 0); + assertEq( + curve.scheduledCurveChanges(nextInterval + curve.maxTime())[1], + -changeInSlope + ); + } + + // user changes their mind to 1m + curve.unsafeCheckpoint( + matt, + LockedBalance({start: nextInterval, amount: 500_000e18}), + LockedBalance({start: nextInterval, amount: 1_000_000e18}) + ); + + { + // assertions: + int baseSlope = curve.getCoefficients(1_000_000e18)[1]; + int changeInSlope = curve.getCoefficients(1_000_000e18)[1]; + uint expTotalVP = curve.getBias(3 days, 1_000_000e18); + + // current index is 2 + assertEq(curve.latestPointIndex(), 2, "index should be 2"); + + // the first point has been applied at the prev interval + GlobalPoint memory p0 = curve.pointHistory(0); + assertEq(p0.ts, 0, "should not have written to 0th index"); + + GlobalPoint memory p1 = curve.pointHistory(1); + assertEq(p1.ts, prevInterval, "should have written to 1st index"); + + assertEq( + p1.coefficients[0] / 1e18, + 1_000_000e18, + "should have written bias to 1st index" + ); + assertEq( + p1.coefficients[1] / 1e18, + baseSlope, + "should have written slope to 1st index" + ); + + GlobalPoint memory p2 = curve.pointHistory(2); + + // expect the coeff1 is the same and the bias is interpolated over 3 days + assertEq(p2.ts, block.timestamp, "point 2 should be at current time"); + assertEq(p2.coefficients[1] / 1e18, baseSlope, "slope should be the same"); + assertEq(uint(p2.coefficients[0]) / 1e18, expTotalVP, "bias should be interpolated"); + + // scheduled increases at the end of the week + assertEq(curve.scheduledCurveChanges(nextInterval)[0], 1_000_000e18); + assertEq(curve.scheduledCurveChanges(nextInterval)[1], changeInSlope); + + // scheduled decreases at the maxTime + assertEq(curve.scheduledCurveChanges(nextInterval + curve.maxTime())[0], 0); + assertEq( + curve.scheduledCurveChanges(nextInterval + curve.maxTime())[1], + -changeInSlope + ); + } + + // user 3 deposits 1m ether exactly on week 5 + vm.warp(5 weeks); + + prevInterval = nextInterval; + nextInterval = uint48(block.timestamp); + curve.unsafeCheckpoint( + phil, + LockedBalance(0, 0), + LockedBalance({start: nextInterval, amount: 1_000_000e18}) + ); + { + // assertions: + assertEq(curve.latestPointIndex(), 5, "index should be 5"); + + // point 3 is when the prior deposit gets applied + GlobalPoint memory p3 = curve.pointHistory(3); + assertEq(p3.ts, prevInterval, "point 3 should be recorded at the prevInterval"); + // the slope should be the result of 2m ether now activated + int slopeP3 = curve.getCoefficients(2_000_000e18)[1]; + assertEq(p3.coefficients[1] / 1e18, slopeP3, "slope should be the result of 2m ether"); + + // the bias should be: 1m with 1 week of accrued voting power PLUS the extra m deposited + // but with no accrued voting power on the extra m + uint expBiasP3 = curve.getBias(1 weeks, 1_000_000e18) + 1_000_000e18; + + // sanity check this should be ~2.009 (according to python) + assertLt(expBiasP3, 2_010_000e18, "bias should be under 2.01m"); + assertGt(expBiasP3, 2_009_000e18, "bias should be over 2.009m"); + + assertEq( + uint(p3.coefficients[0]) / 1e18, + expBiasP3, + "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 5 should be written + GlobalPoint memory p5 = curve.pointHistory(5); + + assertEq(p5.ts, block.timestamp, "point 5 should be at current time"); + + // the slope should be 3x 1m + int slope = curve.getCoefficients(3_000_000e18)[1]; + assertEq(p5.coefficients[1] / 1e18, slope, "slope should be the result of 3m ether"); + + // the bias should be 1m for 1 week (w2 -> w3) + // 2m for 2 weeks (w3 -> w5) + // // + 1m new + // uint expBiasP5 = (curve.getBias(1 weeks, 1_000_000e18) - 1_000_000e18) + // marginal contribution of first deposit + // // marginal contribution of first + second deposit + // // not sure this is correct + // (curve.getBias(2 weeks, 2_000_000e18) - 2_000_000e18) + + // // base qty in the contracts + // 3_000_000e18; + // + // assertEq(uint(p5.coefficients[0]) / 1e18, expBiasP5); + // + // we can calculate this another way: + // evaluate the curve over the first week + // then take the end state of that curve as the deposit for the next 2 weeks + // then add that to the 1m + uint w2To3 = curve.getBias(1 weeks, 1_000_000e18); + console.log("w2To3", w2To3 + 1_000_000e18); + uint w3To5 = curve.getBias(2 weeks, w2To3 + 1_000_000e18); + console.log("w3To5", w3To5 + 1_000_000e18); + uint w5 = w3To5 + 1_000_000e18; + assertEq(uint(p5.coefficients[0]) / 1e18, w5); + } + } +} diff --git a/test/python/global_state.py b/test/python/global_state.py new file mode 100644 index 0000000..1559e11 --- /dev/null +++ b/test/python/global_state.py @@ -0,0 +1,79 @@ +# time utils +HOUR = 60 * 60 +DAY = 24 * HOUR +WEEK = 7 * DAY + +# Variables +AMOUNT = 1000 +SECOND_AMOUNT = 2000 +PERIOD_LENGTH = 2 * WEEK # example period length in seconds (1 week) +WARMUP_PERIOD = 3 * DAY # warmup period in days +MAX_PERIODS = 52 # maximum periods + +QUADRATIC_COEFFICIENT = 0 +LINEAR_COEFFICIENT = 1 / 52 +CONSTANT = 1 + +# Scale amount +amount_scaled = AMOUNT * 1e18 + + +# Function to evaluate y +def evaluate_y(secondsElapsed, PeriodLength, amount_scaled): + x = secondsElapsed / PeriodLength + y = amount_scaled * ( + QUADRATIC_COEFFICIENT * (x**2) + LINEAR_COEFFICIENT * x + CONSTANT + ) + return y + + +def evaluate_y_v2(secondsElapsed, PeriodLength, amount_scaled): + x = secondsElapsed / PeriodLength + y = amount_scaled * ( + QUADRATIC_COEFFICIENT * (x * x) + LINEAR_COEFFICIENT * x + CONSTANT + ) + return y + + +# Time points to evaluate, using tuples with optional labels +time_points = [ + ("0", 0), + ("1 minute", 60), + ("1 hour", 60 * 60), + ("1 day", 60 * 60 * 24), + (f"WARMUP_PERIOD ({WARMUP_PERIOD//DAY} days)", WARMUP_PERIOD), + (f"WARMUP_PERIOD + 1s", (WARMUP_PERIOD) + 1), + ("1 week", 60 * 60 * 24 * 7), + (f"1 period ({PERIOD_LENGTH // (WEEK)} weeks)", PERIOD_LENGTH), + (f"10 periods (10 * PERIOD)", 10 * PERIOD_LENGTH), + (f"50% periods (26 * PERIOD)", 26 * PERIOD_LENGTH), + (f"35 periods (35 * PERIOD)", 35 * PERIOD_LENGTH), + (f"PERIOD_END (26 * PERIOD)", MAX_PERIODS * PERIOD_LENGTH), +] +# +# # Evaluate and print results +# for label, t in time_points: +# y_value = evaluate_y(t, PERIOD_LENGTH, amount_scaled) +# # Avoid scientific notation by formatting with commas and align values vertically +# print(f"{label:<30} Voting Power: {y_value:>20.0f}") +# + + +# Evaluate 1 week of 1000 + 1 week of an additional 1000 +def eval_multiple(): + amount_scaled = AMOUNT * 1e18 + amount_scaled_2 = SECOND_AMOUNT * 1e18 + end_of_week_2 = evaluate_y(1 * WEEK, PERIOD_LENGTH, amount_scaled) + # then add the second amount + end_of_week_2_plus_new_deposit = end_of_week_2 + AMOUNT * 1e18 + y_value_2 = evaluate_y(2 * WEEK, PERIOD_LENGTH, end_of_week_2_plus_new_deposit) + + # print all the values above nicely + print(f"Start of week 2: {amount_scaled}") + print(f"End of week 2: {end_of_week_2}") + print(f"start of week 3 + new deposit: {end_of_week_2_plus_new_deposit}") + print(f"start of week 5 : {y_value_2}") + print(f"Start of week 5 + deposit: {y_value_2 + amount_scaled}") + + +eval_multiple()