LOW severity: Budget is always calculated using the full `epochDuration` regardless of the actual timing of distributions. **Description:** The `DistributionManager` calculates budget allocations based on the full epoch duration and transfers tokens to reward controllers at the end of each epoch. When reward speeds are updated via calls to `setRewardSpeedInternal()`, the budget calculation assumes rewards will be...

Vulnerable code:
// BenqiCoreModule::calculateMarketBudgetUsed
function calculateMarketBudgetUsed(
    MarketVotes memory _market,
    uint256 _supplySpeed,
    uint256 _borrowSpeed,
    uint256 _epochDuration
) public pure returns (uint256) {
    uint256 budget = 0;

    // Only count budget for registered gauges
    if (_market.hasRegisteredSupplyGauge) {
@>      budget += _supplySpeed * _epochDuration; // @audit - uses full epoch duration
    }

    if (_market.hasRegisteredBorrowGauge) {
@>      budget += _borrowSpeed * _epochDuration; // @audit - uses full epoch duration
    }

    return budget;
}

// DistributionManager::_buildActions
uint256 amountWithBuffer = _applyBuffer(budgetUsed);
if (amountWithBuffer != 0) {
    actions[actionCount++] = Action({
        to: address(rewardToken),
        value: 0,
        data: abi.encodeWithSelector(
            IERC20.transfer.selector,
            controller,
@>          amountWithBuffer // @audit - full epoch budget transferred
        )
    });
}

function setRewardSpeedInternal(uint8 rewardType, QiToken qiToken, uint newSupplyRewardSpeed, uint newBorrowRewardSpeed) internal {
        uint currentSupplyRewardSpeed = supplyRewardSpeeds[rewardType][address(qiToken)];
        uint currentBorrowRewardSpeed = borrowRewardSpeeds[rewardType][address(qiToken)];

        if (currentSupplyRewardSpeed != 0) {
@>          updateRewardSupplyIndex(rewardType, address(qiToken));
        } else if (newSupplyRewardSpeed != 0) {
            Market storage market = markets[address(qiToken)];
            require(market.isListed, "Market is not listed");

            ...
         }

        ...

        if (currentSupplyRewardSpeed != newSupplyRewardSpeed) {
@>          supplyRewardSpeeds[rewardType][address(qiToken)] = newSupplyRewardSpeed;
            emit SupplyRewardSpeedUpdated(rewardType, qiToken, newSupplyRewardSpeed);
        }

       ...
    }

    // lib/BENQI-Smart-Contracts/lending/Comptroller.sol:1112-1130
    function updateRewardSupplyIndex(uint8 rewardType, address qiToken) internal {
        require(rewardType <= 1, "rewardType is invalid");
        RewardMarketState storage supplyState = rewardSupplyState[rewardType][qiToken];
        uint supplySpeed = supplyRewardSpeeds[rewardType][qiToken];
        uint blockTimestamp = getBlockTimestamp();
@>      uint deltaTimestamps = sub_(blockTimestamp, uint(supplyState.timestamp)); // @audit - accrues from last update
        if (deltaTimestamps > 0 && supplySpeed > 0) {
            uint supplyTokens = QiToken(qiToken).totalSupply();
@>          uint qiAccrued = mul_(deltaTimestamps, supplySpeed); // @audit - deltaTimestamps < epochDuration
            Double memory ratio = supplyTokens > 0 ? fraction(qiAccrued, supplyTokens) : Double({mantissa: 0});
            Double memory index = add_(Double({mantissa: supplyState.index}), ratio);
            rewardSupplyState[rewardType][qiToken] = RewardMarketState({
                index: safe224(index.mantissa, "new index exceeds 224 bits"),
                timestamp: safe32(blockTimestamp, "block timestamp exceeds 32 bits")
            });
        } else if (deltaTimestamps > 0) {
            supplyState.timestamp = safe32(blockTimestamp, "block timestamp exceeds 32 bits");
        }
    }

function test_wastedBudgetPoC() public {
    // Setup two gauges
    vm.startPrank(ADMIN);
    address gauge1 = gaugeRegistrar.registerGauge(
        QI_TOKEN_1, IGaugeRegistrar.Incentive.Supply, address(coreComptroller), "Gauge 1"
    );
    address gauge2 = gaugeRegistrar.registerGauge(
        QI_TOKEN_2, IGaugeRegistrar.Incentive.Supply, address(coreComptroller), "Gauge 2"
    );
    vm.stopPrank();

    coreComptroller.setMarketListed(QI_TOKEN_1, true);
    coreComptroller.setMarketListed(QI_TOKEN_2, true);

    // Epoch 0: gauge1 gets 80% of votes, gauge2 gets 20%
    addressGaugeVoter.setEpochGaugeVotes(0, gauge1, 800 ether);
    addressGaugeVoter.setEpochGaugeVotes(0, gauge2, 200 ether);

    // Epoch 0: Voting ends Day 7, distribute() called Day 10 (3 day delay)
    uint256 distributeDay10 = 1 weeks + 3 days;
    vm.warp(distributeDay10);

    vm.prank(DISTRIBUTOR);
    distributionManager.distribute();

    uint256 gauge1FirstSpeed = coreComptroller.supplyRewardSpeeds(0, QI_TOKEN_1);

    // Epoch 1: Votes SHIFT - gauge1 now only gets 30%, gauge2 gets 70%
    addressGaugeVoter.setEpochGaugeVotes(0, gauge1, 300 ether);
    addressGaugeVoter.setEpochGaugeVotes(0, gauge2, 700 ether);

    uint256 distributeDay22 = 2 weeks + 1 weeks + 1 days;
    vm.warp(distributeDay22);

    vm.prank(DISTRIBUTOR);
    distributionManager.distribute(); // Overwrites speeds from Epoch 0

    // Check: gauge1 speed REDUCED (now only 30% of budget)
    uint256 gauge1NewSpeed = coreComptroller.supplyRewardSpeeds(0, QI_TOKEN_1);

    assertGt(gauge1FirstSpeed, gauge1NewSpeed, "Speed REDUCED when votes decreased and budget wasted!");
}