Yearn Vault V3 review by panda === ## Scope [Code Repo](https://github.com/jmonteer/yearn-vaults-v3) [Commit](https://github.com/jmonteer/yearn-vaults-v3/commit/90ad39d8f8b8929383feac9b4533b220593d5471) The commit reviewed was 90ad39d8f8b8929383feac9b4533b220593d5471. The review covered the entire repository with a focus on the vyper contracts. ## Critical Findings No critical findings were found. ## High Findings ### 1. High - There is no warranty the strategies uses all the funds it was approved to use A strategy might only transfer some of the funds it was offered to use. It's assumed in the contract that the allowance might be spent less than 100%. This could result in wrong debt accounting, with more debt on the strategy than it was supposed to. ## Proof of concept ``` self.erc20_safe_approve(ASSET.address, strategy, assets_to_transfer) IStrategy(strategy).deposit(assets_to_transfer, self) self.erc20_safe_approve(ASSET.address, strategy, 0) self.total_idle -= assets_to_transfer self.total_debt += assets_to_transfer ``` The `erc20_safe_approve(ASSET.address, strategy, 0)` is only useful if the strategy hasn't used its entire allowance. https://github.com/jmonteer/yearn-vaults-v3/blob/90ad39d8f8b8929383feac9b4533b220593d5471/contracts/VaultV3.vy#L684 ## Impact Non-expected debt on the strategy. ## Recommendation Ensure the strategy spends all the allowance or check for unspent allowance and remove it from `assets_to_transfer`. ### 2. High - Strategy frontrunning The vault has an anti-front-run mechanism. Strategies can get deposits from multiple addresses that aren't limited to yearn vaults. It's possible for anyone to front-run/sandwich the strategy profits on strategies such as selling a token to make a profit. ## Impact Vaults using the strategy will only benefit from a smaller portion of the profit sharing. ## Recomendation Make strategy `deposit` a gate kept function to only vaults. ### 3. High - Accountant funds can be drained Account report isn't protected; with a malicious vault contract, it's possible to drain the funds out of the contract. The issue is also present on Accountant and FlexibleAccountant contracts. ## Proof of concept By creating contracts that behave like a vault, it's possible to drain funds using strategies that have a `refund_ratios` not set to zero by sending a high loss. https://github.com/jmonteer/yearn-vaults-v3/blob/90ad39d8f8b8929383feac9b4533b220593d5471/contracts/periphery/Accountant.vy#L71 ## Impact Funds will be lost to the attacker. ## Recommendation Control the access to the report function. ## Medium Findings ### 1. Medium - Vault shares can be transferred to vault or zero address The transfer function does not prevent shares from being sent to the vault or the zero address. ## Proof of concept ``` @internal def _transfer(sender: address, receiver: address, amount: uint256): # Protect people from accidentally sending their shares to bad places assert self.balance_of[sender] >= amount, "insufficient funds" self.balance_of[sender] -= amount self.balance_of[receiver] += amount log Transfer(sender, receiver, amount) ``` https://github.com/jmonteer/yearn-vaults-v3/blob/90ad39d8f8b8929383feac9b4533b220593d5471/contracts/VaultV3.vy#L191 ## Impact Funds can be lost for a user; I do not see a PPS manipulation possible when transferring funds to the contract. ## Recommendation Prevent transfers to self and zero address. ### 2. Medium - Withdrawing can be performed without loss by manipulating the array of strategies. In the case of a strategy that hasn't reported a loss yet, a user can withdraw his funds without loss by using strategies that aren't lossy before the call to report the loss is made. ## Low findings ### 1. Low - Calling non-standard ERC20 contracts should use the new vyper default response functionality In multiple places in the vyper contracts, the call to non-standard ERC20 is handled with a `raw_call` method. It should be replaced by adding the new `default_return_value` kwarg to the function when calling it. ## Proof of concept ``` response: Bytes[32] = raw_call( token, concat( method_id("approve(address,uint256)"), convert(spender, bytes32), convert(amount, bytes32), ), max_outsize=32, ) if len(response) > 0: assert convert(response, bool), "Transfer failed!" ``` https://github.com/jmonteer/yearn-vaults-v3/blob/90ad39d8f8b8929383feac9b4533b220593d5471/contracts/VaultV3.vy#L337 https://github.com/jmonteer/yearn-vaults-v3/blob/90ad39d8f8b8929383feac9b4533b220593d5471/contracts/VaultV3.vy#L354 https://github.com/jmonteer/yearn-vaults-v3/blob/90ad39d8f8b8929383feac9b4533b220593d5471/contracts/VaultV3.vy#L372 https://github.com/jmonteer/yearn-vaults-v3/blob/90ad39d8f8b8929383feac9b4533b220593d5471/contracts/periphery/Accountant.vy#L108 ## Impact Higher gas usage to deploy and copy the contracts. ## Recommendation Here is one example of how I suggest doing it ``` sucess: bool = ERC20(token).approve(spender, amount, default_return_value=True) assert sucess, "Approval failled." ``` ### 2. Low - FlexibleAccountant doesn't handle non-standard ERC20 Unlike the other contracts, the FlexibleAccountant doesn't use a safe `approve` method ## Proof of concept ``` ERC20(self.asset).approve(msg.sender, total_refunds) ``` https://github.com/jmonteer/yearn-vaults-v3/blob/90ad39d8f8b8929383feac9b4533b220593d5471/contracts/periphery/FlexibleAccountant.vy#L109 ## Recommendation Use the following method. ``` sucess: bool = ERC20(token).approve(spender, amount, default_return_value=True) assert sucess, "Approval failled." ``` ## Gas findings ### 1. Gas - unnecessary assert condition https://github.com/jmonteer/yearn-vaults-v3/blob/90ad39d8f8b8929383feac9b4533b220593d5471/contracts/VaultV3.vy#L468 ``` if shares == max_value(uint256): shares = shares_balance assert shares_balance >= shares, "insufficient shares to redeem" ``` ## Recommendation ``` if shares == max_value(uint256): shares = shares_balance else: assert shares_balance >= shares, "insufficient shares to redeem" ``` ## Question findings ### 1. Question - Should it be possible to add strategies to a blacklist to prevent withdrawal from it. On v2, it's possible to remove a strategy from the withdrawal list or to put it at the bottom of the list to make sure funds aren't withdrawn from it. I wonder if a mechanism to block/prevent from withdrawing from specific strategies is required? ### 2. Question - Funds might not be withdrawable without loss I can see a case where a vault has multiple strategies, some that are lossy if funds are withdrawn and some that are not. It seems to me some will be able to empty the non-lossy strategies when possible and prevent others from withdrawing without a loss. This could be an issue for projects building on top of the vault that might not be able to withdraw any amount of tokens without a loss. ### 3. Question - How does a strategy estimate the unrealized loss I wonder how possible it is for all the strategies to estimate the unrealized loss without triggering it. ### 4. Question - Should the minimum_total_idle be expressed in percentage When minimum_total_idle expressed in assets isn't zero, the amount of funds left is most likely too little for big vaults or too big for small vaults. With some of the vaults funds moving rapidly, it will require more monitoring and actions than a value expressed in percentage. ## Naming On the line https://github.com/jmonteer/yearn-vaults-v3/blob/90ad39d8f8b8929383feac9b4533b220593d5471/contracts/VaultV3.vy#L496 using share is confusing since it's not referring to shares but to the shared loss.