Vulnerability: Keeper design complexity
Severity: medium

Description:
The current design of the protocol relies on the keeper being operated correctly in a complex manner. Since the offchain code for the keeper wasn't in scope of this audit, the following is a commentary on the complexity of the keeper operations in the context of the contracts. Keeper logic such as the order of operations and function argument parameters with log querying are some examples where if the keeper doesn't execute them correctly, there may be inconsistencies and issues with accounting of vault shares and vault funds resulting in unexpected behaviour. While it may represent little risk or issues to the current Brahma-fi team as the vault is recently live, the keeper logic and exact steps should be well documented so that public keepers (if and when they are enabled) can execute the logic securely and future iterations of the vault code can account for any intricacies of the keeper logic.\\n1. Order of operations: Convex rewards & new depositors profiting at the expense of old depositors' yielded reward tokens. As part of the vault's strategy, the depositors' ETH is provided to Curve and the LP tokens are staked in Convex, which yield rewards such as CRV, CVX, and LDO tokens. As new depositors provide their ETH, the vault shares minted for their deposits will be less compared to old deposits as they account for the increasing value of LP tokens staked in these pools. In other words, if the first depositor provides 1 ETH, then when a new depositor provides 1 ETH much later, the new depositor will get less shares back as the `totalVaultFunds()` will increase:\\n```\\nshares = totalSupply() > 0\\n    ? (totalSupply() \\* amountIn) / totalVaultFunds()\\n    : amountIn;\\n```\\n\\n```\\nfunction totalVaultFunds() public view returns (uint256) {\\n    return\\n        IERC20(wantToken).balanceOf(address(this)) + totalExecutorFunds();\\n}\\n```\\n\\n```\\nfunction totalFunds() public view override returns (uint256, uint256) {\\n    return ConvexPositionHandler.positionInWantToken();\\n}\\n```\\n\\n```\\nfunction positionInWantToken()\\n    public\\n    view\\n    override\\n    returns (uint256, uint256)\\n{\\n    (\\n        uint256 stakedLpBalanceInETH,\\n        uint256 lpBalanceInETH,\\n        uint256 ethBalance\\n    ) = \\_getTotalBalancesInETH(true);\\n\\n    return (\\n        stakedLpBalanceInETH + lpBalanceInETH + ethBalance,\\n        block.number\\n    );\\n}\\n```\\n\\nHowever, this does not account for the reward tokens yielded throughout that time. From the smart contract logic alone, there is no requirement to first execute the reward token harvest. It is up to the keeper to execute `ConvexTradeExecutor.claimRewards` in order to claim and swap their rewards into ETH, which only then will be included into the yield in the above `ConvexPositionHandler.positionInWantToken` function. If this is not done prior to processing new deposits and minting new shares, new depositors would unfairly benefit from the reward tokens' yield that was generated before they deposited but accounted for in the vault funds only after they deposited.\\n2. Order of operations: closing Lyra options before processing new deposits.\\nThe other part of the vault's strategy is utilising the yield from Convex to purchase options from Lyra on Optimism. While Lyra options are risky and can become worthless in the event of bad trades, only yield is used for them, therefore keeping user deposits' initial value safe. However, their value could also yield significant returns, increasing the overall funds of the vault. Just as with `ConvexTradeExecutor`, `LyraTradeExecutor` also has a `totalFunds()` function that feeds into the vault's `totalVaultFunds()` function. In Lyra's case, however, it is a manually set value by the keeper that is supposed to represent the value of Lyra L2 options:\\n```\\nfunction totalFunds()\\n    public\\n    view\\n    override\\n    returns (uint256 posValue, uint256 lastUpdatedBlock)\\n{\\n    return (\\n        positionInWantToken.posValue +\\n            IERC20(vaultWantToken()).balanceOf(address(this)),\\n        positionInWantToken.lastUpdatedBlock\\n    );\\n}\\n```\\n\\n```\\nfunction setPosValue(uint256 \\_posValue) public onlyKeeper {\\n    LyraPositionHandler.\\_setPosValue(\\_posValue);\\n}\\n```\\n\\n```\\nfunction \\_setPosValue(uint256 \\_posValue) internal {\\n    positionInWantToken.posValue = \\_posValue;\\n    positionInWantToken.lastUpdatedBlock = block.number;\\n}\\n```\\n\\nSolely from the smart contract logic, there is a possibility that a user deposits when Lyra options are valued high, meaning the total vault funds are high as well, thus decreasing the amount of shares the user would have received if it weren't for the Lyra options' value. Consequently, if after the deposit the Lyra options become worthless, decreasing the total vault funds, the user's newly minted shares will now represent less than what they have deposited.\\nWhile this is not currently mitigated by smart contract logic, it may be worked around by the keeper first settling and closing all Lyra options and transferring all their yielded value in ETH, if any, to the Convex trade executor. Only then the keeper would process new deposits and mint new shares. This order of operations is critical to maintain the vault's intended safe strategy of maintaining the user's deposited value, and is dependent entirely on the keeper offchain logic.\\n3. Order of operations: additional trade executors and their specific management Similarly to the above examples, as more trade executors and position handlers are added to the vault, the complexity for the keeper will go up significantly, requiring it to maintain all correct orders of operations not just to keep the shares and funds accounting intact, but simply for the trade executors to function normally. For example, in the case of Lyra, the keepers need to manually call `confirmDeposit` and `confirmWithdraw` to update their `depositStatus` and `withdrawalStatus` respectively to continue normal operations or otherwise new deposits and withdrawals wouldn't be processed. On the other hand, the Convex executor does it automatically. Due to the system design, there may be no single standard way to handle a trade executor. New executors may also require specific calls to be done manually, increasing overall complexity keeper logic to support the system.\\n4. Keeper calls & arguments: depositFunds/batchDeposit and initiateWithdrawal/batchWithdraw `userAddresses[]` array + gas overhead With the current gated approach and batching for deposits and withdrawals to and from the vault, users aren't able to directly mint and redeem their vault shares. Instead, they interact with the `Batcher` contract that then communicates with the `Vault` contract with the help of the keeper. However, while each user's deposit and withdrawal amounts are registered in the contract state variables such as `depositLedger[user]` and `withdrawLedger[user]`, and there is an event emitted with the user address and their action, to process them the keeper is required to keep track of all the user addresses in the batch they need to process. In particular, the keeper needs to provide `address[] memory users` for both `batchDeposit()` and `batchWithdraw()` functions that communicate with the vault. There is no stored list of users within the contract that could provide or verify the right users, so it is entirely up to the keeper's offchain logic to query the logs and retrieve the addresses required. Therefore, depending on the size of the `address[] memory users` array, the keepers may need to consider the transaction gas limit, possibly requiring splitting the array up and doing several transactions to process all of them. In addition, in the event of withdrawals, the keepers need to calculate how much of the `wantToken` (WETH in our case) will be required to process the withdrawals, and call `withdrawFromExecutor()` with that amount to provide enough assets to cover withdrawals from the vault.\\n5. Timing: 50 block radius for updates on trade executors that need to have their values updated via a call Some trade executors, like the Convex one, can retrieve their funds value at any time from Layer 1, thereby always being up to date with the current block. Others, like the Lyra trade executor, require the keeper to update their position value by initiating a call, which also updates their `positionInWantToken.lastUpdatedBlock` state variable. However, this variable is also called during during the vault.totalVaultFunds()call during deposits and withdrawals via `totalExecutorFunds()`, which eventually calls `areFundsUpdated(blockUpdated)`. This is a check to ensure that the current transaction's `block.number <= _blockUpdated + BLOCK_LIMIT`, where BLOCK_LIMIT=50 blocks, i.e. roughly 12-15 min. As a result, keepers need to make sure that all executors that require a call for this have their position values updated before and rather close to processing and deposits or withdrawals, or `areFundsUpdated()` will revert those calls.

Impact:

Recommended mitigation:
Document the exact order of operations, steps, necessary logs and parameters that keepers need to keep track of in order for the vault strategy to succeed.