Conversation
590a713 to
fc89964
Compare
| if (address(oldSubVault) == address(0)) revert NoExistingSubVault(); | ||
|
|
||
| uint256 _totalSupply = totalSupply(); | ||
| uint256 assetReceived = oldSubVault.withdraw(oldSubVault.maxWithdraw(address(this)), address(this), address(this)); |
There was a problem hiding this comment.
there is an edge case here - the subvault may not have enough liquidity to serve this big withdrawal all at once.
we probably need to make switching vaults more robust to those liquidity constaints.
the same could be said about depositing to the new vault, it could be such a large deposit that slippage starts to become a serious issue
There was a problem hiding this comment.
we could do check whether maxWithdraw will return same amount of what master vault actually own
There was a problem hiding this comment.
function withdraw(uint256 assets, address receiver, address owner) public returns (uint256 shares)
note ERC4626.withdraw returns share withdrawn not assetReceived
Make master vault upgradable
fdd512f to
e9d49e5
Compare
* feat: access control roles * remove OwnableUpgradeable * add note about permissionless fee manager --------- Co-authored-by: Henry <11198460+godzillaba@users.noreply.github.com>
MasterVault: remove stored subvault exchange rate
MasterVault storage packing
MasterVault fix setters
tests: add more scenario tests
| pragma solidity ^0.8.0; | ||
|
|
||
| interface IMasterVault { | ||
| function setSubVault(address subVault, uint256 minSubVaultExchRateWad) external; |
|
|
||
| /// @dev Overridden to add EXTRA_DECIMALS to the underlying asset decimals | ||
| function decimals() public view override returns (uint8) { | ||
| return IERC20Metadata(address(asset)).decimals() + EXTRA_DECIMALS; |
There was a problem hiding this comment.
nit: erc20 can have no decimals
There was a problem hiding this comment.
if a token has no decimals method, can we safely assume it's 18 decimals or it would default to zero?
| /// @dev Can only be called by the token bridge gateway | ||
| /// @param assets The amount of underlying assets to deposit | ||
| /// @return shares The amount of vault shares minted to the depositor | ||
| function deposit(uint256 assets) |
There was a problem hiding this comment.
slippage will be handled by the gateway, similar to #166
| uint256 totalAssetsUp = _totalAssetsLessProfit(MathUpgradeable.Rounding.Up); | ||
| uint256 totalAssetsDown = _totalAssetsLessProfit(MathUpgradeable.Rounding.Down); |
There was a problem hiding this comment.
maybe we should also allocate profit?
There was a problem hiding this comment.
i'm indifferent, but i guess it is more capital efficient that way
| asset.safeTransfer(beneficiary, amountToTransfer); | ||
| } | ||
| if (amountToWithdraw > 0) { | ||
| subVault.withdraw(amountToWithdraw, beneficiary, address(this)); |
There was a problem hiding this comment.
this does not check withdrawal limit with preview, might revert, should allow partial withdrawal
same applies to other subvault withdraw
| false, withdrawAmount, desiredWithdraw, minimumRebalanceAmount | ||
| ); | ||
| } | ||
| subVault.withdraw(withdrawAmount, address(this), address(this)); |
There was a problem hiding this comment.
dangerous no slippage protection rebalance, can be sandwiched
There was a problem hiding this comment.
we used to have an exchange rate based slippage here. I'll reintroduce one
There was a problem hiding this comment.
I believe if we use exchange rate approach then keeper can't be permissionless
| true, depositAmount, desiredDeposit, minimumRebalanceAmount | ||
| ); | ||
| } | ||
| subVault.deposit(depositAmount, address(this)); |
There was a problem hiding this comment.
dangerous no slippage protection rebalance, can be sandwiched
| function rebalance() external whenNotPaused nonReentrant onlyKeeper { | ||
| // Check cooldown | ||
| uint256 timeSinceLastRebalance = block.timestamp - lastRebalanceTime; | ||
| if (timeSinceLastRebalance < rebalanceCooldown) { | ||
| revert RebalanceCooldownNotMet(timeSinceLastRebalance, rebalanceCooldown); | ||
| } | ||
|
|
||
| uint256 totalAssetsUp = _totalAssetsLessProfit(MathUpgradeable.Rounding.Up); | ||
| uint256 totalAssetsDown = _totalAssetsLessProfit(MathUpgradeable.Rounding.Down); | ||
| uint256 idleTargetUp = | ||
| totalAssetsUp.mulDiv(1e18 - targetAllocationWad, 1e18, MathUpgradeable.Rounding.Up); | ||
| uint256 idleTargetDown = | ||
| totalAssetsDown.mulDiv(1e18 - targetAllocationWad, 1e18, MathUpgradeable.Rounding.Down); | ||
| uint256 idleBalance = asset.balanceOf(address(this)); | ||
|
|
||
| if (idleTargetDown <= idleBalance && idleBalance <= idleTargetUp) { | ||
| revert TargetAllocationMet(); | ||
| } | ||
|
|
||
| if (idleBalance < idleTargetDown) { | ||
| // we need to withdraw from subvault | ||
| uint256 desiredWithdraw = idleTargetDown - idleBalance; | ||
| uint256 maxWithdrawable = subVault.maxWithdraw(address(this)); | ||
| uint256 withdrawAmount = | ||
| desiredWithdraw < maxWithdrawable ? desiredWithdraw : maxWithdrawable; | ||
| if (withdrawAmount < minimumRebalanceAmount) { | ||
| revert RebalanceAmountTooSmall( | ||
| false, withdrawAmount, desiredWithdraw, minimumRebalanceAmount | ||
| ); | ||
| } | ||
| subVault.withdraw(withdrawAmount, address(this), address(this)); | ||
| emit Rebalanced(false, desiredWithdraw, withdrawAmount); | ||
| } else { | ||
| // we need to deposit into subvault | ||
| uint256 desiredDeposit = idleBalance - idleTargetUp; | ||
| uint256 maxDepositable = subVault.maxDeposit(address(this)); | ||
| uint256 depositAmount = | ||
| desiredDeposit < maxDepositable ? desiredDeposit : maxDepositable; | ||
| if (depositAmount < minimumRebalanceAmount) { | ||
| revert RebalanceAmountTooSmall( | ||
| true, depositAmount, desiredDeposit, minimumRebalanceAmount | ||
| ); | ||
| } | ||
| asset.safeApprove(address(subVault), depositAmount); | ||
| subVault.deposit(depositAmount, address(this)); | ||
| emit Rebalanced(true, desiredDeposit, depositAmount); | ||
| } | ||
|
|
||
| lastRebalanceTime = uint40(block.timestamp); | ||
| } |
Check warning
Code scanning / Slither
Reentrancy vulnerabilities Medium
| function rebalance() external whenNotPaused nonReentrant onlyKeeper { | ||
| // Check cooldown | ||
| uint256 timeSinceLastRebalance = block.timestamp - lastRebalanceTime; | ||
| if (timeSinceLastRebalance < rebalanceCooldown) { | ||
| revert RebalanceCooldownNotMet(timeSinceLastRebalance, rebalanceCooldown); | ||
| } | ||
|
|
||
| uint256 totalAssetsUp = _totalAssetsLessProfit(MathUpgradeable.Rounding.Up); | ||
| uint256 totalAssetsDown = _totalAssetsLessProfit(MathUpgradeable.Rounding.Down); | ||
| uint256 idleTargetUp = | ||
| totalAssetsUp.mulDiv(1e18 - targetAllocationWad, 1e18, MathUpgradeable.Rounding.Up); | ||
| uint256 idleTargetDown = | ||
| totalAssetsDown.mulDiv(1e18 - targetAllocationWad, 1e18, MathUpgradeable.Rounding.Down); | ||
| uint256 idleBalance = asset.balanceOf(address(this)); | ||
|
|
||
| if (idleTargetDown <= idleBalance && idleBalance <= idleTargetUp) { | ||
| revert TargetAllocationMet(); | ||
| } | ||
|
|
||
| if (idleBalance < idleTargetDown) { | ||
| // we need to withdraw from subvault | ||
| uint256 desiredWithdraw = idleTargetDown - idleBalance; | ||
| uint256 maxWithdrawable = subVault.maxWithdraw(address(this)); | ||
| uint256 withdrawAmount = | ||
| desiredWithdraw < maxWithdrawable ? desiredWithdraw : maxWithdrawable; | ||
| if (withdrawAmount < minimumRebalanceAmount) { | ||
| revert RebalanceAmountTooSmall( | ||
| false, withdrawAmount, desiredWithdraw, minimumRebalanceAmount | ||
| ); | ||
| } | ||
| subVault.withdraw(withdrawAmount, address(this), address(this)); | ||
| emit Rebalanced(false, desiredWithdraw, withdrawAmount); | ||
| } else { | ||
| // we need to deposit into subvault | ||
| uint256 desiredDeposit = idleBalance - idleTargetUp; | ||
| uint256 maxDepositable = subVault.maxDeposit(address(this)); | ||
| uint256 depositAmount = | ||
| desiredDeposit < maxDepositable ? desiredDeposit : maxDepositable; | ||
| if (depositAmount < minimumRebalanceAmount) { | ||
| revert RebalanceAmountTooSmall( | ||
| true, depositAmount, desiredDeposit, minimumRebalanceAmount | ||
| ); | ||
| } | ||
| asset.safeApprove(address(subVault), depositAmount); | ||
| subVault.deposit(depositAmount, address(this)); | ||
| emit Rebalanced(true, desiredDeposit, depositAmount); | ||
| } | ||
|
|
||
| lastRebalanceTime = uint40(block.timestamp); | ||
| } |
Check warning
Code scanning / Slither
Unused return Medium
| function rebalance() external whenNotPaused nonReentrant onlyKeeper { | ||
| // Check cooldown | ||
| uint256 timeSinceLastRebalance = block.timestamp - lastRebalanceTime; | ||
| if (timeSinceLastRebalance < rebalanceCooldown) { | ||
| revert RebalanceCooldownNotMet(timeSinceLastRebalance, rebalanceCooldown); | ||
| } | ||
|
|
||
| uint256 totalAssetsUp = _totalAssetsLessProfit(MathUpgradeable.Rounding.Up); | ||
| uint256 totalAssetsDown = _totalAssetsLessProfit(MathUpgradeable.Rounding.Down); | ||
| uint256 idleTargetUp = | ||
| totalAssetsUp.mulDiv(1e18 - targetAllocationWad, 1e18, MathUpgradeable.Rounding.Up); | ||
| uint256 idleTargetDown = | ||
| totalAssetsDown.mulDiv(1e18 - targetAllocationWad, 1e18, MathUpgradeable.Rounding.Down); | ||
| uint256 idleBalance = asset.balanceOf(address(this)); | ||
|
|
||
| if (idleTargetDown <= idleBalance && idleBalance <= idleTargetUp) { | ||
| revert TargetAllocationMet(); | ||
| } | ||
|
|
||
| if (idleBalance < idleTargetDown) { | ||
| // we need to withdraw from subvault | ||
| uint256 desiredWithdraw = idleTargetDown - idleBalance; | ||
| uint256 maxWithdrawable = subVault.maxWithdraw(address(this)); | ||
| uint256 withdrawAmount = | ||
| desiredWithdraw < maxWithdrawable ? desiredWithdraw : maxWithdrawable; | ||
| if (withdrawAmount < minimumRebalanceAmount) { | ||
| revert RebalanceAmountTooSmall( | ||
| false, withdrawAmount, desiredWithdraw, minimumRebalanceAmount | ||
| ); | ||
| } | ||
| subVault.withdraw(withdrawAmount, address(this), address(this)); | ||
| emit Rebalanced(false, desiredWithdraw, withdrawAmount); | ||
| } else { | ||
| // we need to deposit into subvault | ||
| uint256 desiredDeposit = idleBalance - idleTargetUp; | ||
| uint256 maxDepositable = subVault.maxDeposit(address(this)); | ||
| uint256 depositAmount = | ||
| desiredDeposit < maxDepositable ? desiredDeposit : maxDepositable; | ||
| if (depositAmount < minimumRebalanceAmount) { | ||
| revert RebalanceAmountTooSmall( | ||
| true, depositAmount, desiredDeposit, minimumRebalanceAmount | ||
| ); | ||
| } | ||
| asset.safeApprove(address(subVault), depositAmount); | ||
| subVault.deposit(depositAmount, address(this)); | ||
| emit Rebalanced(true, desiredDeposit, depositAmount); | ||
| } | ||
|
|
||
| lastRebalanceTime = uint40(block.timestamp); | ||
| } |
Check warning
Code scanning / Slither
Unused return Medium
This isolate the implementation for the master vault and its related factory and subvault.