VotingEscrow.sol::checkpoint is completely broken because of incorrect update of Epoch.
Vulnerability Details
Note - I have selected the closest impact. Please adjust the impact/severity as you may seem proper. Although it is clearly apparent but still I will present more POC/ evidence as to why this is a critical issue. As the time of boost is about to end, I am submitting this just confirming the basic vulnerability
Note for Immunefi Triage - Do not close this issue as the boost period is about to end. This is a legitimate issue, please read the whole report carefully, if any doubt ask in the comments. Thanks!
In the VotingEscrow.sol::_checkpoint, the epoch is updated after every 1 week increment and not 2 week increment. So , the epochs are not updated correctly.
function_checkpoint(uint256_tokenId,LockedBalancememory oldLocked,LockedBalancememory newLocked) internal { Point memory oldPoint; Point memory newPoint;int256 oldDslope =0;int256 newDslope =0;uint256 _epoch = epoch;if (oldLocked.maxLockEnabled) oldLocked.end = ((block.timestamp + MAXTIME) / WEEK) * WEEK;if (newLocked.maxLockEnabled) newLocked.end = ((block.timestamp + MAXTIME) / WEEK) * WEEK;if (_tokenId !=0) { oldPoint =_calculatePoint(oldLocked, block.timestamp); newPoint =_calculatePoint(newLocked, block.timestamp);// Read values of scheduled changes in the slope// oldLocked.end can be in the past and in the future// newLocked.end can ONLY by in the FUTURE unless everything expired: then zeros oldDslope = slopeChanges[oldLocked.end];if (newLocked.end !=0) {if (newLocked.end == oldLocked.end) { newDslope = oldDslope; } else { newDslope = slopeChanges[newLocked.end]; } } } Point memory lastPoint =Point({ bias:0, slope:0, ts: block.timestamp, blk: block.number });if (_epoch >0) { lastPoint = pointHistory[_epoch]; }uint256 lastCheckpoint = lastPoint.ts;// initialLastPoint is used for extrapolation to calculate block number// (approximately, for *At methods) and save them// as we cannot figure that out exactly from inside the contract Point memory initialLastPoint = lastPoint;uint256 blockSlope =0; // dblock/dtif (block.timestamp > lastPoint.ts) { blockSlope = (MULTIPLIER * (block.number - lastPoint.blk)) / (block.timestamp - lastPoint.ts); }// If last point is already recorded in this block, slope=0// We know the block in such case// Go over weeks to fill history and calculate what the current point is {uint256 _time = (lastCheckpoint / WEEK) * WEEK;for (uint256 i =0; i <255; ++i) {// Hopefully it won't happen that this won't get used in 5 years!// If it does, users will be able to withdraw but vote weight will be broken @> _time += WEEK; // time increase by 1 weekint256 dSlope =0;if (_time > block.timestamp) { _time = block.timestamp; } else { dSlope = slopeChanges[_time]; }int256 biasCalculation = lastPoint.slope * (int256(_time - lastCheckpoint));// Make sure we still subtract from bias if value is negative biasCalculation >=0? lastPoint.bias -= biasCalculation : lastPoint.bias += biasCalculation; lastPoint.slope += dSlope;if (lastPoint.bias <0) {// This can happen lastPoint.bias =0; }if (lastPoint.slope <0) {// This cannot happen - just in case lastPoint.slope =0; } lastCheckpoint = _time; lastPoint.ts = _time; lastPoint.blk = initialLastPoint.blk + (blockSlope * (_time - initialLastPoint.ts)) / MULTIPLIER;@> _epoch +=1; // epoch increases by 1 (i.e 2 weeks)if (_time == block.timestamp) { lastPoint.blk = block.number;break; } else { pointHistory[_epoch] = lastPoint; } } } epoch = _epoch;// Now pointHistory is filled until t=nowif (_tokenId !=0) {// If last point was in this block, the slope change has been applied already// But in such case we have 0 slope(s) lastPoint.slope += (newPoint.slope - oldPoint.slope); lastPoint.bias += (newPoint.bias - oldPoint.bias);