MEDIUM severity: Voting power returns 0 during oracle failures causing unfair voting outcomes. **Description:** The `PricedTokensChainlinkVPCalc::stakeToVotingPowerAt` silently returns zero voting power when Chainlink oracles fail or return stale data. Operators lose all voting power without notification creating unintended voting outcomes.

The core issue lies in the...

Vulnerable code:
// ChainlinkPriceFeed.sol
function getPriceAt(address aggregator, uint48 timestamp, bool invert, uint48 stalenessDuration)
    public view returns (uint256) {
    (bool success, RoundData memory roundData) = getPriceDataAt(aggregator, timestamp, invert, stalenessDuration);
    return success ? roundData.answer : 0; // @audit silently returns 0
}

// PricedTokensChainlinkVPCalc.sol
function stakeToVotingPowerAt(address vault, uint256 stake, bytes memory extraData, uint48 timestamp)
	public view virtual override returns (uint256) {
	return super.stakeToVotingPowerAt(vault, stake, extraData, timestamp) * getTokenPriceAt(_getCollateral(vault), timestamp); // @audit returns 0 when oracle is stale
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import "forge-std/Test.sol";

import {VotingPowerProvider} from "../../../../../src/contracts/modules/voting-power/VotingPowerProvider.sol";
import {VotingPowerProviderLogic} from
    "../../../../../src/contracts/modules/voting-power/logic/VotingPowerProviderLogic.sol";
import {MultiToken} from "../../../../../src/contracts/modules/voting-power/extensions/MultiToken.sol";
import {IVotingPowerProvider} from "../../../../../src/interfaces/modules/voting-power/IVotingPowerProvider.sol";
import {INetworkManager} from "../../../../../src/interfaces/modules/base/INetworkManager.sol";
import {IOzEIP712} from "../../../../../src/interfaces/modules/base/IOzEIP712.sol";
import {NoPermissionManager} from "../../../../../test/mocks/NoPermissionManager.sol";
import {PricedTokensChainlinkVPCalc} from
    "../../../../../src/contracts/modules/voting-power/common/voting-power-calc/PricedTokensChainlinkVPCalc.sol";
import {OperatorVaults} from "../../../../../src/contracts/modules/voting-power/extensions/OperatorVaults.sol";
import {ChainlinkPriceFeed} from
    "../../../../../src/contracts/modules/voting-power/common/voting-power-calc/libraries/ChainlinkPriceFeed.sol";

import {BN254} from "../../../../../src/contracts/libraries/utils/BN254.sol";
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
import "../../../../InitSetup.sol";

// Mock Chainlink Aggregator that simulates stale data conditions
contract MockChainlinkAggregator is AggregatorV3Interface {
    uint80 private _roundId;
    int256 private _answer;
    uint256 private _startedAt;
    uint256 private _updatedAt;
    uint80 private _answeredInRound;
    uint8 private _decimals;

    bool public isStale;
    bool public shouldRevert;

    constructor(uint8 decimals_) {
        _decimals = decimals_;
        _roundId = 1;
        _answer = 2000e8; // $2000 price
        _startedAt = block.timestamp;
        _updatedAt = block.timestamp;
        _answeredInRound = 1;
        isStale = false;
        shouldRevert = false;
    }

    function decimals() external view override returns (uint8) {
        return _decimals;
    }

    function description() external pure override returns (string memory) {
        return "Mock Chainlink Aggregator";
    }

    function version() external pure override returns (uint256) {
        return 1;
    }

    function getRoundData(uint80 roundId) external view override
        returns (uint80, int256, uint256, uint256, uint80) {
        if (shouldRevert) {
            revert("Oracle failure");
        }
        return (_roundId, _answer, _startedAt, _updatedAt, _answeredInRound);
    }

    function latestRoundData() external view override
        returns (uint80, int256, uint256, uint256, uint80) {
        if (shouldRevert) {
            revert("Oracle failure");
        }

        uint256 updatedAt = _updatedAt;
        if (isStale) {
            // Make data appear stale by setting updatedAt to a very old timestamp
            updatedAt = block.timestamp - 1 days;
        }

        return (_roundId, _answer, _startedAt, updatedAt, _answeredInRound);
    }

    // Test helpers to simulate oracle conditions
    function setStale(bool _isStale) external {
        isStale = _isStale;
    }

    function setShouldRevert(bool _shouldRevert) external {
        shouldRevert = _shouldRevert;
    }

    function setPrice(int256 newPrice) external {
        _answer = newPrice;
        _roundId++;
        _updatedAt = block.timestamp;
        _answeredInRound = _roundId;
    }

    function setUpdatedAt(uint256 newUpdatedAt) external {
        _updatedAt = newUpdatedAt;
    }
}

// Test voting power provider that uses PricedTokensChainlinkVPCalc
contract TestVotingPowerProvider is VotingPowerProvider, PricedTokensChainlinkVPCalc, NoPermissionManager {
    constructor(address operatorRegistry, address vaultFactory) VotingPowerProvider(operatorRegistry, vaultFactory) {}

    function initialize(
        IVotingPowerProvider.VotingPowerProviderInitParams memory votingPowerProviderInit
    ) external initializer {
        __VotingPowerProvider_init(votingPowerProviderInit);
    }

    function registerOperator(address operator) external {
        _registerOperator(operator);
    }

    function unregisterOperator(address operator) external {
        _unregisterOperator(operator);
    }

    function setSlashingWindow(uint48 sw) external {
        _setSlashingWindow(sw);
    }

    function registerToken(address token) external {
        _registerToken(token);
    }

    function unregisterToken(address token) external {
        _unregisterToken(token);
    }

    function registerSharedVault(address vault) external {
        _registerSharedVault(vault);
    }

    function unregisterSharedVault(address vault) external {
        _unregisterSharedVault(vault);
    }

    function registerOperatorVault(address operator, address vault) external {
        _registerOperatorVault(operator, vault);
    }

    function unregisterOperatorVault(address operator, address vault) external {
        _unregisterOperatorVault(operator, vault);
    }

    // Expose the setTokenHops function for testing
    function setTokenHops(
        address token,
        address[2] memory aggregators,
        bool[2] memory inverts,
        uint48[2] memory stalenessDurations
    ) public override {
        _setTokenHops(token, aggregators, inverts, stalenessDurations);
    }
}

contract PricedTokensChainlinkVPCalcTest is InitSetupTest {
    TestVotingPowerProvider private votingPowerProvider;
    MockChainlinkAggregator private mockAggregator;
    address private vault;

    address operator1 = address(0xAAA1);
    address operator2 = address(0xAAA2);

    uint256 private constant STAKE_AMOUNT = 1000e18; // 1000 tokens
    uint48 private constant STALENESS_DURATION = 1 hours;

     function setUp() public override {
        InitSetupTest.setUp();

        address token = initSetupParams.masterChain.tokens[0];
        vault = initSetupParams.masterChain.vaults[0];

        // Deploy mock aggregator (8 decimals like most Chainlink feeds)
        mockAggregator = new MockChainlinkAggregator(8);

        // Create a mock vault for testing
        vault = makeAddr("vault");

        // Deploy voting power provider
        votingPowerProvider = new TestVotingPowerProvider(
            address(symbioticCore.operatorRegistry),
            address(symbioticCore.vaultFactory)
        );

          // Initialize the voting power provider
        INetworkManager.NetworkManagerInitParams memory netInit =
            INetworkManager.NetworkManagerInitParams({network: vars.network.addr, subnetworkID: IDENTIFIER});
        IVotingPowerProvider.VotingPowerProviderInitParams memory initParams = IVotingPowerProvider
            .VotingPowerProviderInitParams({
                networkManagerInitParams: netInit,
                ozEip712InitParams: IOzEIP712.OzEIP712InitParams({name: "MyVotingPowerProvider", version: "1"}),
                slashingWindow: 100,
                token: token
        });

        votingPowerProvider.initialize(initParams);
        _networkSetMiddleware_SymbioticCore(vars.network.addr, address(votingPowerProvider));


        _registerOperator_SymbioticCore(symbioticCore, operator1);
        _registerOperator_SymbioticCore(symbioticCore, operator2);

         vm.startPrank(operator1);
        votingPowerProvider.registerOperator(operator1);
        vm.stopPrank();

        vm.startPrank(operator2);
        votingPowerProvider.registerOperator(operator2);
        vm.stopPrank();

          // Create a proper vault for operator1 with correct parameters
        vault = _getVault_SymbioticCore(
            VaultParams({
                owner: operator1,
                collateral: token,
                burner: 0x000000000000000000000000000000000000dEaD,
                epochDuration: votingPowerProvider.getSlashingWindow() * 2,
                whitelistedDepositors: new address[](0),
                depositLimit: 0,
                delegatorIndex: 2, // OPERATOR_SPECIFIC delegator
                hook: address(0),
                network: address(0),
                withSlasher: true,
                slasherIndex: 0,
                vetoDuration: 1
            })
        );

         _operatorOptIn_SymbioticCore(operator1, vault);

          _networkSetMaxNetworkLimit_SymbioticCore(
            votingPowerProvider.NETWORK(),
            vault,
            votingPowerProvider.SUBNETWORK_IDENTIFIER(),
            type(uint256).max
        );

        _curatorSetNetworkLimit_SymbioticCore(
            operator1, vault, votingPowerProvider.SUBNETWORK(), type(uint256).max
        );

        _deal_Symbiotic(token, getStaker(0).addr, STAKE_AMOUNT *10, true); // some big number
        _stakerDeposit_SymbioticCore(getStaker(0).addr, vault, STAKE_AMOUNT);

        vm.startPrank(vars.network.addr);
        votingPowerProvider.registerOperatorVault(operator1, vault);
        vm.stopPrank();

          // Set up token price hops with our mock aggregator
        address[2] memory aggregators = [address(mockAggregator), address(0)];
        bool[2] memory inverts = [false, false];
        uint48[2] memory stalenessDurations = [STALENESS_DURATION, 0];

        votingPowerProvider.setTokenHops(token, aggregators, inverts, stalenessDurations);

     }

     function test_NormalPriceReturnsCorrectVotingPower() public {
        // Set oracle to return price = 2000 (2000e8 in 8 decimals)
        mockAggregator.setPrice(2000e8);

        //Calculate voting power
        uint256 votingPower = votingPowerProvider.stakeToVotingPower(vault, STAKE_AMOUNT, "");

        assertEq(votingPower, (STAKE_AMOUNT * 10**(24-18)) * 2000*10**8*10**(18-8), "Voting power should be stake multiplied by price"); // normalization scaling to 10^24

        // 2000*10**8*10**(18-8) -> chainlink price is scaled to 18 decimals
        // voting power is normalized to 24 decimals
     }

    function test_StaleOracleReturnsZeroVotingPower() public {
        // Set oracle to stale
        mockAggregator.setStale(true);

        // Calculate voting power
        uint256 votingPower = votingPowerProvider.stakeToVotingPower(vault, STAKE_AMOUNT, "");

        // Voting power is 0 due to stale data - silently distorts the voting outcomes
        assertEq(votingPower, 0, "Voting power should be zero when oracle data is stale");
    }

}