Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add debt max loss #190

Merged
merged 1 commit into from
Jan 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion TECH_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ This responsibility is taken by callers with DEBT_MANAGER role

This role can increase or decrease strategies specific debt.

The vault sends and receives funds to/from strategies. The function updateDebt(strategy, target_debt) will set the current_debt of the strategy to target_debt (if possible)
The vault sends and receives funds to/from strategies. The function update_debt(strategy, target_debt, max_loss) will set the current_debt of the strategy to target_debt (if possible)

If the strategy currently has less debt than the target_debt, the vault will send funds to it.

Expand Down
18 changes: 14 additions & 4 deletions contracts/VaultV3.vy
Original file line number Diff line number Diff line change
Expand Up @@ -1036,7 +1036,7 @@ def _revoke_strategy(strategy: address, force: bool=False):

# DEBT MANAGEMENT #
@internal
def _update_debt(strategy: address, target_debt: uint256) -> uint256:
def _update_debt(strategy: address, target_debt: uint256, max_loss: uint256) -> uint256:
"""
The vault will re-balance the debt vs target debt. Target debt must be
smaller or equal to strategy's max_debt. This function will compare the
Expand Down Expand Up @@ -1096,8 +1096,13 @@ def _update_debt(strategy: address, target_debt: uint256) -> uint256:
# We pull funds with {redeem} so there can be losses or rounding differences.
withdrawn: uint256 = min(post_balance - pre_balance, current_debt)

# If we didn't get the amount we asked for and there is a max loss.
if withdrawn < assets_to_withdraw and max_loss < MAX_BPS:
# Make sure the loss is within the allowed range.
assert assets_to_withdraw - withdrawn <= assets_to_withdraw * max_loss / MAX_BPS, "too much loss"

# If we got too much make sure not to increase PPS.
if withdrawn > assets_to_withdraw:
elif withdrawn > assets_to_withdraw:
assets_to_withdraw = withdrawn

# Update storage.
Expand Down Expand Up @@ -1711,15 +1716,20 @@ def update_max_debt_for_strategy(strategy: address, new_max_debt: uint256):

@external
@nonreentrant("lock")
def update_debt(strategy: address, target_debt: uint256) -> uint256:
def update_debt(
strategy: address,
target_debt: uint256,
max_loss: uint256 = MAX_BPS
) -> uint256:
"""
@notice Update the debt for a strategy.
@param strategy The strategy to update the debt for.
@param target_debt The target debt for the strategy.
@param max_loss Optional to check realized losses on debt decreases.
@return The amount of debt added or removed.
"""
self._enforce_role(msg.sender, Roles.DEBT_MANAGER)
return self._update_debt(strategy, target_debt)
return self._update_debt(strategy, target_debt, max_loss)

## EMERGENCY MANAGEMENT ##
@external
Expand Down
118 changes: 98 additions & 20 deletions tests/unit/vault/test_debt_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,26 +207,6 @@ def test_update_debt__with_new_debt_greater_than_max_desired_debt(
assert vault.totalDebt() == initial_debt + difference


# def test_update_debt__with_new_debt_less_than_min_desired_debt__reverts(
# gov, asset, vault, strategy, add_debt_to_strategy
# ):
# vault_balance = asset.balanceOf(vault)
# current_debt = vault_balance // 2
# new_debt = vault_balance
# min_desired_debt = vault_balance * 2
#
# # set existing debt
# add_debt_to_strategy(gov, strategy, vault, current_debt)
#
# # set new max debt lower than min debt
# vault.update_max_debt_for_strategy(strategy.address, new_debt, sender=gov)
# strategy.setMinDebt(min_desired_debt, sender=gov)
#
# with ape.reverts("new debt less than min debt"):
# vault.update_debt(strategy.address, sender=gov)
#


@pytest.mark.parametrize("minimum_total_idle", [0, 10**21])
def test_set_minimum_total_idle__with_minimum_total_idle(
gov, vault, minimum_total_idle
Expand Down Expand Up @@ -495,6 +475,51 @@ def test_update_debt__with_lossy_strategy_that_withdraws_less_than_requested(
assert vault.totalDebt() == new_debt


def test_update_debt__with_lossy_strategy_that_withdraws_less_than_requested__max_loss(
gov, asset, vault, lossy_strategy, add_debt_to_strategy
):
vault_balance = asset.balanceOf(vault)

add_debt_to_strategy(gov, lossy_strategy, vault, vault_balance)

initial_idle = vault.totalIdle()
initial_debt = vault.totalDebt()
current_debt = vault.strategies(lossy_strategy.address).current_debt
loss = current_debt // 10
new_debt = 0
difference = current_debt - loss

lossy_strategy.setWithdrawingLoss(loss, sender=gov)

initial_pps = vault.pricePerShare()

# With 0 max loss should revert.
with ape.reverts("too much loss"):
vault.update_debt(lossy_strategy.address, 0, 0, sender=gov)

# Up to the loss percent still reverts
with ape.reverts("too much loss"):
vault.update_debt(lossy_strategy.address, 0, 999, sender=gov)

# Over the loss percent will succeed and account correctly.
tx = vault.update_debt(lossy_strategy.address, 0, 1_000, sender=gov)
event = list(tx.decode_logs(vault.DebtUpdated))

# Should have recorded the loss
assert len(event) == 1
assert event[0].strategy == lossy_strategy.address
assert event[0].current_debt == current_debt
assert event[0].new_debt == new_debt

# assert we got back 90% of requested and it recorded the loss.
assert vault.pricePerShare() < initial_pps
assert vault.strategies(lossy_strategy.address).current_debt == new_debt
assert asset.balanceOf(lossy_strategy) == new_debt
assert asset.balanceOf(vault) == (vault_balance - loss)
assert vault.totalIdle() == initial_idle + difference
assert vault.totalDebt() == new_debt


def test_update_debt__with_faulty_strategy_that_withdraws_more_than_requested__only_half_withdrawn(
gov, asset, vault, lossy_strategy, add_debt_to_strategy, airdrop_asset
):
Expand Down Expand Up @@ -638,3 +663,56 @@ def test_update_debt__with_lossy_strategy_that_withdraws_less_than_requested_wit
assert asset.balanceOf(vault) == (vault_balance - loss + fish_amount)
assert vault.totalIdle() == initial_idle + difference
assert vault.totalDebt() == new_debt


def test_update_debt__with_lossy_strategy_that_withdraws_less_than_requested_with_airdrop_and_max_loss(
gov,
asset,
vault,
lossy_strategy,
add_debt_to_strategy,
airdrop_asset,
fish_amount,
):
vault_balance = asset.balanceOf(vault)

add_debt_to_strategy(gov, lossy_strategy, vault, vault_balance)

initial_idle = vault.totalIdle()
initial_debt = vault.totalDebt()
current_debt = vault.strategies(lossy_strategy.address).current_debt
loss = current_debt // 10
new_debt = 0
difference = current_debt - loss

lossy_strategy.setWithdrawingLoss(loss, sender=gov)

initial_pps = vault.pricePerShare()

# airdrop some asset to the vault
airdrop_asset(gov, asset, vault, fish_amount)

# With 0 max loss should revert.
with ape.reverts("too much loss"):
vault.update_debt(lossy_strategy.address, 0, 0, sender=gov)

# Up to the loss percent still reverts
with ape.reverts("too much loss"):
vault.update_debt(lossy_strategy.address, 0, 999, sender=gov)

# At the amount doesn't revert
tx = vault.update_debt(lossy_strategy.address, 0, 1_000, sender=gov)
event = list(tx.decode_logs(vault.DebtUpdated))

assert len(event) == 1
assert event[0].strategy == lossy_strategy.address
assert event[0].current_debt == current_debt
assert event[0].new_debt == new_debt

# assert we only got back half of what was requested and the vault recorded it correctly
assert vault.pricePerShare() < initial_pps
assert vault.strategies(lossy_strategy.address).current_debt == new_debt
assert asset.balanceOf(lossy_strategy) == new_debt
assert asset.balanceOf(vault) == (vault_balance - loss + fish_amount)
assert vault.totalIdle() == initial_idle + difference
assert vault.totalDebt() == new_debt
Loading