# `Vault.update(anyUser,0,0,0)` can be called for free to increase `checkpoint.count` and pay smaller keeper fee than necessary

Zero-value Vault.update calls from arbitrary accounts inflate checkpoint.count, diluting the shared keeper fee so an attacker underpays settlement fees at other users' expense.

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

**Target:** Perennial V2 Vault contract (Vault.update / checkpoint accounting)
**Root cause:** Vault.update(user,0,0,0) passes all invariant checks and unconditionally increments checkpoint.count via Checkpoint.update, even though the caller pays no fee; keeper fee is split as keeper/count, so extra zero-value calls dilute each real user's share.
**Steps:**
1. Deposit or redeem normally so checkpoint.count=1 for the epoch.
2. From 3 unrelated funded-only-for-gas addresses call `Vault.update(addrN, 0, 0, 0)`; each passes invariants and increments checkpoint.count to 4.
3. After oracle update and settle, the real user's keeper fee is computed as `keeper / count` = settlementFee/4 while the vault still pays markets the full settlementFee.
4. Verify via test: checkpoint.count=3, user2 shares 10013140463 vs 9999780702 without inflation.
**How found:** Code review of Vault._update invariants and _socialize: noticed (0,0,0) passes all checks when global.assets==0, and traced Checkpoint.update unconditionally doing self.count++ while _withoutKeeperLocal divides keeper by count.
**Exploitation:** Attacker with a pending deposit/redeem calls zero-value updates from multiple throwaway addresses (gas only) to inflate checkpoint.count, reducing their own settlement fee share; the shortfall is socialized onto other vault users.
**Insight:** When a shared fee is divided by a counter, check every code path that increments that counter without paying into the pool — zero-amount/no-op calls that pass invariants are free counter inflators.
