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

MultiInvoker._latest reads oracle.latest price without validity check, returning 0 when latest oracle version is invalid, zeroing liquidation fees and mis-triggering orders.

- Payout: Contest reward
- Severity: medium
- Program: Perennial
- Credited: panprog
- Disclosed: 2023-10-26
- Write-up quality: 7/10
- Original: https://github.com/sherlock-audit/2023-10-perennial-judging/issues/31
- Library page: https://bugbountylibrary.com/r/sherlock%3A2023-10-perennial%3AM-8
- Tags: domain:web3 impact:funds-loss kind:contest platform:sherlock sev:medium tech:evm tech:solidity technique:source-review vuln:accounting-error vuln:business-logic vuln:oracle-manipulation

**Target:** MultiInvoker extension contract / oracle latest price handling
**Root cause:** MultiInvoker._latest uses oracle.latest().price directly without checking version validity, unlike Market which falls back to global.latestPrice when the latest oracle version is invalid (price 0).
**Steps:**
1. Observe KeeperOracle._commitRequested: commits after timeout increment latestIndex without setting _prices, leaving price 0 and version invalid.
2. Read MultiInvoker._latest: `latestPrice = market.oracle().latest().price;` with no validity check.
3. Call LIQUIDATE action via MultiInvoker after an invalid oracle version; _liquidationFee multiplies order size by latestPrice=0, yielding 0 fee.
4. Call canExecuteOrder for a trigger order; comparison uses latestPrice=0, so `<=` orders trigger incorrectly.
**How found:** Code-reading comparison: noticed 2.1 changed oracle version handling so latest version can be invalid (price 0); Market handles this by falling back to global.latestPrice, but MultiInvoker._latest does not, so traced the zero price into liquidationFee and canExecuteOrder.
**Exploitation:** Liquidator invokes MultiInvoker LIQUIDATE after an oracle version committed past timeout (invalid, price 0); liquidationFee computes to 0 so liquidator receives no fee. Separately, trigger orders with <= comparison execute at real price far from trigger because canExecuteOrder compares against 0.
**Insight:** When a protocol changes oracle semantics (every requested version committed, valid or invalid), audit every consumer of oracle.latest() for the new invalid-version case; helper contracts often lag behind the core's fallback logic.
