Skip to content

Commit

Permalink
close but some logic to check
Browse files Browse the repository at this point in the history
  • Loading branch information
jordaniza committed Oct 20, 2024
1 parent bf6e4b2 commit fd92a32
Show file tree
Hide file tree
Showing 5 changed files with 706 additions and 35 deletions.
99 changes: 64 additions & 35 deletions src/escrow/increasing/LinearIncreasingEscrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -265,6 +275,7 @@ contract LinearIncreasingEscrow is
}
return lower;
}

function votingPowerAt(uint256 _tokenId, uint256 _t) external view returns (uint256) {
uint256 interval = _getPastTokenPointInterval(_tokenId, _t);

Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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] =
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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);
}

Expand All @@ -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];

Expand Down
169 changes: 169 additions & 0 deletions test/escrow/curve/linear/LinearApplyTokenToGlobal.t.sol
Original file line number Diff line number Diff line change
@@ -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
}
32 changes: 32 additions & 0 deletions test/escrow/curve/linear/LinearBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit fd92a32

Please sign in to comment.