diff --git a/contracts/VaultV3.vy b/contracts/VaultV3.vy index a08b3e7c..b405b6c6 100644 --- a/contracts/VaultV3.vy +++ b/contracts/VaultV3.vy @@ -131,6 +131,9 @@ event UpdateWithdrawLimitModule: event UpdateDefaultQueue: new_default_queue: DynArray[address, MAX_QUEUE] +event UpdateUseDefaultQueue: + use_default_queue: bool + event UpdatedMaxDebtForStrategy: sender: indexed(address) strategy: indexed(address) @@ -218,6 +221,8 @@ FACTORY: public(immutable(address)) strategies: public(HashMap[address, StrategyParams]) # The current default withdrawal queue. default_queue: public(DynArray[address, MAX_QUEUE]) +# Should the vault use the default_queue regardless whats passed in. +use_default_queue: public(bool) # ERC20 - amount of shares per account balance_of: HashMap[address, uint256] @@ -459,7 +464,7 @@ def _convert_to_assets(shares: uint256, rounding: Rounding) -> uint256: """ assets = shares * (total_assets / total_supply) --- (== price_per_share * shares) """ - if shares == MAX_UINT256 or shares == 0: + if shares == max_value(uint256) or shares == 0: return shares total_supply: uint256 = self._total_supply() @@ -480,7 +485,7 @@ def _convert_to_shares(assets: uint256, rounding: Rounding) -> uint256: """ shares = amount * (total_supply / total_assets) --- (== amount / price_per_share) """ - if assets == MAX_UINT256 or assets == 0: + if assets == max_value(uint256) or assets == 0: return assets total_supply: uint256 = self._total_supply() @@ -620,10 +625,13 @@ def _max_withdraw( have: uint256 = current_idle loss: uint256 = 0 - # If no queue was passed use the default one. - _strategies: DynArray[address, MAX_QUEUE] = strategies - if len(_strategies) == 0: - _strategies = self.default_queue + # Cache the default queue. + _strategies: DynArray[address, MAX_QUEUE] = self.default_queue + + # If a custom queue was passed, and we dont force the default queue. + if len(strategies) != 0 and not self.use_default_queue: + # Use the custom queue. + _strategies = strategies for strategy in _strategies: # Can't use an invalid strategy. @@ -798,6 +806,7 @@ def _redeem( to the user that is redeeming their vault shares. """ assert receiver != empty(address), "ZERO ADDRESS" + assert max_loss <= MAX_BPS, "max loss" # If there is a withdraw limit module, check the max. if self.withdraw_limit_module != empty(address): @@ -822,13 +831,13 @@ def _redeem( # funds from strategies. if requested_assets > curr_total_idle: - # Cache the input withdrawal queue. - _strategies: DynArray[address, MAX_QUEUE] = strategies + # Cache the default queue. + _strategies: DynArray[address, MAX_QUEUE] = self.default_queue - # If no queue was passed. - if len(_strategies) == 0: - # Use the default queue. - _strategies = self.default_queue + # If a custom queue was passed, and we dont force the default queue. + if len(strategies) != 0 and not self.use_default_queue: + # Use the custom queue. + _strategies = strategies # load to memory to save gas curr_total_debt: uint256 = self.total_debt @@ -1360,6 +1369,19 @@ def set_default_queue(new_default_queue: DynArray[address, MAX_QUEUE]): log UpdateDefaultQueue(new_default_queue) +@external +def set_use_default_queue(use_default_queue: bool): + """ + @notice Set a new value for `use_default_queue`. + @dev If set `True` the default queue will always be + used no matter whats passed in. + @param use_default_queue new value. + """ + self._enforce_role(msg.sender, Roles.QUEUE_MANAGER) + self.use_default_queue = use_default_queue + + log UpdateUseDefaultQueue(use_default_queue) + @external def set_deposit_limit(deposit_limit: uint256): """ @@ -1386,7 +1408,7 @@ def set_deposit_limit_module(deposit_limit_module: address): """ assert self.shutdown == False # Dev: shutdown self._enforce_role(msg.sender, Roles.DEPOSIT_LIMIT_MANAGER) - assert self.deposit_limit == MAX_UINT256, "using deposit limit" + assert self.deposit_limit == max_value(uint256), "using deposit limit" self.deposit_limit_module = deposit_limit_module diff --git a/tests/unit/vault/test_erc4626.py b/tests/unit/vault/test_erc4626.py index fd2275be..4d4f6d0f 100644 --- a/tests/unit/vault/test_erc4626.py +++ b/tests/unit/vault/test_erc4626.py @@ -310,6 +310,53 @@ def test_max_withdraw__with_locked_strategy( assert vault.maxWithdraw(fish.address) == assets - locked +def test_max_withdraw__with_use_default_queue( + asset, + fish, + fish_amount, + gov, + create_vault, + create_strategy, + add_debt_to_strategy, + add_strategy_to_vault, + user_deposit, +): + vault = create_vault(asset) + shares = fish_amount + assets = shares + strategy = create_strategy(vault) + strategy_deposit = assets + total_idle = assets - strategy_deposit + + vault.set_role( + gov.address, + ROLES.ADD_STRATEGY_MANAGER + | ROLES.DEBT_MANAGER + | ROLES.MAX_DEBT_MANAGER + | ROLES.QUEUE_MANAGER, + sender=gov, + ) + user_deposit(fish, vault, asset, assets) + add_strategy_to_vault(gov, strategy, vault) + add_debt_to_strategy(gov, strategy, vault, strategy_deposit) + + assert vault.maxWithdraw(fish.address) == assets + assert vault.maxWithdraw(fish.address, 22) == assets + assert vault.maxWithdraw(fish.address, 22, [strategy]) == assets + # Using an inactive strategy will revert. + with ape.reverts("inactive strategy"): + vault.maxWithdraw(fish.address, 22, [vault]) + + # Set use_default_queue to true + vault.set_use_default_queue(True, sender=gov) + + assert vault.maxWithdraw(fish.address) == assets + assert vault.maxWithdraw(fish.address, 22) == assets + assert vault.maxWithdraw(fish.address, 22, [strategy]) == assets + # Even sending an inactive strategy will return the correct amount. + assert vault.maxWithdraw(fish.address, 22, [vault]) == assets + + def test_preview_redeem(asset, fish, fish_amount, create_vault, user_deposit): vault = create_vault(asset) shares = fish_amount @@ -520,6 +567,53 @@ def test_max_redeem__with_locked_strategy( assert vault.maxRedeem(fish.address) == assets - locked +def test_max_redeem__with_use_default_queue( + asset, + fish, + fish_amount, + gov, + create_vault, + create_strategy, + add_debt_to_strategy, + add_strategy_to_vault, + user_deposit, +): + vault = create_vault(asset) + shares = fish_amount + assets = shares + strategy = create_strategy(vault) + strategy_deposit = assets + total_idle = assets - strategy_deposit + + vault.set_role( + gov.address, + ROLES.ADD_STRATEGY_MANAGER + | ROLES.DEBT_MANAGER + | ROLES.MAX_DEBT_MANAGER + | ROLES.QUEUE_MANAGER, + sender=gov, + ) + user_deposit(fish, vault, asset, assets) + add_strategy_to_vault(gov, strategy, vault) + add_debt_to_strategy(gov, strategy, vault, strategy_deposit) + + assert vault.maxRedeem(fish.address) == assets + assert vault.maxRedeem(fish.address, 22) == assets + assert vault.maxRedeem(fish.address, 22, [strategy]) == assets + # Using an inactive strategy will revert. + with ape.reverts("inactive strategy"): + vault.maxRedeem(fish.address, 22, [vault]) + + # Set use_default_queue to true + vault.set_use_default_queue(True, sender=gov) + + assert vault.maxRedeem(fish.address) == assets + assert vault.maxRedeem(fish.address, 22) == assets + assert vault.maxRedeem(fish.address, 22, [strategy]) == assets + # Even sending an inactive strategy will return the correct amount. + assert vault.maxRedeem(fish.address, 22, [vault]) == assets + + # With limit modules diff --git a/tests/unit/vault/test_role_base_access.py b/tests/unit/vault/test_role_base_access.py index 32e8c055..9310ec0d 100644 --- a/tests/unit/vault/test_role_base_access.py +++ b/tests/unit/vault/test_role_base_access.py @@ -414,6 +414,11 @@ def test_set_default_queue__no_queue_manager__reverts(bunny, vault): vault.set_default_queue([], sender=bunny) +def test_use_default_queue__no_queue_manager__reverts(bunny, vault): + with ape.reverts("not allowed"): + vault.set_use_default_queue(True, sender=bunny) + + def test_set_default_queue__queue_manager(gov, vault, strategy, bunny): # We temporarily give bunny the role of DEBT_MANAGER tx = vault.set_role(bunny.address, ROLES.QUEUE_MANAGER, sender=gov) @@ -428,6 +433,24 @@ def test_set_default_queue__queue_manager(gov, vault, strategy, bunny): assert vault.get_default_queue() == [] +def test_set_use_default_queue__queue_manager(gov, vault, strategy, bunny): + # We temporarily give bunny the role of DEBT_MANAGER + tx = vault.set_role(bunny.address, ROLES.QUEUE_MANAGER, sender=gov) + + event = list(tx.decode_logs(vault.RoleSet)) + assert len(event) == 1 + assert event[0].account == bunny.address + assert event[0].role == ROLES.QUEUE_MANAGER + + assert vault.use_default_queue() == False + tx = vault.set_use_default_queue(True, sender=bunny) + + event = list(tx.decode_logs(vault.UpdateUseDefaultQueue)) + assert len(event) == 1 + assert event[0].use_default_queue == True + assert vault.use_default_queue() == True + + # PROFIT UNLOCK MANAGER diff --git a/tests/unit/vault/test_role_permissioned_access.py b/tests/unit/vault/test_role_permissioned_access.py index 339ce7e6..7dff5116 100644 --- a/tests/unit/vault/test_role_permissioned_access.py +++ b/tests/unit/vault/test_role_permissioned_access.py @@ -812,6 +812,11 @@ def test_set_default_queue__queue_manager_closed__reverts(bunny, vault): vault.set_default_queue([], sender=bunny) +def test_set_use_default_queue__queue_manager_closed__reverts(bunny, vault): + with ape.reverts("not allowed"): + vault.set_use_default_queue(True, sender=bunny) + + def test_set_default_queue__queue_manager_open(gov, vault, strategy, bunny): # We temporarily give bunny the role of DEBT_MANAGER tx = vault.set_open_role(ROLES.QUEUE_MANAGER, sender=gov) @@ -826,6 +831,24 @@ def test_set_default_queue__queue_manager_open(gov, vault, strategy, bunny): assert vault.get_default_queue() == [] +def test_set_use_default_queue__queue_manager_open(gov, vault, strategy, bunny): + # We temporarily give bunny the role of DEBT_MANAGER + tx = vault.set_open_role(ROLES.QUEUE_MANAGER, sender=gov) + + event = list(tx.decode_logs(vault.RoleStatusChanged)) + assert len(event) == 1 + assert event[0].role == ROLES.QUEUE_MANAGER + assert event[0].status == RoleStatusChange.OPENED + + assert vault.use_default_queue() == False + tx = vault.set_use_default_queue(True, sender=bunny) + + event = list(tx.decode_logs(vault.UpdateUseDefaultQueue)) + assert len(event) == 1 + assert event[0].use_default_queue == True + assert vault.use_default_queue() == True + + def test_set_default_queue__queue_manager_open_then_close__reverts( gov, vault, strategy, bunny, fish ): @@ -850,3 +873,33 @@ def test_set_default_queue__queue_manager_open_then_close__reverts( with ape.reverts("not allowed"): vault.set_default_queue([], sender=fish) + + +def test_set_use_default_queue__queue_manager_open_then_close__reverts( + gov, vault, strategy, bunny, fish +): + # We temporarily give bunny the role of DEBT_MANAGER + tx = vault.set_open_role(ROLES.QUEUE_MANAGER, sender=gov) + + event = list(tx.decode_logs(vault.RoleStatusChanged)) + assert len(event) == 1 + assert event[0].role == ROLES.QUEUE_MANAGER + assert event[0].status == RoleStatusChange.OPENED + + assert vault.use_default_queue() == False + tx = vault.set_use_default_queue(True, sender=bunny) + + event = list(tx.decode_logs(vault.UpdateUseDefaultQueue)) + assert len(event) == 1 + assert event[0].use_default_queue == True + assert vault.use_default_queue() == True + + tx = vault.close_open_role(ROLES.QUEUE_MANAGER, sender=gov) + + event = list(tx.decode_logs(vault.RoleStatusChanged)) + assert len(event) == 1 + assert event[0].role == ROLES.QUEUE_MANAGER + assert event[0].status == RoleStatusChange.CLOSED + + with ape.reverts("not allowed"): + vault.set_use_default_queue(False, sender=fish) diff --git a/tests/unit/vault/test_strategy_withdraw.py b/tests/unit/vault/test_strategy_withdraw.py index b213ba03..0efe8aa5 100644 --- a/tests/unit/vault/test_strategy_withdraw.py +++ b/tests/unit/vault/test_strategy_withdraw.py @@ -2030,3 +2030,191 @@ def test_withdraw__with_multiple_liquid_strategies_more_assets_than_debt__withdr assert asset.balanceOf(first_strategy) == profit assert asset.balanceOf(second_strategy) == 0 assert asset.balanceOf(fish) == amount + + +def test_withdraw__with_custom_queue_and_use_default_queue__overrides( + gov, + fish, + fish_amount, + asset, + create_vault, + create_strategy, + user_deposit, + add_strategy_to_vault, + add_debt_to_strategy, +): + vault = create_vault(asset) + amount = fish_amount + amount_per_strategy = amount // 2 # deposit half of amount per strategy + shares = amount // 2 + first_strategy = create_strategy(vault) + second_strategy = create_strategy(vault) + strategies = [first_strategy, second_strategy] + max_loss = 0 + + # deposit assets to vault + user_deposit(fish, vault, asset, amount) + + # set up strategies + vault.set_role( + gov.address, + ROLES.ADD_STRATEGY_MANAGER + | ROLES.DEBT_MANAGER + | ROLES.MAX_DEBT_MANAGER + | ROLES.QUEUE_MANAGER, + sender=gov, + ) + for strategy in strategies: + add_strategy_to_vault(gov, strategy, vault) + add_debt_to_strategy(gov, strategy, vault, amount_per_strategy) + + # Set override to true + vault.set_use_default_queue(True, sender=gov) + + # Set queue to opposite of the custom one + vault.set_default_queue([second_strategy, first_strategy], sender=gov) + + tx = vault.withdraw( + shares, + fish.address, + fish.address, + max_loss, + [s.address for s in strategies], + sender=fish, + ) + event = list(tx.decode_logs(vault.Withdraw)) + + assert len(event) >= 1 + n = len(event) - 1 + assert event[n].sender == fish + assert event[n].receiver == fish + assert event[n].owner == fish + assert event[n].shares == shares + assert event[n].assets == shares + + event = list(tx.decode_logs(vault.DebtUpdated)) + + # Should have only withdrawn from second strategy + assert len(event) == 1 + assert event[0].strategy == second_strategy.address + assert event[0].current_debt == amount_per_strategy + assert event[0].new_debt == 0 + + assert vault.strategies(first_strategy)["current_debt"] == amount_per_strategy + assert vault.strategies(second_strategy)["current_debt"] == 0 + assert asset.balanceOf(vault) == 0 + assert asset.balanceOf(first_strategy) == amount_per_strategy + assert asset.balanceOf(second_strategy) == 0 + assert asset.balanceOf(fish) == shares + assert vault.balanceOf(fish) > 0 + + +def test_redeem__with_custom_queue_and_use_default_queue__overrides( + gov, + fish, + fish_amount, + asset, + create_vault, + create_strategy, + user_deposit, + add_strategy_to_vault, + add_debt_to_strategy, +): + vault = create_vault(asset) + amount = fish_amount + amount_per_strategy = amount // 2 # deposit half of amount per strategy + shares = amount // 2 + first_strategy = create_strategy(vault) + second_strategy = create_strategy(vault) + strategies = [first_strategy, second_strategy] + max_loss = 0 + + # deposit assets to vault + user_deposit(fish, vault, asset, amount) + + # set up strategies + vault.set_role( + gov.address, + ROLES.ADD_STRATEGY_MANAGER + | ROLES.DEBT_MANAGER + | ROLES.MAX_DEBT_MANAGER + | ROLES.QUEUE_MANAGER, + sender=gov, + ) + for strategy in strategies: + add_strategy_to_vault(gov, strategy, vault) + add_debt_to_strategy(gov, strategy, vault, amount_per_strategy) + + # Set override to true + vault.set_use_default_queue(True, sender=gov) + + # Set queue to opposite of the custom one + vault.set_default_queue([second_strategy, first_strategy], sender=gov) + + tx = vault.redeem( + shares, + fish.address, + fish.address, + max_loss, + [s.address for s in strategies], + sender=fish, + ) + event = list(tx.decode_logs(vault.Withdraw)) + + assert len(event) >= 1 + n = len(event) - 1 + assert event[n].sender == fish + assert event[n].receiver == fish + assert event[n].owner == fish + assert event[n].shares == shares + assert event[n].assets == shares + + event = list(tx.decode_logs(vault.DebtUpdated)) + + # Should have only withdrawn from second strategy + assert len(event) == 1 + assert event[0].strategy == second_strategy.address + assert event[0].current_debt == amount_per_strategy + assert event[0].new_debt == 0 + + assert vault.strategies(first_strategy)["current_debt"] == amount_per_strategy + assert vault.strategies(second_strategy)["current_debt"] == 0 + assert asset.balanceOf(vault) == 0 + assert asset.balanceOf(first_strategy) == amount_per_strategy + assert asset.balanceOf(second_strategy) == 0 + assert asset.balanceOf(fish) == shares + assert vault.balanceOf(fish) > 0 + + +def test_withdraw__with_max_loss_too_high__reverts( + fish, fish_amount, asset, create_vault +): + vault = create_vault(asset) + amount = fish_amount + max_loss = 10_001 + + with ape.reverts("max loss"): + vault.withdraw( + amount, + fish.address, + fish.address, + max_loss, + sender=fish, + ) + + +def test_redeem__with_max_loss_too_high__reverts( + fish, fish_amount, asset, create_vault +): + vault = create_vault(asset) + shares = fish_amount + max_loss = 10_001 + + with ape.reverts("max loss"): + vault.redeem( + shares, + fish.address, + fish.address, + max_loss, + sender=fish, + )