# Malicious keepers can manipulate the price when executing an order

Permissionless keepers can pass empty priceUpdateData to executeOrder, skipping Pyth refresh and settling orders at stale on-chain prices favoring LPs or traders.

- Payout: Contest reward
- Severity: high
- Program: Flatmoney
- Credited: LTDingZhen
- Disclosed: 2023-12-11
- Write-up quality: 7/10
- Original: https://github.com/sherlock-audit/2023-12-flatmoney-judging/issues/194
- Library page: https://bugbountylibrary.com/r/sherlock%3A2023-12-flatmoney%3AH-6
- Tags: domain:web3 impact:funds-loss kind:contest platform:sherlock sev:high tech:evm tech:solidity technique:source-review vuln:business-logic vuln:oracle-manipulation

**Target:** DelayedOrder.executeOrder / OracleModule.updatePythPrice
**Root cause:** executeOrder's updatePythPrice modifier forwards keeper-supplied priceUpdateData to Pyth.updatePriceFeeds without checking it is non-empty or matches the collateral price ID; an empty array makes Pyth perform zero updates, leaving the stale on-chain price.
**Steps:**
1. Keeper calls `executeOrder(account, [])` with empty priceUpdateData.
2. OracleModule.updatePythPrice calls `offchainOracle.oracleContract.updatePriceFeeds{value: fee}([])`; Pyth loops zero times, no price refresh.
3. executeOpen uses the existing stale on-chain Pyth price (timestamp within maxAge) to set the trader's entry price.
4. Keeper chooses stale price favorable to LPs (or colluding traders) when on-chain price deviates from latest off-chain price.
**How found:** Read the executeOrder modifier chain and noticed the keeper-supplied priceUpdateData is passed unchecked to Pyth; inspected Pyth.updatePriceFeeds source and saw an empty array yields zero updates, then reasoned the maxDiffPercent oracle check tolerates the deviation (e.g. $950/$1000 vs $975 Chainlink within 5%).
**Exploitation:** A keeper who is also an LP waits until the stale on-chain price ($1000) exceeds the latest off-chain price ($950), then executes the trader's open order with empty priceUpdateData so the long position enters at $1000; once the real $950 price lands, the trader is instantly down $50, transferred to LPs. Reverse direction favors long traders at LP expense.
**Insight:** When a protocol delegates oracle refresh to a permissionless caller, verify the callee actually performs an update on empty/mismatched input arrays—Pyth's updatePriceFeeds silently no-ops on empty data, so freshness/deviation checks alone don't prevent stale-price settlement.
