Vulnerability: `MultiInvoker._latest` will return `latestPrice = 0` when latest oracle version is invalid causing liquidation to send 0 fee to liquidator or incorrect order execution
Severity: medium

Description:
There was a slight change of oracle versions handling in 2.1: now each requested oracle version must be commited, either as valid or invalid. This means that now the latest version can be invalid (price = 0). This is handled correctly in `Market`, which only uses timestamp from the latest oracle version, but the price comes either from latest version (if valid) or `global.latestPrice` (if invalid).\\nHowever, `MultiInvoker` always uses price from `oracle.latest` without verifying if it's valid, meaning it will return `latestPrice = 0` if the latest oracle version is invalid. This is returned from the `_latest` function.\\nSuch latest price = 0 leads to 2 main problems:\\nLiquidations orders in MultiInvoker will send 0 liquidation fee to liquidator (will liquidate for free)\\nSome TriggerOrders will trigger incorrectly (canExecuteOrder will return true when the real price didn't reach the trigger price, or false even if the real prices reached the trigger price)\\n`MultiInvoker._latest` has the following code for latest price assignment:\\n```\\nOracleVersion memory latestOracleVersion = market.oracle().latest();\\nlatestPrice = latestOracleVersion.price;\\nIPayoffProvider payoff = market.payoff();\\nif (address(payoff) != address(0)) latestPrice = payoff.payoff(latestPrice);\\n```\\n\\nThis `latestPrice` is what's returned from the `_latest`, it isn't changed anywhere else. Notice that there is no check for latest oracle version validity.\\nAnd this is the code for KeeperOracle._commitRequested:\\n```\\nfunction _commitRequested(OracleVersion memory version) private returns (bool) {\\n    if (block.timestamp <= (next() + timeout)) {\\n        if (!version.valid) revert KeeperOracleInvalidPriceError();\\n        _prices[version.timestamp] = version.price;\\n    }\\n    _global.latestIndex++;\\n    return true;\\n}\\n```\\n\\nNotice that commits made outside the timeout window simply increase `_global.latestIndex` without assigning `_prices`, meaning it remains 0 (invalid). This means that latest oracle version will return price=0 and will be invalid if commited after the timeout from request time has passed.\\nPrice returned by `_latest` is used when calculating liquidationFee:\\n```\\nfunction _liquidationFee(IMarket market, address account) internal view returns (Position memory, UFixed6, UFixed6) {\\n    // load information about liquidation\\n    RiskParameter memory riskParameter = market.riskParameter();\\n@@@ (Position memory latestPosition, Fixed6 latestPrice, UFixed6 closableAmount) = _latest(market, account);\\n\\n    // create placeholder order for liquidation fee calculation (fee is charged the same on all sides)\\n    Order memory placeholderOrder;\\n    placeholderOrder.maker = Fixed6Lib.from(closableAmount);\\n\\n    return (\\n        latestPosition,\\n        placeholderOrder\\n@@@         .liquidationFee(OracleVersion(latestPosition.timestamp, latestPrice, true), riskParameter)\\n            .min(UFixed6Lib.from(market.token().balanceOf(address(market)))),\\n        closableAmount\\n    );\\n}\\n```\\n\\n`liquidationFee` calculation in order multiplies order size by `latestPrice`, meaning it will be 0 when price = 0. This liquidation fee is then used in `market.update` for liquidation fee to receive by liquidator:\\n```\\n    function _liquidate(IMarket market, address account, bool revertOnFailure) internal isMarketInstance(market) {\\n@@@     (Position memory latestPosition, UFixed6 liquidationFee, UFixed6 closable) = _liquidationFee(market, account);\\n        Position memory currentPosition = market.pendingPositions(account, market.locals(account).currentId);\\n        currentPosition.adjust(latestPosition);\\n\\n        try market.update(\\n                account,\\n                currentPosition.maker.isZero() ? UFixed6Lib.ZERO : currentPosition.maker.sub(closable),\\n                currentPosition.long.isZero() ? UFixed6Lib.ZERO : currentPosition.long.sub(closable),\\n                currentPosition.short.isZero() ? UFixed6Lib.ZERO : currentPosition.short.sub(closable),\\n@@@             Fixed6Lib.from(-1, liquidationFee),\\n                true\\n```\\n\\nThis means liquidator will receive 0 fee for the liquidation.\\nIt is also used in canExecuteOrder:\\n```\\n    function _executeOrder(address account, IMarket market, uint256 nonce) internal {\\n        if (!canExecuteOrder(account, market, nonce)) revert MultiInvokerCantExecuteError();\\n// rest of code\\n    function canExecuteOrder(address account, IMarket market, uint256 nonce) public view returns (bool) {\\n        TriggerOrder memory order = orders(account, market, nonce);\\n        if (order.fee.isZero()) return false;\\n@@@     (, Fixed6 latestPrice, ) = _latest(market, account);\\n@@@     return order.fillable(latestPrice);\\n    }\\n```\\n\\nMeaning `canExecuteOrder` will do comparision with price = 0 instead of real latest price. For example: limit buy order to buy when price <= 1000 (when current price = 1100) will trigger and execute buy at the price = 1100 instead of 1000 or lower.

Impact:
liquidation done after invalid oracle version via `MultiInvoker` `LIQUIDATE` action will charge and send 0 liquidation fee from the liquidating account, thus liquidator loses these funds.\\nsome orders with comparison of type -1 (<= price) will incorrectly trigger and will be executed when price is far from reaching the trigger price. This loses user funds due to unexpected execution price of the pending order.

Recommended mitigation:
`_latest` should replicate the process for the latest price from `Market` instead of using price from the oracle's latest version:\\nif the latest oracle version is valid, then use its price\\nif the latest oracle version is invalid, then iterate all global pending positions backwards and use price of any valid oracle version at the position.\\nif all pending positions are at invalid oracles, use market's `global.latestPrice`