Gain From Balancer Vaults Can Be Stolen
The BPT gain (rewards) of the vault can be stolen by an attacker.\\nAt T0 (Time 0), assume that the state of the WETH/wstETH MetaPool Vault is as follows:\\ntotalBPTHeld = 1000 BPT\\ntotalStrategyTokenGlobal = 1000\\n1 Strategy Token can claim 1 BPT\\nAlice holds 1000 Strategy Tokens, and she is the only person invested in the vault at this point in time\\nAssume that if the `reinvestReward` is called, it will reinvest 1000 BPT back into the vault. Thus, if the `reinvestReward` is called, the `totalBPTHeld` of the vault will become 2000 BPT.\\nFollowing is the description of the attack:\\nThe attacker notice that if the `reinvestReward` is called, it will result in a large increase in the total BPT held by the vault\\nThe attacker flash-loan a large amount of WETH (e.g. 1,000,000) from a lending protocol (e.g. dydx)\\nEnter the vault by depositing 1,000,000 WETH by calling the `VaultAccountAction.enterVault` function. However, do not borrow any cash from Notional by setting the `fCash` parameter of the `VaultAccountAction.enterVault` function to `0`.\\nThere is no need to borrow from Notional as the attacker could already flash-loan a large amount of WETH with a non-existence fee rate (e.g. 1 Wei in dydx). Most importantly, the vault fee will only be charged if the user borrows from Notional. The fee is assessed within the `VaultAccount._borrowIntoVault`, which will be skipped if users are not borrowing. By not borrowing from Notional, the attacker does not need to pay any fee when entering the vault and this will make the attacker more profitable.\\nThe vault will deposit 1,000,000 WETH to the Balancer pool and receive a large amount of BPT in return. For simplicity's sake, assume that the vault receives 1,000,000 BPT in return.\\nBased on the `StrategyUtils._convertBPTClaimToStrategyTokens` function, the attacker will receive 100,000 strategy tokens. The state of the vault will be as follows after the attacker deposits:\\ntotalBPTHeld = 1,001,000 BPT\\ntotalStrategyTokenGlobal = 1,001,000\\n1 Strategy Token can claim 1 BPT\\nAlice holds 1000 Strategy Tokens\\nAttacker holds 1,000,000 Strategy Tokens\\nThe attacker calls the `reinvestReward` function, and reward tokens will be reinvested. Assume that the vault receives 1000 BPT. The state of the vault will be as follows after the reinvest:\\ntotalBPTHeld = 1,002,000 BPT\\ntotalStrategyTokenGlobal = 1,001,000\\n1 Strategy Token can claim ~1.0009 BPT\\nAlice holds 1000 Strategy Tokens\\nAttacker holds 1,000,000 Strategy Tokens\\nThe attacker exits the vault with all his strategy tokens by calling the `VaultAccountAction.exitVault` function. This will cause the vault the redeem all the 100,000 Strategy Tokens owned by the attacker. Based on the `StrategyUtils._convertStrategyTokensToBPTClaim` function, the attacker will receive 1,000,999 BPT in return. Note that there is no fee for exiting the vault and there is no need for repaying the debt as the attacker did not borrow any assets from Notional at the beginning.\\n```\\nbptClaim = (strategyTokenAmount * context.totalBPTHeld) / context.vaultState.totalStrategyTokenGlobal;\\n1,000,999 = (1000000 * 1002000) / 1001000\\n```\\n\\nProceed to repay the flash-loan at the end of the transaction. All the above steps are executed within a single transaction. Within a single transaction/block, the attacker is able to increase his holding of 1,000,000 BPT to 1,000,999 BPT after calling the `reinvestReward` function, and effectively gain around 999 BPT.\\nAlice who had been invested in the vault since the vault was first launched should be entitled to the majority of the rewards (Close to 1000 BPT). However, the attacker who came in right before the `reinvestReward` function was triggered managed to obtain almost all of her allocated shares of rewards (999 BPT) and left only 1 BPT for Alice.\\nNote: A flash-loan is not required if the attacker has sufficient liquidity to carry out the attack or the vault does not have much liquidity.\\nFollowing are the two functions for converting between BPT and Strategy Token for reference.\\n```\\n/// @notice Converts BPT to strategy tokens\\nfunction _convertBPTClaimToStrategyTokens(StrategyContext memory context, uint256 bptClaim)\\n    internal pure returns (uint256 strategyTokenAmount) {\\n    if (context.totalBPTHeld == 0) {\\n        // Strategy tokens are in 8 decimal precision, BPT is in 18. Scale the minted amount down.\\n        return (bptClaim * uint256(Constants.INTERNAL_TOKEN_PRECISION)) / \\n            BalancerConstants.BALANCER_PRECISION;\\n    }\\n\\n    // BPT held in maturity is calculated before the new BPT tokens are minted, so this calculation\\n    // is the tokens minted that will give the account a corresponding share of the new bpt balance held.\\n    // The precision here will be the same as strategy token supply.\\n    strategyTokenAmount = (bptClaim * context.vaultState.totalStrategyTokenGlobal) / context.totalBPTHeld;\\n}\\n```\\n\\n```\\n/// @notice Converts strategy tokens to BPT\\nfunction _convertStrategyTokensToBPTClaim(StrategyContext memory context, uint256 strategyTokenAmount)\\n    internal pure returns (uint256 bptClaim) {\\n    require(strategyTokenAmount <= context.vaultState.totalStrategyTokenGlobal);\\n    if (context.vaultState.totalStrategyTokenGlobal > 0) {\\n        bptClaim = (strategyTokenAmount * context.totalBPTHeld) / context.vaultState.totalStrategyTokenGlobal;\\n    }\\n}\\n```\\n
Following are the list of root causes of the issue and some recommendation to mitigate them.\\n`reinvestReward` function is permissionless and can be called by anyone. It is recommended to implement access control to ensure that this function can only be triggered by Notional. Do note that even if the attacker cannot trigger the `reinvestReward` function, it is still possible for the attacker to front-run and back-end the `reinvestReward` transaction to carry out the attack if they see this transaction in the public mempool. Thus, consider sending the `reinvestReward` transaction as a private transaction via Flashbot so that the attacker cannot sandwich the transaction.\\nThere is no withdrawal fee. Also, there is no deposit fee as long as users did not borrow from Notional. Therefore, this attack is mostly profitable. It is recommended to impose a fee on the users of the vault even if the users did not borrow from Notional. All users should be charged a fee for the use of the vault. This will make the attack less likely to be profitable in most cases.\\nUsers can enter and exit the vault within the same transaction/block. This allows the attacker to leverage the flash-loan facility to reduce the cost of the attack to almost nothing. It is recommended to prevent users from entering and exiting the vault within the same transaction/block. If the user entered the vault in this block, he/she could only exit at the next block.\\nThere is no snapshotting to keep track of the deposit to ensure that BPT gain/rewards distributions are weighted according to deposit duration. Thus, a whale could deposit right before the `reinvestReward` function is triggered and exit the vault afterward and reap most of the gains. Consider implementing snapshotting within the vault.
Loss of assets for the users as their BPT gain (rewards) can be stolen. This issue affects all balancer-related vaults that contain the permissionless `reinvestReward` function.
```\\nbptClaim = (strategyTokenAmount * context.totalBPTHeld) / context.vaultState.totalStrategyTokenGlobal;\\n1,000,999 = (1000000 * 1002000) / 1001000\\n```\\n