Skip to content

Commit

Permalink
build: only burn or mint (#193)
Browse files Browse the repository at this point in the history
* build: only burn or mint

* build: target end supply

* chore: comments

* fix: comments

* fix: strategy changes
  • Loading branch information
Schlagonia authored Jan 21, 2024
1 parent 49706f2 commit 0f3c9ad
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 69 deletions.
130 changes: 62 additions & 68 deletions contracts/VaultV3.vy
Original file line number Diff line number Diff line change
Expand Up @@ -409,25 +409,6 @@ def _total_supply() -> uint256:
# Need to account for the shares issued to the vault that have unlocked.
return self.total_supply - self._unlocked_shares()

@internal
def _burn_unlocked_shares():
"""
Burns shares that have been unlocked since last update.
In case the full unlocking period has passed, it stops the unlocking.
"""
# Get the amount of shares that have unlocked
unlocked_shares: uint256 = self._unlocked_shares()

# Update last profit time no matter what.
self.last_profit_update = block.timestamp

# IF 0 there's nothing to do.
if unlocked_shares == 0:
return

# Burn the shares unlocked.
self._burn_shares(unlocked_shares, self)

@view
@internal
def _total_assets() -> uint256:
Expand Down Expand Up @@ -1167,9 +1148,6 @@ def _process_report(strategy: address) -> (uint256, uint256):
# Make sure we have a valid strategy.
assert self.strategies[strategy].activation != 0, "inactive strategy"

# Burn shares that have been unlocked since the last update
self._burn_unlocked_shares()

# Vault assesses profits using 4626 compliant interface.
# NOTE: It is important that a strategies `convertToAssets` implementation
# cannot be manipulated or else the vault could report incorrect gains/losses.
Expand All @@ -1182,55 +1160,93 @@ def _process_report(strategy: address) -> (uint256, uint256):
gain: uint256 = 0
loss: uint256 = 0

### Asses Gain or Loss ###

# Compare reported assets vs. the current debt.
if total_assets > current_debt:
# We have a gain.
gain = unsafe_sub(total_assets, current_debt)
else:
# We have a loss.
loss = unsafe_sub(current_debt, total_assets)

### Asses Fees and Refunds ###

# For Accountant fee assessment.
total_fees: uint256 = 0
total_refunds: uint256 = 0

accountant: address = self.accountant
# If accountant is not set, fees and refunds remain unchanged.
accountant: address = self.accountant
if accountant != empty(address):
total_fees, total_refunds = IAccountant(accountant).report(strategy, gain, loss)

if total_refunds > 0:
# Make sure we have enough approval and enough asset to pull.
total_refunds = min(total_refunds, min(ASSET.balanceOf(accountant), ASSET.allowance(accountant, self)))

# Total fees to charge in shares.
total_fees_shares: uint256 = 0
# For Protocol fee assessment.
protocol_fee_bps: uint16 = 0
protocol_fees_shares: uint256 = 0
protocol_fee_recipient: address = empty(address)

# `shares_to_burn` is derived from amounts that would reduce the vaults PPS.
# NOTE: this needs to be done before any pps changes
shares_to_burn: uint256 = 0
total_fees_shares: uint256 = 0
protocol_fees_shares: uint256 = 0
# Only need to burn shares if there is a loss or fees.
if loss + total_fees > 0:
# The amount of shares we will want to burn to offset losses and fees.
shares_to_burn += self._convert_to_shares(loss + total_fees, Rounding.ROUND_UP)
shares_to_burn = self._convert_to_shares(loss + total_fees, Rounding.ROUND_UP)

# Vault calculates the amount of shares to mint as fees before changing totalAssets / totalSupply.
# If we have fees then get the proportional amount of shares to issue.
if total_fees > 0:
# Get the total amount shares to issue for the fees.
total_fees_shares = self._convert_to_shares(total_fees, Rounding.ROUND_DOWN)
total_fees_shares = shares_to_burn * total_fees / (loss + total_fees)

# Get the config for this vault.
# Get the protocol fee config for this vault.
protocol_fee_bps, protocol_fee_recipient = IFactory(FACTORY).protocol_fee_config()

# If there is a protocol fee.
if protocol_fee_bps > 0:
# Get the percent of fees to go to protocol fees.
protocol_fees_shares = total_fees_shares * convert(protocol_fee_bps, uint256) / MAX_BPS

# Shares to lock is any amounts that would otherwise increase the vaults PPS.
newly_locked_shares: uint256 = 0

# Shares to lock is any amount that would otherwise increase the vaults PPS.
shares_to_lock: uint256 = 0
profit_max_unlock_time: uint256 = self.profit_max_unlock_time
# Get the amount we will lock to avoid a PPS increase.
if gain + total_refunds > 0 and profit_max_unlock_time != 0:
shares_to_lock = self._convert_to_shares(gain + total_refunds, Rounding.ROUND_DOWN)

# The total current supply including locked shares.
total_supply: uint256 = self.total_supply
# The total shares the vault currently owns. Both locked and unlocked.
total_locked_shares: uint256 = self.balance_of[self]

# Get the desired end amount of shares after all accounting.
ending_supply: uint256 = total_supply + shares_to_lock - shares_to_burn - self._unlocked_shares()

# If we will end with more shares than we have now.
if ending_supply > total_supply:
# Issue the difference.
self._issue_shares(unsafe_sub(ending_supply, total_supply), self)

# Else we need to burn shares.
elif total_supply > ending_supply:
# Can't burn more than the vault owns.
to_burn: uint256 = min(unsafe_sub(total_supply, ending_supply), total_locked_shares)
self._burn_shares(to_burn, self)

# Adjust the amount to lock for this period.
if shares_to_lock > shares_to_burn:
# Don't lock fees or losses.
shares_to_lock = unsafe_sub(shares_to_lock, shares_to_burn)
else:
shares_to_burn = 0

# Pull refunds
if total_refunds > 0:
# Make sure we have enough approval and enough asset to pull.
total_refunds = min(total_refunds, min(ASSET.balanceOf(accountant), ASSET.allowance(accountant, self)))
# Transfer the refunded amount of asset to the vault.
self._erc20_safe_transfer_from(ASSET.address, accountant, self, total_refunds)
# Update storage to increase total assets.
Expand All @@ -1243,62 +1259,40 @@ def _process_report(strategy: address) -> (uint256, uint256):
self.strategies[strategy].current_debt = current_debt
self.total_debt += gain

profit_max_unlock_time: uint256 = self.profit_max_unlock_time
# Mint anything we are locking to the vault.
if gain + total_refunds > 0 and profit_max_unlock_time != 0:
newly_locked_shares = self._issue_shares_for_amount(gain + total_refunds, self)

# Strategy is reporting a loss
if loss > 0:
# Or record any reported loss
elif loss > 0:
current_debt = unsafe_sub(current_debt, loss)
self.strategies[strategy].current_debt = current_debt
self.total_debt -= loss

# NOTE: should be precise (no new unlocked shares due to above's burn of shares)
# newly_locked_shares have already been minted / transferred to the vault, so they need to be subtracted
# no risk of underflow because they have just been minted.
previously_locked_shares: uint256 = self.balance_of[self] - newly_locked_shares

# Now that pps has updated, we can burn the shares we intended to burn as a result of losses/fees.
# NOTE: If a value reduction (losses / fees) has occurred, prioritize burning locked profit to avoid
# negative impact on price per share. Price per share is reduced only if losses exceed locked value.
if shares_to_burn > 0:
# Cant burn more than the vault owns.
shares_to_burn = min(shares_to_burn, previously_locked_shares + newly_locked_shares)
self._burn_shares(shares_to_burn, self)

# We burn first the newly locked shares, then the previously locked shares.
shares_not_to_lock: uint256 = min(shares_to_burn, newly_locked_shares)
# Reduce the amounts to lock by how much we burned
newly_locked_shares -= shares_not_to_lock
previously_locked_shares -= (shares_to_burn - shares_not_to_lock)

# Issue shares for fees that were calculated above if applicable.
if total_fees_shares > 0:
# Accountant fees are (total_fees - protocol_fees).
self._issue_shares(total_fees_shares - protocol_fees_shares, accountant)

if protocol_fees_shares > 0:
self._issue_shares(protocol_fees_shares, protocol_fee_recipient)
# If we also have protocol fees.
if protocol_fees_shares > 0:
self._issue_shares(protocol_fees_shares, protocol_fee_recipient)

# Update unlocking rate and time to fully unlocked.
total_locked_shares: uint256 = previously_locked_shares + newly_locked_shares
total_locked_shares = self.balance_of[self]
if total_locked_shares > 0:
previously_locked_time: uint256 = 0
_full_profit_unlock_date: uint256 = self.full_profit_unlock_date
# Check if we need to account for shares still unlocking.
if _full_profit_unlock_date > block.timestamp:
# There will only be previously locked shares if time remains.
# We calculate this here since it will not occur every time we lock shares.
previously_locked_time = previously_locked_shares * (_full_profit_unlock_date - block.timestamp)
previously_locked_time = (total_locked_shares - shares_to_lock) * (_full_profit_unlock_date - block.timestamp)

# new_profit_locking_period is a weighted average between the remaining time of the previously locked shares and the profit_max_unlock_time
new_profit_locking_period: uint256 = (previously_locked_time + newly_locked_shares * profit_max_unlock_time) / total_locked_shares
new_profit_locking_period: uint256 = (previously_locked_time + shares_to_lock * profit_max_unlock_time) / total_locked_shares
# Calculate how many shares unlock per second.
self.profit_unlocking_rate = total_locked_shares * MAX_BPS_EXTENDED / new_profit_locking_period
# Calculate how long until the full amount of shares is unlocked.
self.full_profit_unlock_date = block.timestamp + new_profit_locking_period

# Update the last profitable report timestamp.
self.last_profit_update = block.timestamp
else:
# NOTE: only setting this to the 0 will turn in the desired effect,
# no need to update profit_unlocking_rate
Expand All @@ -1316,7 +1310,7 @@ def _process_report(strategy: address) -> (uint256, uint256):
gain,
loss,
current_debt,
total_fees * convert(protocol_fee_bps, uint256) / MAX_BPS,
total_fees * convert(protocol_fee_bps, uint256) / MAX_BPS, # Protocol Fees
total_fees,
total_refunds
)
Expand Down
2 changes: 1 addition & 1 deletion contracts/test/mocks/ERC4626/MockTokenizedStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ contract MockTokenizedStrategy is TokenizedStrategy {
function availableDepositLimit(
address
) public view virtual returns (uint256) {
uint256 _totalAssets = strategyStorage().totalIdle;
uint256 _totalAssets = totalAssets();
uint256 _maxDebt = maxDebt;
return _maxDebt > _totalAssets ? _maxDebt - _totalAssets : 0;
}
Expand Down

0 comments on commit 0f3c9ad

Please sign in to comment.