# Consecutive symbol price updates can be exploited to drain protocol funds

Liquidator can re-call setSymbolsPrice between partial liquidations with unchanged upnl/totalUnrealizedLoss, letting Party B collect CVA twice and drain protocol funds.

- Payout: Contest reward
- Severity: medium
- Program: Symmetrical
- Credited: berndartmueller
- Disclosed: 2023-06-05
- Write-up quality: 7/10
- Original: https://github.com/sherlock-audit/2023-06-symmetrical-judging/issues/291
- Library page: https://bugbountylibrary.com/r/sherlock%3A2023-06-symmetrical%3AM-21
- Tags: domain:web3 impact:funds-loss kind:contest platform:sherlock sev:medium tech:evm tech:solidity technique:chained technique:source-review vuln:accounting-error vuln:business-logic

**Target:** LiquidationFacetImpl.setSymbolsPrice / liquidatePositionsPartyA
**Root cause:** setSymbolsPrice only enforces that upnl and totalUnrealizedLoss match prior liquidation details, not that symbol prices or open-position count are unchanged, so a liquidator can refresh prices mid-liquidation.
**Steps:**
1. Open two positions for Party A (BTCUSDT and AAVEUSDT), each 100e18 qty at 1e18, CVA 25e18, LF 25e18, MM 0.
2. Drop BTCUSDT 40% to 0.6e18; Party A upnl=-40e18, totalUnrealizedLoss=-40e18; call liquidatePartyA with priceSig for both symbols.
3. Call `liquidatePositionsPartyA(user,[1])`; Party B credited 40e18 loss + 25e18 CVA = 65e18.
4. Drop AAVEUSDT 40% to 0.6e18; call `setSymbolsPrice(user, priceSig([2],[6e17],-40e18,-40e18))` — require passes since upnl/totalUnrealizedLoss unchanged.
5. Call `liquidatePositionsPartyA(user,[2])`; Party B credited another 65e18, total 130e18 vs 100e18 locked, 30e18 bad debt to protocol.
**How found:** Code review of setSymbolsPrice's require block: noticed it only pins upnl and totalUnrealizedLoss, leaving symbol prices mutable across partial liquidations; built a Hardhat test with two positions and a mid-liquidation price update to confirm double CVA payout.
**Exploitation:** Liquidator colludes with whitelisted Party B; during a volatile market they split liquidation into multiple calls and refresh symbol prices in between while keeping upnl and totalUnrealizedLoss identical, so each liquidated position pays out full CVA again, extracting protocol funds as bad debt.
**Insight:** When a state-mutating setter is guarded by equality checks on only some fields, check whether the unguarded fields (here symbol prices) can be refreshed between partial state transitions to re-trigger payouts like CVA per position.
