# TWAP misweights EMAs in RiskEngine, anchoring liquidation price to slow EMA and letting insolvent accounts dodge liquidation

RiskEngine.twapEMA destructured getEMAs() in wrong order, weighting slow EMA 60% and ignoring eons EMA, anchoring liquidation TWAP to stale prices so insolvent accounts dodge liquidation.

- Payout: Contest reward
- Severity: medium
- Program: Panoptic
- Credited: mrdafidi
- Disclosed: 2026-03-09
- Write-up quality: 6/10
- Original: https://code4rena.com/reports/2025-12-panoptic-next-core#m-03
- Library page: https://bugbountylibrary.com/r/c4%3A2025-12-panoptic-next-core%3AM-03
- Tags: domain:web3 impact:funds-loss kind:contest platform:code4rena sev:medium tech:evm tech:solidity technique:source-review vuln:accounting-error vuln:oracle-manipulation

**Target:** RiskEngine.twapEMA / OraclePack.getEMAs (Solidity smart contract)
**Root cause:** twapEMA destructures getEMAs() as (eonsEMA, slowEMA, fastEMA, ) while the function returns (spotEMA, fastEMA, slowEMA, eonsEMA, medianTick), so the 60/30/10 fast/slow/eons weighting becomes 60% slow, 30% fast, 10% spot and drops eons entirely.
**Steps:**
1. Read OraclePack.getEMAs() return order: (spotEMA, fastEMA, slowEMA, eonsEMA, medianTick).
2. Compare to RiskEngine.sol L836 destructuring `(int256 eonsEMA, int256 slowEMA, int256 fastEMA, )`.
3. Observe weighting `(6*fastEMA + 3*slowEMA + eonsEMA)/10` now uses slow as fast and spot as eons.
4. Call PanopticPool.getTWAP() and confirm it returns a slow-EMA-anchored value.
5. Simulate rapid adverse price move; check dispatchFrom solvency at [spot, twap, latest, current] and observe NotMarginCalled blocking liquidation.
6. Run PoC: `forge test --match-path test/foundry/core/RiskEngine/RiskEngineTwapEMAPoC.t.sol --match-test twapEMA_weights_fast_slow_eons_not_spot`.
**How found:** Code-reading comparison of the destructuring in RiskEngine.twapEMA against the actual return tuple of OraclePack.getEMAs(), spotting the positional mismatch and the resulting weight inversion.
**Exploitation:** An attacker or simply a market move leaves an underwater position; because the liquidation TWAP tracks the slow EMA, the account still appears solvent at the twap tick, so dispatchFrom reverts with NotMarginCalled and liquidation/force-exercise is blocked, delaying cleanup and risking bad debt or mispriced exercises.
**Insight:** When a function returns a multi-value tuple, verify every destructuring site against the declared return order; positional tuple unpacking in Solidity silently misassigns same-typed values, so a wrong order compiles cleanly and only shows up as skewed oracle math.
