Skip to content

Instantly share code, notes, and snippets.

@charles-cooper
Created November 24, 2024 19:25
sample yearn contract
from ethereum.ercs import IERC20
from ethereum.ercs import IERC20Detailed
# INTERFACES #
interface IStrategy:
def asset() -> address: view
def balanceOf(owner: address) -> uint256: view
def maxDeposit(receiver: address) -> uint256: view
def maxWithdraw(owner: address) -> uint256: view
def withdraw(amount: uint256, receiver: address, owner: address) -> uint256: nonpayable
def deposit(assets: uint256, receiver: address) -> uint256: nonpayable
def totalAssets() -> (uint256): view
def convertToAssets(shares: uint256) -> (uint256): view
def convertToShares(assets: uint256) -> (uint256): view
interface IAccountant:
def report(strategy: address, gain: uint256, loss: uint256) -> (uint256, uint256): nonpayable
interface IQueueManager:
def withdraw_queue(vault: address) -> (DynArray[address, 10]): nonpayable
def new_strategy(strategy: address): nonpayable
def remove_strategy(strategy: address): nonpayable
interface IFactory:
def protocol_fee_config() -> (uint16, uint32, address): view
# EVENTS #
# ERC4626 EVENTS
event Deposit:
sender: indexed(address)
owner: indexed(address)
assets: uint256
shares: uint256
event Withdraw:
sender: indexed(address)
receiver: indexed(address)
owner: indexed(address)
assets: uint256
shares: uint256
# ERC20 EVENTS
event Transfer:
sender: indexed(address)
receiver: indexed(address)
value: uint256
event Approval:
owner: indexed(address)
spender: indexed(address)
value: uint256
# STRATEGY EVENTS
event StrategyChanged:
strategy: indexed(address)
change_type: indexed(StrategyChangeType)
event StrategyReported:
strategy: indexed(address)
gain: uint256
loss: uint256
current_debt: uint256
protocol_fees: uint256
total_fees: uint256
total_refunds: uint256
# DEBT MANAGEMENT EVENTS
event DebtUpdated:
strategy: indexed(address)
current_debt: uint256
new_debt: uint256
# ROLE UPDATES
event RoleSet:
account: indexed(address)
role: indexed(Roles)
event RoleStatusChanged:
role: indexed(Roles)
status: indexed(RoleStatusChange)
# STORAGE MANAGEMENT EVENTS
event UpdateRoleManager:
role_manager: indexed(address)
event UpdateAccountant:
accountant: indexed(address)
event UpdateQueueManager:
queue_manager: indexed(address)
event UpdatedMaxDebtForStrategy:
sender: indexed(address)
strategy: indexed(address)
new_debt: uint256
event UpdateDepositLimit:
deposit_limit: uint256
event UpdateMinimumTotalIdle:
minimum_total_idle: uint256
event UpdateProfitMaxUnlockTime:
profit_max_unlock_time: uint256
event Shutdown:
pass
event Sweep:
token: indexed(address)
amount: uint256
# STRUCTS #
struct StrategyParams:
activation: uint256
last_report: uint256
current_debt: uint256
max_debt: uint256
# CONSTANTS #
MAX_BPS: constant(uint256) = 10_000
MAX_BPS_EXTENDED: constant(uint256) = 1_000_000_000_000
PROTOCOL_FEE_ASSESSMENT_PERIOD: constant(uint256) = 24 * 3600 # assess once a day
API_VERSION: constant(String[28]) = "3.1.0"
# ENUMS #
# Each permissioned function has its own Role.
# Roles can be combined in any combination or all kept seperate.
# Follows python Enum patterns so the first Enum == 1 and doubles each time.
flag Roles:
ADD_STRATEGY_MANAGER # can add strategies to the vault
REVOKE_STRATEGY_MANAGER # can remove strategies from the vault
FORCE_REVOKE_MANAGER # can force remove a strategy causing a loss
ACCOUNTANT_MANAGER # can set the accountant that assesss fees
QUEUE_MANAGER # can set the queue_manager
REPORTING_MANAGER # calls report for strategies
DEBT_MANAGER # adds and removes debt from strategies
MAX_DEBT_MANAGER # can set the max debt for a strategy
DEPOSIT_LIMIT_MANAGER # sets deposit limit for the vault
MINIMUM_IDLE_MANAGER # sets the minimun total idle the vault should keep
PROFIT_UNLOCK_MANAGER # sets the profit_max_unlock_time
SWEEPER # can sweep tokens from the vault
EMERGENCY_MANAGER # can shutdown vault in an emergency
flag StrategyChangeType:
ADDED
REVOKED
flag Rounding:
ROUND_DOWN
ROUND_UP
flag RoleStatusChange:
OPENED
CLOSED
# IMMUTABLE #
ASSET: immutable(IERC20)
DECIMALS: immutable(uint256)
FACTORY: public(immutable(address))
# STORAGE #
# HashMap that records all the strategies that are allowed to receive assets from the vault
strategies: public(HashMap[address, StrategyParams])
# ERC20 - amount of shares per account
balance_of: HashMap[address, uint256]
# ERC20 - owner -> (spender -> amount)
allowance: public(HashMap[address, HashMap[address, uint256]])
# Total amount of shares that are currently minted
# To get the ERC20 compliant version user totalSupply().
total_supply: public(uint256)
# Total amount of assets that has been deposited in strategies
total_debt: uint256
# Current assets held in the vault contract. Replacing balanceOf(this) to avoid price_per_share manipulation
total_idle: uint256
# Minimum amount of assets that should be kept in the vault contract to allow for fast, cheap redeems
minimum_total_idle: public(uint256)
# Maximum amount of tokens that the vault can accept. If totalAssets > deposit_limit, deposits will revert
deposit_limit: public(uint256)
# Contract that charges fees and can give refunds
accountant: public(address)
# Contract that will supply a optimal withdrawal queue of strategies
queue_manager: public(address)
# HashMap mapping addresses to their roles
roles: public(HashMap[address, Roles])
# HashMap mapping roles to their permissioned state. If false, the role is not open to the public
open_roles: public(HashMap[Roles, bool])
# Address that can add and remove addresses to roles
role_manager: public(address)
# Temporary variable to store the address of the next role_manager until the role is accepted
future_role_manager: public(address)
# State of the vault - if set to true, only withdrawals will be available. It can't be reverted
shutdown: public(bool)
# ERC20 - name of the token
name: public(String[64])
# ERC20 - symbol of the token
symbol: public(String[32])
# The amount of time profits will unlock over
profit_max_unlock_time: uint256
# The timestamp of when the current unlocking period ends
full_profit_unlock_date: uint256
# The per second rate at which profit will unlcok
profit_unlocking_rate: uint256
# Last timestamp of the most recent _report() call
last_profit_update: uint256
# Last protocol fees were charged
last_report: uint256
# `nonces` track `permit` approvals with signature.
nonces: public(HashMap[address, uint256])
DOMAIN_TYPE_HASH: constant(bytes32) = keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')
PERMIT_TYPE_HASH: constant(bytes32) = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")
# Constructor
@deploy
def __init__(asset: IERC20, name: String[64], symbol: String[32], role_manager: address, profit_max_unlock_time: uint256):
"""
@notice
The constructor for the vault. Sets the asset, name, symbol, and role manager.
@param asset
The address of the asset that the vault will accept.
@param name
The name of the vault token.
@param symbol
The symbol of the vault token.
@param role_manager
The address that can add and remove roles to addresses
@param profit_max_unlock_time
The maximum amount of time that the profit can be locked for
"""
ASSET = asset
DECIMALS = convert(staticcall IERC20Detailed(asset.address).decimals(), uint256)
assert DECIMALS < 256 # dev: see VVE-2020-0001
FACTORY = msg.sender
# Must be > 0 so we can unlock shares
assert profit_max_unlock_time > 0 # dev: profit unlock time too low
# Must be less than one year for report cycles
assert profit_max_unlock_time <= 31_556_952 # dev: profit unlock time too long
self.profit_max_unlock_time = profit_max_unlock_time
self.name = name
self.symbol = symbol
self.last_report = block.timestamp
self.role_manager = role_manager
self.shutdown = False
## SHARE MANAGEMENT ##
## ERC20 ##
@internal
def _spend_allowance(owner: address, spender: address, amount: uint256):
# Unlimited approval does nothing (saves an SSTORE)
current_allowance: uint256 = self.allowance[owner][spender]
if (current_allowance < max_value(uint256)):
assert current_allowance >= amount, "insufficient allowance"
self._approve(owner, spender, current_allowance - amount)
@internal
def _transfer(sender: address, receiver: address, amount: uint256):
assert self.balance_of[sender] >= amount, "insufficient funds"
self.balance_of[sender] -= amount
self.balance_of[receiver] += amount
log Transfer(sender, receiver, amount)
@internal
def _transfer_from(sender: address, receiver: address, amount: uint256) -> bool:
self._spend_allowance(sender, msg.sender, amount)
self._transfer(sender, receiver, amount)
return True
@internal
def _approve(owner: address, spender: address, amount: uint256) -> bool:
self.allowance[owner][spender] = amount
log Approval(owner, spender, amount)
return True
@internal
def _increase_allowance(owner: address, spender: address, amount: uint256) -> bool:
self.allowance[owner][spender] += amount
log Approval(owner, spender, self.allowance[owner][spender])
return True
@internal
def _decrease_allowance(owner: address, spender: address, amount: uint256) -> bool:
self.allowance[owner][spender] -= amount
log Approval(owner, spender, self.allowance[owner][spender])
return True
@internal
def _permit(owner: address, spender: address, amount: uint256, deadline: uint256, v: uint8, r: bytes32, s: bytes32) -> bool:
assert owner != empty(address), "invalid owner"
assert deadline >= block.timestamp, "permit expired"
nonce: uint256 = self.nonces[owner]
digest: bytes32 = keccak256(
concat(
b'\x19\x01',
self.domain_separator(),
keccak256(
concat(
PERMIT_TYPE_HASH,
convert(owner, bytes32),
convert(spender, bytes32),
convert(amount, bytes32),
convert(nonce, bytes32),
convert(deadline, bytes32),
)
)
)
)
assert ecrecover(digest, convert(v, uint256), convert(r, uint256), convert(s, uint256)) == owner, "invalid signature"
self.allowance[owner][spender] = amount
self.nonces[owner] = nonce + 1
log Approval(owner, spender, amount)
return True
@internal
def _burn_shares(shares: uint256, owner: address):
self.balance_of[owner] -= shares
self.total_supply -= shares
log Transfer(owner, empty(address), shares)
@view
@internal
def _unlocked_shares() -> uint256:
# To avoid sudden price_per_share spikes, profit must be processed through an unlocking period.
# The mechanism involves shares to be minted to the vault which are unlocked gradually over time.
# Shares that have been locked are gradually unlocked over profit_max_unlock_time seconds
_full_profit_unlock_date: uint256 = self.full_profit_unlock_date
unlocked_shares: uint256 = 0
if _full_profit_unlock_date > block.timestamp:
unlocked_shares = self.profit_unlocking_rate * (block.timestamp - self.last_profit_update) // MAX_BPS_EXTENDED
elif _full_profit_unlock_date != 0:
# All shares have been unlocked
unlocked_shares = self.balance_of[self]
return unlocked_shares
@view
@internal
def _total_supply() -> uint256:
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
"""
unlocked_shares: uint256 = self._unlocked_shares()
if unlocked_shares == 0:
return
# Only do an SSTORE if necessary
if self.full_profit_unlock_date > block.timestamp:
self.last_profit_update = block.timestamp
self._burn_shares(unlocked_shares, self)
@view
@internal
def _total_assets() -> uint256:
"""
Total amount of assets that are in the vault and in the strategies.
"""
return self.total_idle + self.total_debt
@view
@internal
def _convert_to_assets(shares: uint256, rounding: Rounding) -> uint256:
"""
assets = shares * (total_assets / total_supply) --- (== price_per_share * shares)
"""
total_supply: uint256 = self._total_supply()
# if total_supply is 0, price_per_share is 1
if total_supply == 0:
return shares
numerator: uint256 = shares * self._total_assets()
amount: uint256 = numerator // total_supply
if rounding == Rounding.ROUND_UP and numerator % total_supply != 0:
amount += 1
return amount
@view
@internal
def _convert_to_shares(assets: uint256, rounding: Rounding) -> uint256:
"""
shares = amount * (total_supply / total_assets) --- (== amount / price_per_share)
"""
total_supply: uint256 = self._total_supply()
total_assets: uint256 = self._total_assets()
if total_assets == 0:
# if total_assets and total_supply is 0, price_per_share is 1
if total_supply == 0:
return assets
else:
# Else if total_supply > 0 price_per_share is 0
return 0
numerator: uint256 = assets * total_supply
shares: uint256 = numerator // total_assets
if rounding == Rounding.ROUND_UP and numerator % total_assets != 0:
shares += 1
return shares
@internal
def erc20_safe_approve(token: address, spender: address, amount: uint256):
# Used only to send tokens that are not the type managed by this Vault.
# HACK: Used to handle non-compliant tokens like USDT
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!"
@internal
def erc20_safe_transfer_from(token: address, sender: address, receiver: address, amount: uint256):
# Used only to send tokens that are not the type managed by this Vault.
# HACK: Used to handle non-compliant tokens like USDT
response: Bytes[32] = raw_call(
token,
concat(
method_id("transferFrom(address,address,uint256)"),
convert(sender, bytes32),
convert(receiver, bytes32),
convert(amount, bytes32),
),
max_outsize=32,
)
if len(response) > 0:
assert convert(response, bool), "Transfer failed!"
@internal
def erc20_safe_transfer(token: address, receiver: address, amount: uint256):
# Used only to send tokens that are not the type managed by this Vault.
# HACK: Used to handle non-compliant tokens like USDT
response: Bytes[32] = raw_call(
token,
concat(
method_id("transfer(address,uint256)"),
convert(receiver, bytes32),
convert(amount, bytes32),
),
max_outsize=32,
)
if len(response) > 0:
assert convert(response, bool), "Transfer failed!"
@internal
def _issue_shares(shares: uint256, recipient: address):
self.balance_of[recipient] += shares
self.total_supply += shares
log Transfer(empty(address), recipient, shares)
@internal
def _issue_shares_for_amount(amount: uint256, recipient: address) -> uint256:
"""
Issues shares that are worth 'amount' in the underlying token (asset)
WARNING: this takes into account that any new assets have been summed
to total_assets (otherwise pps will go down)
"""
total_supply: uint256 = self._total_supply()
total_assets: uint256 = self._total_assets()
new_shares: uint256 = 0
if total_supply == 0:
new_shares = amount
elif total_assets > amount:
new_shares = amount * total_supply // (total_assets - amount)
else:
# If total_supply > 0 but amount = totalAssets we want to revert because
# after first deposit, getting here would mean that the rest of the shares
# would be diluted to a price_per_share of 0. Issuing shares would then mean
# either the new depositer or the previous depositers will loose money.
assert total_assets > amount, "amount too high"
# We don't make the function revert
if new_shares == 0:
return 0
self._issue_shares(new_shares, recipient)
return new_shares
## ERC4626 ##
@view
@internal
def _max_deposit(receiver: address) -> uint256:
_total_assets: uint256 = self._total_assets()
_deposit_limit: uint256 = self.deposit_limit
if (_total_assets >= _deposit_limit):
return 0
return _deposit_limit - _total_assets
@view
@internal
def _max_redeem(owner: address) -> uint256:
if self.queue_manager != empty(address):
# if a queue_manager is set we assume full redeems are possible
return self.balance_of[owner]
else:
# NOTE: this will return the max amount that is available to redeem using ERC4626
# (which can only withdraw from the vault contract)
return min(self.balance_of[owner], self._convert_to_shares(self.total_idle, Rounding.ROUND_DOWN))
@view
@internal
def _max_withdraw(owner: address) -> uint256:
if self.queue_manager != empty(address):
# if a queue_manager is set we assume full withdraws are possible
return self._convert_to_assets(self.balance_of[owner], Rounding.ROUND_DOWN)
else:
# NOTE: this will return the max amount that is available to withdraw using ERC4626
# (which can only withdraw from the vault contract)
return min(self._convert_to_assets(self.balance_of[owner], Rounding.ROUND_DOWN), self.total_idle)
@internal
def _deposit(sender: address, recipient: address, assets: uint256) -> uint256:
assert self.shutdown == False # dev: shutdown
assert recipient not in [self, empty(address)], "invalid recipient"
assert self._total_assets() + assets <= self.deposit_limit, "exceed deposit limit"
self.erc20_safe_transfer_from(ASSET.address, msg.sender, self, assets)
self.total_idle += assets
shares: uint256 = self._issue_shares_for_amount(assets, recipient)
assert shares > 0, "cannot mint zero"
log Deposit(sender, recipient, assets, shares)
return shares
@view
@internal
def _assess_share_of_unrealised_losses(strategy: address, assets_needed: uint256) -> uint256:
"""
Returns the share of losses that a user would take if withdrawing from this strategy
e.g. if the strategy has unrealised losses for 10% of its current debt and the user
wants to withdraw 1000 tokens, the losses that he will take are 100 token
"""
strategy_current_debt: uint256 = self.strategies[strategy].current_debt
vault_shares: uint256 = staticcall IStrategy(strategy).balanceOf(self)
strategy_assets: uint256 = staticcall IStrategy(strategy).convertToAssets(vault_shares)
# If no losses, return 0
if strategy_assets >= strategy_current_debt or strategy_current_debt == 0:
return 0
# user will withdraw assets_to_withdraw divided by loss ratio (strategy_assets / strategy_current_debt - 1)
# but will only receive assets_to_withdraw
# NOTE: if there are unrealised losses, the user will take his share
losses_user_share: uint256 = assets_needed - assets_needed * strategy_assets // strategy_current_debt
return losses_user_share
@internal
def _redeem(sender: address, receiver: address, owner: address, shares_to_burn: uint256, strategies: DynArray[address, 10]) -> uint256:
shares: uint256 = shares_to_burn
shares_balance: uint256 = self.balance_of[owner]
assert shares > 0, "no shares to redeem"
assert shares_balance >= shares, "insufficient shares to redeem"
if sender != owner:
self._spend_allowance(owner, sender, shares_to_burn)
requested_assets: uint256 = self._convert_to_assets(shares, Rounding.ROUND_DOWN)
# load to memory to save gas
curr_total_idle: uint256 = self.total_idle
# If there are not enough assets in the Vault contract, we try to free funds from strategies specified in the input
if requested_assets > curr_total_idle:
_strategies: DynArray[address, 10] = strategies
queue_manager: address = self.queue_manager
if queue_manager != empty(address):
if len(_strategies) == 0:
_strategies = extcall IQueueManager(queue_manager).withdraw_queue(self)
# load to memory to save gas
curr_total_debt: uint256 = self.total_debt
# Withdraw from strategies if insufficient total idle
assets_needed: uint256 = requested_assets - curr_total_idle
assets_to_withdraw: uint256 = 0
# NOTE: to compare against real withdrawals from strategies
previous_balance: uint256 = staticcall ASSET.balanceOf(self)
for strategy: address in _strategies:
assert self.strategies[strategy].activation != 0, "inactive strategy"
current_debt: uint256 = self.strategies[strategy].current_debt
# What is the max amount to withdraw from this strategy.
assets_to_withdraw = min(assets_needed, current_debt)
# Cache max_withdraw for use if unrealized loss > 0
max_withdraw: uint256 = staticcall IStrategy(strategy).maxWithdraw(self)
# CHECK FOR UNREALISED LOSSES
# If unrealised losses > 0, then the user will take the proportional share and realize it (required to avoid users withdrawing from lossy strategies)
# NOTE: strategies need to manage the fact that realising part of the loss can mean the realisation of 100% of the loss !!
# (i.e. if for withdrawing 10% of the strategy it needs to unwind the whole position, generated losses might be bigger)
unrealised_losses_share: uint256 = self._assess_share_of_unrealised_losses(strategy, assets_to_withdraw)
if unrealised_losses_share > 0:
# If max withdraw is limiting the amount to pull, we need to adjust the portion of
# the unrealized loss the user should take.
if max_withdraw < assets_to_withdraw - unrealised_losses_share:
# How much would we want to withdraw
wanted: uint256 = assets_to_withdraw - unrealised_losses_share
# Get the proportion of unrealised comparing what we want vs. what we can get
unrealised_losses_share = unrealised_losses_share * max_withdraw // wanted
# Adjust assets_to_withdraw so all future calcultations work correctly
assets_to_withdraw = max_withdraw + unrealised_losses_share
# User now "needs" less assets to be unlocked (as he took some as losses)
assets_to_withdraw -= unrealised_losses_share
requested_assets -= unrealised_losses_share
# NOTE: done here instead of waiting for regular update of these values
# because it's a rare case (so we can save minor amounts of gas)
assets_needed -= unrealised_losses_share
curr_total_debt -= unrealised_losses_share
# If max withdraw is 0 and unrealised loss is still > 0 then the strategy likely realized
# a 100% loss and we will need to realize that loss before moving on.
if max_withdraw == 0 and unrealised_losses_share > 0:
new_debt: uint256 = current_debt - unrealised_losses_share
# Update strategies storage
self.strategies[strategy].current_debt = new_debt
# Log the debt update
log DebtUpdated(strategy, current_debt, new_debt)
# Adjust based on the max withdraw of the strategy
assets_to_withdraw = min(assets_to_withdraw, max_withdraw)
# Can't withdraw 0.
if assets_to_withdraw == 0:
continue
# WITHDRAW FROM STRATEGY
extcall IStrategy(strategy).withdraw(assets_to_withdraw, self, self)
post_balance: uint256 = staticcall ASSET.balanceOf(self)
# If we have not received what we expected, we consider the difference a loss
loss: uint256 = 0
if(previous_balance + assets_to_withdraw > post_balance):
loss = previous_balance + assets_to_withdraw - post_balance
# NOTE: strategy's debt decreases by the full amount but the total idle increases
# by the actual amount only (as the difference is considered lost)
curr_total_idle += (assets_to_withdraw - loss)
requested_assets -= loss
curr_total_debt -= assets_to_withdraw
# Vault will reduce debt because the unrealised loss has been taken by user
new_debt: uint256 = current_debt - (assets_to_withdraw + unrealised_losses_share)
# Update strategies storage
self.strategies[strategy].current_debt = new_debt
# Log the debt update
log DebtUpdated(strategy, current_debt, new_debt)
# NOTE: the user will receive less tokens (the rest were lost)
# break if we have enough total idle to serve initial request
if requested_assets <= curr_total_idle:
break
# NOTE: we update the previous_balance variable here to save gas in next iteration
previous_balance = post_balance
# Reduce what we still need.
assets_needed -= assets_to_withdraw
# if we exhaust the queue and still have insufficient total idle, revert
assert curr_total_idle >= requested_assets, "insufficient assets in vault"
# commit memory to storage
self.total_debt = curr_total_debt
self._burn_shares(shares, owner)
# commit memory to storage
self.total_idle = curr_total_idle - requested_assets
self.erc20_safe_transfer(ASSET.address, receiver, requested_assets)
log Withdraw(sender, receiver, owner, requested_assets, shares)
return requested_assets
## STRATEGY MANAGEMENT ##
@internal
def _add_strategy(new_strategy: address):
assert new_strategy not in [self, empty(address)], "strategy cannot be zero address"
assert staticcall IStrategy(new_strategy).asset() == ASSET.address, "invalid asset"
assert self.strategies[new_strategy].activation == 0, "strategy already active"
self.strategies[new_strategy] = StrategyParams(
activation=block.timestamp,
last_report=block.timestamp,
current_debt=0,
max_debt=0,
)
# we cache queue_manager since expected behavior is it being set
queue_manager: address = self.queue_manager
if queue_manager != empty(address):
# tell the queue_manager we have a new strategy
extcall IQueueManager(queue_manager).new_strategy(new_strategy)
log StrategyChanged(new_strategy, StrategyChangeType.ADDED)
@internal
def _revoke_strategy(strategy: address, force: bool=False):
assert self.strategies[strategy].activation != 0, "strategy not active"
loss: uint256 = 0
if self.strategies[strategy].current_debt != 0:
assert force, "strategy has debt"
loss = self.strategies[strategy].current_debt
self.total_debt -= loss
log StrategyReported(strategy, 0, loss, 0, 0, 0, 0)
# NOTE: strategy params are set to 0 (WARNING: it can be readded)
self.strategies[strategy] = StrategyParams(
activation=0,
last_report=0,
current_debt=0,
max_debt=0,
)
# we cache queue_manager since expected behavior is it being set
queue_manager: address = self.queue_manager
if queue_manager != empty(address):
# tell the queue_manager we removed a strategy
extcall IQueueManager(queue_manager).remove_strategy(strategy)
log StrategyChanged(strategy, StrategyChangeType.REVOKED)
# DEBT MANAGEMENT #
@internal
def _update_debt(strategy: address, target_debt: uint256) -> uint256:
"""
The vault will rebalance the debt vs target debt. Target debt must be smaller or equal to strategy's max_debt.
This function will compare the current debt with the target debt and will take funds or deposit new
funds to the strategy.
The strategy can require a maximum amount of funds that it wants to receive to invest.
The strategy can also reject freeing funds if they are locked.
The vault will not invest the funds into the underlying protocol, which is responsibility of the strategy.
"""
new_debt: uint256 = target_debt
current_debt: uint256 = self.strategies[strategy].current_debt
if self.shutdown:
new_debt = 0
assert new_debt != current_debt, "new debt equals current debt"
if current_debt > new_debt:
# reduce debt
assets_to_withdraw: uint256 = current_debt - new_debt
# ensure we always have minimum_total_idle when updating debt
minimum_total_idle: uint256 = self.minimum_total_idle
total_idle: uint256 = self.total_idle
# Respect minimum total idle in vault
if total_idle + assets_to_withdraw < minimum_total_idle:
assets_to_withdraw = minimum_total_idle - total_idle
if assets_to_withdraw > current_debt:
assets_to_withdraw = current_debt
withdrawable: uint256 = staticcall IStrategy(strategy).maxWithdraw(self)
assert withdrawable != 0, "nothing to withdraw"
# if insufficient withdrawable, withdraw what we can
if withdrawable < assets_to_withdraw:
assets_to_withdraw = withdrawable
# If there are unrealised losses we don't let the vault reduce its debt until there is a new report
unrealised_losses_share: uint256 = self._assess_share_of_unrealised_losses(strategy, assets_to_withdraw)
assert unrealised_losses_share == 0, "strategy has unrealised losses"
pre_balance: uint256 = staticcall ASSET.balanceOf(self)
extcall IStrategy(strategy).withdraw(assets_to_withdraw, self, self)
post_balance: uint256 = staticcall ASSET.balanceOf(self)
# making sure we are changing according to the real result no matter what. This will spend more gas but makes it more robust
# also prevents issues from faulty strategy that either under or over delievers 'assets_to_withdraw'
assets_to_withdraw = min(post_balance - pre_balance, current_debt)
self.total_idle += assets_to_withdraw
self.total_debt -= assets_to_withdraw
new_debt = current_debt - assets_to_withdraw
else:
# Revert if target_debt cannot be achieved due to configured max_debt for given strategy
assert new_debt <= self.strategies[strategy].max_debt, "target debt higher than max debt"
# Vault is increasing debt with the strategy by sending more funds
max_deposit: uint256 = staticcall IStrategy(strategy).maxDeposit(self)
assert max_deposit != 0, "nothing to deposit"
assets_to_deposit: uint256 = new_debt - current_debt
if assets_to_deposit > max_deposit:
assets_to_deposit = max_deposit
# take into consideration minimum_total_idle
minimum_total_idle: uint256 = self.minimum_total_idle
total_idle: uint256 = self.total_idle
assert total_idle > minimum_total_idle, "no funds to deposit"
available_idle: uint256 = total_idle - minimum_total_idle
# if insufficient funds to deposit, transfer only what is free
if assets_to_deposit > available_idle:
assets_to_deposit = available_idle
if assets_to_deposit > 0:
self.erc20_safe_approve(ASSET.address, strategy, assets_to_deposit)
pre_balance: uint256 = staticcall ASSET.balanceOf(self)
extcall IStrategy(strategy).deposit(assets_to_deposit, self)
post_balance: uint256 = staticcall ASSET.balanceOf(self)
self.erc20_safe_approve(ASSET.address, strategy, 0)
# making sure we are changing according to the real result no matter what.
# This will spend more gas but makes it more robust
assets_to_deposit = pre_balance - post_balance
self.total_idle -= assets_to_deposit
self.total_debt += assets_to_deposit
new_debt = current_debt + assets_to_deposit
# commit memory to storage
self.strategies[strategy].current_debt = new_debt
log DebtUpdated(strategy, current_debt, new_debt)
return new_debt
@internal
def _assess_protocol_fees() -> (uint256, address):
protocol_fees: uint256 = 0
protocol_fee_recipient: address = empty(address)
seconds_since_last_report: uint256 = block.timestamp - self.last_report
# to avoid wasting gas for minimal fees vault will only assess once every PROTOCOL_FEE_ASSESSMENT_PERIOD seconds
if(seconds_since_last_report >= PROTOCOL_FEE_ASSESSMENT_PERIOD):
protocol_fee_bps: uint16 = 0
protocol_fee_last_change: uint32 = 0
protocol_fee_bps, protocol_fee_last_change, protocol_fee_recipient = staticcall IFactory(FACTORY).protocol_fee_config()
if(protocol_fee_bps > 0):
# NOTE: charge fees since last report OR last fee change (this will mean less fees are charged after a change in protocol_fees, but fees should not change frequently)
seconds_since_last_report = min(seconds_since_last_report, block.timestamp - convert(protocol_fee_last_change, uint256))
# fees = total_assets * protocol fees bpbs * time elapsed / seconds per year / max bps
protocol_fees = self._total_assets() * convert(protocol_fee_bps, uint256) * seconds_since_last_report // 31_556_952 // MAX_BPS
self.last_report = block.timestamp
return (protocol_fees, protocol_fee_recipient)
## ACCOUNTING MANAGEMENT ##
@internal
def _process_report(strategy: address) -> (uint256, uint256):
"""
Processing a report means comparing the debt that the strategy has taken with the current amount of funds it is reporting
If the strategy owes less than it currently has, it means it has had a profit
Else (assets < debt) it has had a loss
Different strategies might choose different reporting strategies: pessimistic, only realised P&L, ...
The best way to report depends on the strategy
The profit will be distributed following a smooth curve over the next profit_max_unlock_time seconds.
Losses will be taken immediately, first from the profit buffer (avoiding an impact in pps), then will reduce pps
"""
assert self.strategies[strategy].activation != 0, "inactive strategy"
# Vault needs to assess
# Using strategy shares because some may be a ERC4626 vault
strategy_shares: uint256 = staticcall IStrategy(strategy).balanceOf(self)
total_assets: uint256 = staticcall IStrategy(strategy).convertToAssets(strategy_shares)
current_debt: uint256 = self.strategies[strategy].current_debt
# Burn shares that have been unlocked since the last update
self._burn_unlocked_shares()
gain: uint256 = 0
loss: uint256 = 0
if total_assets > current_debt:
gain = total_assets - current_debt
else:
loss = current_debt - total_assets
total_fees: uint256 = 0
total_refunds: uint256 = 0
accountant: address = self.accountant
# if accountant is not set, fees and refunds remain unchanged
if accountant != empty(address):
total_fees, total_refunds = extcall IAccountant(accountant).report(strategy, gain, loss)
# Protocol fee assessment
protocol_fees: uint256 = 0
protocol_fee_recipient: address = empty(address)
protocol_fees, protocol_fee_recipient = self._assess_protocol_fees()
total_fees += protocol_fees
# We calculate the amount of shares that could be insta unlocked to avoid pps changes
# NOTE: this needs to be done before any pps changes
shares_to_burn: uint256 = 0
accountant_fees_shares: uint256 = 0
protocol_fees_shares: uint256 = 0
if loss + total_fees > 0:
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 total_fees > 0:
accountant_fees_shares = self._convert_to_shares(total_fees - protocol_fees, Rounding.ROUND_DOWN)
if protocol_fees > 0:
protocol_fees_shares = self._convert_to_shares(protocol_fees, Rounding.ROUND_DOWN)
newly_locked_shares: uint256 = 0
if total_refunds > 0:
# if refunds are non-zero, transfer shares worth of assets
total_refunds_shares: uint256 = min(self._convert_to_shares(total_refunds, Rounding.ROUND_UP), self.balance_of[accountant])
# Shares received as a refund are locked to avoid sudden pps change (like profits)
self._transfer(accountant, self, total_refunds_shares)
newly_locked_shares += total_refunds_shares
if gain > 0:
# NOTE: this will increase total_assets
self.strategies[strategy].current_debt += gain
self.total_debt += gain
# NOTE: vault will issue shares worth the profit to avoid instant pps change
newly_locked_shares += self._issue_shares_for_amount(gain, self)
# Strategy is reporting a loss
if loss > 0:
self.strategies[strategy].current_debt -= loss
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 / transfered to the vault, so they need to be substracted
# 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 occured, 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:
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)
newly_locked_shares -= shares_not_to_lock
previously_locked_shares -= (shares_to_burn - shares_not_to_lock)
# issue shares that were calculated above
if accountant_fees_shares > 0:
self._issue_shares(accountant_fees_shares, accountant)
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
_profit_max_unlock_time: uint256 = self.profit_max_unlock_time
if total_locked_shares > 0:
# Calculate how long until the full amount of shares is unlocked
remaining_time: uint256 = 0
_full_profit_unlock_date: uint256 = self.full_profit_unlock_date
if _full_profit_unlock_date > block.timestamp:
remaining_time = _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_shares * remaining_time + newly_locked_shares * _profit_max_unlock_time) // total_locked_shares
self.profit_unlocking_rate = total_locked_shares * MAX_BPS_EXTENDED // new_profit_locking_period
self.full_profit_unlock_date = block.timestamp + new_profit_locking_period
self.last_profit_update = block.timestamp
else:
# NOTE: only setting this to 0 will turn in the desired effect, no need to update last_profit_update or full_profit_unlock_date
self.profit_unlocking_rate = 0
self.strategies[strategy].last_report = block.timestamp
# We have to recalculate the fees paid for cases with an overall loss
log StrategyReported(
strategy,
gain,
loss,
self.strategies[strategy].current_debt,
self._convert_to_assets(protocol_fees_shares, Rounding.ROUND_DOWN),
self._convert_to_assets(protocol_fees_shares + accountant_fees_shares, Rounding.ROUND_DOWN),
total_refunds
)
return (gain, loss)
# SETTERS #
@external
def set_accountant(new_accountant: address):
"""
@notice Set the new accountant address.
@param new_accountant The new accountant address.
"""
self._enforce_role(msg.sender, Roles.ACCOUNTANT_MANAGER)
self.accountant = new_accountant
log UpdateAccountant(new_accountant)
@external
def set_queue_manager(new_queue_manager: address):
"""
@notice Set the new queue manager address.
@param new_queue_manager The new queue manager address.
"""
self._enforce_role(msg.sender, Roles.QUEUE_MANAGER)
self.queue_manager = new_queue_manager
log UpdateQueueManager(new_queue_manager)
@external
def set_deposit_limit(deposit_limit: uint256):
"""
@notice Set the new deposit limit.
@dev can not be changed if shutdown.
@param deposit_limit The new deposit limit.
"""
assert self.shutdown == False # Dev: shutdown
self._enforce_role(msg.sender, Roles.DEPOSIT_LIMIT_MANAGER)
self.deposit_limit = deposit_limit
log UpdateDepositLimit(deposit_limit)
@external
def set_minimum_total_idle(minimum_total_idle: uint256):
"""
@notice Set the new minimum total idle.
@param minimum_total_idle The new minimum total idle.
"""
self._enforce_role(msg.sender, Roles.MINIMUM_IDLE_MANAGER)
self.minimum_total_idle = minimum_total_idle
log UpdateMinimumTotalIdle(minimum_total_idle)
@external
def set_profit_max_unlock_time(new_profit_max_unlock_time: uint256):
"""
@notice Set the new profit max unlock time.
@dev The time is denominated in seconds and must be more than 0
and less than 1 year. We don't need to update locking period
since the current period will use the old rate and on the next
report it will be reset with the new unlocking time.
@param new_profit_max_unlock_time The new profit max unlock time.
"""
self._enforce_role(msg.sender, Roles.PROFIT_UNLOCK_MANAGER)
# Must be > 0 so we can unlock shares
assert new_profit_max_unlock_time > 0, "profit unlock time too low"
# Must be less than one year for report cycles
assert new_profit_max_unlock_time <= 31_556_952, "profit unlock time too long"
self.profit_max_unlock_time = new_profit_max_unlock_time
log UpdateProfitMaxUnlockTime(new_profit_max_unlock_time)
# ROLE MANAGEMENT #
@internal
def _enforce_role(account: address, role: Roles):
assert role in self.roles[account] or self.open_roles[role], "not allowed"
@external
def set_role(account: address, role: Roles):
"""
@notice Set the role of an account.
@param account The account to set the role for.
@param role The role to set.
"""
assert msg.sender == self.role_manager
self.roles[account] = role
log RoleSet(account, role)
@external
def set_open_role(role: Roles):
"""
@notice Set the role to be open.
@param role The role to set.
"""
assert msg.sender == self.role_manager
self.open_roles[role] = True
log RoleStatusChanged(role, RoleStatusChange.OPENED)
@external
def close_open_role(role: Roles):
"""
@notice Close the role.
@param role The role to close.
"""
assert msg.sender == self.role_manager
self.open_roles[role] = False
log RoleStatusChanged(role, RoleStatusChange.CLOSED)
@external
def transfer_role_manager(role_manager: address):
"""
@notice Transfer the role manager to a new address.
@param role_manager The new role manager address.
"""
assert msg.sender == self.role_manager
self.future_role_manager = role_manager
@external
def accept_role_manager():
"""
@notice Accept the role manager transfer.
"""
assert msg.sender == self.future_role_manager
self.role_manager = msg.sender
self.future_role_manager = empty(address)
log UpdateRoleManager(msg.sender)
# VAULT STATUS VIEWS
@view
@external
def unlocked_shares() -> uint256:
"""
@notice Get the amount of shares that are not locked.
@return The amount of shares that are not locked.
"""
return self._unlocked_shares()
@view
@external
def pricePerShare() -> uint256:
"""
@notice Get the price per share.
@dev This value offers limited precision. Integrations the require
exact precision should use convertToAssets or convertToShares instead.
@return The price per share.
"""
return self._convert_to_assets(10 ** DECIMALS, Rounding.ROUND_DOWN)
@view
@external
def availableDepositLimit() -> uint256:
"""
@notice Get the available deposit limit.
@return The available deposit limit.
"""
if self.deposit_limit > self._total_assets():
return self.deposit_limit - self._total_assets()
return 0
## REPORTING MANAGEMENT ##
@external
def process_report(strategy: address) -> (uint256, uint256):
"""
@notice Process the report of a strategy.
@param strategy The strategy to process the report for.
@return The gain and loss of the strategy.
"""
self._enforce_role(msg.sender, Roles.REPORTING_MANAGER)
return self._process_report(strategy)
@external
@nonreentrant
def sweep(token: address) -> (uint256):
"""
@notice Sweep the token from airdop or sent by mistake.
@param token The token to sweep.
@return The amount of dust swept.
"""
self._enforce_role(msg.sender, Roles.SWEEPER)
assert token != self, "can't sweep self"
assert self.strategies[token].activation == 0, "can't sweep strategy"
amount: uint256 = 0
if token == ASSET.address:
amount = staticcall ASSET.balanceOf(self) - self.total_idle
else:
amount = staticcall IERC20(token).balanceOf(self)
assert amount != 0, "no dust"
self.erc20_safe_transfer(token, msg.sender, amount)
log Sweep(token, amount)
return amount
## STRATEGY MANAGEMENT ##
@external
def add_strategy(new_strategy: address):
"""
@notice Add a new strategy.
@param new_strategy The new strategy to add.
"""
self._enforce_role(msg.sender, Roles.ADD_STRATEGY_MANAGER)
self._add_strategy(new_strategy)
@external
def revoke_strategy(strategy: address):
"""
@notice Revoke a strategy.
@param strategy The strategy to revoke.
"""
self._enforce_role(msg.sender, Roles.REVOKE_STRATEGY_MANAGER)
self._revoke_strategy(strategy)
@external
def force_revoke_strategy(strategy: address):
"""
@notice Force revoke a strategy.
@param strategy The strategy to force revoke.
@dev The vault will remove the inputed strategy and write off any debt left in it as loss.
This function is a dangerous function as it can force a strategy to take a loss.
All possible assets should be removed from the strategy first via update_debt
Note that if a strategy is removed erroneously it can be re-added and the loss will be credited as profit. Fees will apply
"""
self._enforce_role(msg.sender, Roles.FORCE_REVOKE_MANAGER)
self._revoke_strategy(strategy, True)
## DEBT MANAGEMENT ##
@external
def update_max_debt_for_strategy(strategy: address, new_max_debt: uint256):
"""
@notice Update the max debt for a strategy.
@param strategy The strategy to update the max debt for.
@param new_max_debt The new max debt for the strategy.
"""
self._enforce_role(msg.sender, Roles.MAX_DEBT_MANAGER)
assert self.strategies[strategy].activation != 0, "inactive strategy"
self.strategies[strategy].max_debt = new_max_debt
log UpdatedMaxDebtForStrategy(msg.sender, strategy, new_max_debt)
@external
@nonreentrant
def update_debt(strategy: address, target_debt: uint256) -> 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.
@return The amount of debt added or removed.
"""
self._enforce_role(msg.sender, Roles.DEBT_MANAGER)
return self._update_debt(strategy, target_debt)
## EMERGENCY MANAGEMENT ##
@external
def shutdown_vault():
"""
@notice Shutdown the vault.
"""
self._enforce_role(msg.sender, Roles.EMERGENCY_MANAGER)
assert self.shutdown == False
# Shutdown the vault.
self.shutdown = True
# Set deposit limit to 0.
self.deposit_limit = 0
log UpdateDepositLimit(0)
self.roles[msg.sender] = self.roles[msg.sender] | Roles.DEBT_MANAGER
log Shutdown()
## SHARE MANAGEMENT ##
## ERC20 + ERC4626 ##
@external
@nonreentrant
def deposit(assets: uint256, receiver: address) -> uint256:
"""
@notice Deposit assets into the vault.
@param assets The amount of assets to deposit.
@param receiver The address to receive the shares.
@return The amount of shares minted.
"""
return self._deposit(msg.sender, receiver, assets)
@external
@nonreentrant
def mint(shares: uint256, receiver: address) -> uint256:
"""
@notice Mint shares for the receiver.
@param shares The amount of shares to mint.
@param receiver The address to receive the shares.
@return The amount of assets deposited.
"""
assets: uint256 = self._convert_to_assets(shares, Rounding.ROUND_UP)
self._deposit(msg.sender, receiver, assets)
return assets
@external
@nonreentrant
def withdraw(assets: uint256, receiver: address, owner: address, strategies: DynArray[address, 10] = []) -> uint256:
"""
@notice Withdraw an amount of asset to `receiver` burning `owner`s shares.
@param assets The amount of asset to withdraw.
@param receiver The address to receive the assets.
@param owner The address whos shares are being burnt.
@param strategies Optional array of strategies to withdraw from.
@return The amount of shares actually burnt.
"""
shares: uint256 = self._convert_to_shares(assets, Rounding.ROUND_UP)
self._redeem(msg.sender, receiver, owner, shares, strategies)
return shares
@external
@nonreentrant
def redeem(shares: uint256, receiver: address, owner: address, strategies: DynArray[address, 10] = []) -> uint256:
"""
@notice Redeems an amount of shares of `owners` shares sending funds to `receiver`.
@param shares The amount of shares to burn.
@param receiver The address to receive the assets.
@param owner The address whos shares are being burnt.
@param strategies Optional array of strategies to withdraw from.
@return The amount of assets actually withdrawn.
"""
assets: uint256 = self._redeem(msg.sender, receiver, owner, shares, strategies)
return assets
@external
def approve(spender: address, amount: uint256) -> bool:
"""
@notice Approve an address to spend the vault's shares.
@param spender The address to approve.
@param amount The amount of shares to approve.
@return True if the approval was successful.
"""
return self._approve(msg.sender, spender, amount)
@external
def transfer(receiver: address, amount: uint256) -> bool:
"""
@notice Transfer shares to a receiver.
@param receiver The address to transfer shares to.
@param amount The amount of shares to transfer.
@return True if the transfer was successful.
"""
assert receiver not in [self, empty(address)]
self._transfer(msg.sender, receiver, amount)
return True
@external
def transferFrom(sender: address, receiver: address, amount: uint256) -> bool:
"""
@notice Transfer shares from a sender to a receiver.
@param sender The address to transfer shares from.
@param receiver The address to transfer shares to.
@param amount The amount of shares to transfer.
@return True if the transfer was successful.
"""
assert receiver not in [self, empty(address)]
return self._transfer_from(sender, receiver, amount)
## ERC20+4626 compatibility
@external
def increaseAllowance(spender: address, amount: uint256) -> bool:
"""
@notice Increase the allowance for a spender.
@param spender The address to increase the allowance for.
@param amount The amount to increase the allowance by.
@return True if the increase was successful.
"""
return self._increase_allowance(msg.sender, spender, amount)
@external
def decreaseAllowance(spender: address, amount: uint256) -> bool:
"""
@notice Decrease the allowance for a spender.
@param spender The address to decrease the allowance for.
@param amount The amount to decrease the allowance by.
@return True if the decrease was successful.
"""
return self._decrease_allowance(msg.sender, spender, amount)
@external
def permit(owner: address, spender: address, amount: uint256, deadline: uint256, v: uint8, r: bytes32, s: bytes32) -> bool:
"""
@notice Approve an address to spend the vault's shares.
@param owner The address to approve.
@param spender The address to approve.
@param amount The amount of shares to approve.
@param deadline The deadline for the permit.
@param v The v component of the signature.
@param r The r component of the signature.
@param s The s component of the signature.
@return True if the approval was successful.
"""
return self._permit(owner, spender, amount, deadline, v, r, s)
@view
@external
def balanceOf(addr: address) -> uint256:
"""
@notice Get the balance of a user.
@param addr The address to get the balance of.
@return The balance of the user.
"""
if(addr == self):
return self.balance_of[addr] - self._unlocked_shares()
return self.balance_of[addr]
@view
@external
def totalSupply() -> uint256:
"""
@notice Get the total supply of shares.
@return The total supply of shares.
"""
return self._total_supply()
@view
@external
def asset() -> address:
"""
@notice Get the address of the asset.
@return The address of the asset.
"""
return ASSET.address
@view
@external
def decimals() -> uint8:
"""
@notice Get the number of decimals of the asset/share.
@return The number of decimals of the asset/share.
"""
return convert(DECIMALS, uint8)
@view
@external
def totalAssets() -> uint256:
"""
@notice Get the total assets held by the vault.
@return The total assets held by the vault.
"""
return self._total_assets()
@view
@external
def totalIdle() -> uint256:
"""
@notice Get the amount of loose `asset` the vault holds.
@return The current total idle.
"""
return self.total_idle
@view
@external
def totalDebt() -> uint256:
"""
@notice Get the the total amount of funds invested
across all strategies.
@return The current total debt.
"""
return self.total_debt
@view
@external
def convertToShares(assets: uint256) -> uint256:
"""
@notice Convert an amount of assets to shares.
@param assets The amount of assets to convert.
@return The amount of shares.
"""
return self._convert_to_shares(assets, Rounding.ROUND_DOWN)
@view
@external
def previewDeposit(assets: uint256) -> uint256:
"""
@notice Preview the amount of shares that would be minted for a deposit.
@param assets The amount of assets to deposit.
@return The amount of shares that would be minted.
"""
return self._convert_to_shares(assets, Rounding.ROUND_DOWN)
@view
@external
def previewMint(shares: uint256) -> uint256:
"""
@notice Preview the amount of assets that would be deposited for a mint.
@param shares The amount of shares to mint.
@return The amount of assets that would be deposited.
"""
return self._convert_to_assets(shares, Rounding.ROUND_UP)
@view
@external
def convertToAssets(shares: uint256) -> uint256:
"""
@notice Convert an amount of shares to assets.
@param shares The amount of shares to convert.
@return The amount of assets.
"""
return self._convert_to_assets(shares, Rounding.ROUND_DOWN)
@view
@external
def maxDeposit(receiver: address) -> uint256:
"""
@notice Get the maximum amount of assets that can be deposited.
@param receiver The address that will receive the shares.
@return The maximum amount of assets that can be deposited.
"""
return self._max_deposit(receiver)
@view
@external
def maxMint(receiver: address) -> uint256:
"""
@notice Get the maximum amount of shares that can be minted.
@param receiver The address that will receive the shares.
@return The maximum amount of shares that can be minted.
"""
max_deposit: uint256 = self._max_deposit(receiver)
return self._convert_to_shares(max_deposit, Rounding.ROUND_DOWN)
@view
@external
def maxWithdraw(owner: address) -> uint256:
"""
@notice Get the maximum amount of assets that can be withdrawn.
@param owner The address that owns the shares.
@return The maximum amount of assets that can be withdrawn.
"""
# NOTE: if a queue_manager is not set a withdraw function that complies with ERC4626 won't withdraw from strategies,
# so this will just uses liquidity available in the vault contract
return self._max_withdraw(owner)
@view
@external
def maxRedeem(owner: address) -> uint256:
"""
@notice Get the maximum amount of shares that can be redeemed.
@param owner The address that owns the shares.
@return The maximum amount of shares that can be redeemed.
"""
# NOTE: if a queue_manager is not set a redeem function that complies with ERC4626 won't withdraw from strategies,
# so this will just uses liquidity available in the vault contract
return self._max_redeem(owner)
@view
@external
def previewWithdraw(assets: uint256) -> uint256:
"""
@notice Preview the amount of shares that would be redeemed for a withdraw.
@param assets The amount of assets to withdraw.
@return The amount of shares that would be redeemed.
"""
return self._convert_to_shares(assets, Rounding.ROUND_UP)
@view
@external
def previewRedeem(shares: uint256) -> uint256:
"""
@notice Preview the amount of assets that would be withdrawn for a redeem.
@param shares The amount of shares to redeem.
@return The amount of assets that would be withdrawn.
"""
return self._convert_to_assets(shares, Rounding.ROUND_DOWN)
@view
@external
def api_version() -> String[28]:
"""
@notice Get the API version of the vault.
@return The API version of the vault.
"""
return API_VERSION
@view
@external
def assess_share_of_unrealised_losses(strategy: address, assets_needed: uint256) -> uint256:
"""
@notice Assess the share of unrealised losses that a strategy has.
@param strategy The address of the strategy.
@param assets_needed The amount of assets needed to be withdrawn.
@return The share of unrealised losses that the strategy has.
"""
assert self.strategies[strategy].current_debt >= assets_needed
return self._assess_share_of_unrealised_losses(strategy, assets_needed)
## Profit locking getter functions ##
@view
@external
def profitMaxUnlockTime() -> uint256:
"""
@notice Gets the current time profits are set to unlock over.
@return The current profit max unlock time.
"""
return self.profit_max_unlock_time
@view
@external
def fullProfitUnlockDate() -> uint256:
"""
@notice Gets the timestamp at which all profits will be unlocked.
@return The full profit unlocking timestamp
"""
return self.full_profit_unlock_date
@view
@external
def profitUnlockingRate() -> uint256:
"""
@notice The per second rate at which profits are unlocking.
@dev This is denominated in EXTENDED_BPS decimals.
@return The current profit unlocking rate.
"""
return self.profit_unlocking_rate
@view
@external
def lastReport() -> uint256:
"""
@notice The timestamp of the last time protocol fees were charged.
@return The last report.
"""
return self.last_report
# eip-1344
@view
@internal
def domain_separator() -> bytes32:
return keccak256(
concat(
DOMAIN_TYPE_HASH,
keccak256(convert("Yearn Vault", Bytes[11])),
keccak256(convert(API_VERSION, Bytes[28])),
convert(chain.id, bytes32),
convert(self, bytes32)
)
)
@view
@external
def DOMAIN_SEPARATOR() -> bytes32:
"""
@notice Get the domain separator.
@return The domain separator.
"""
return self.domain_separator()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment