diff --git a/docs/developers/v3/overview.md b/docs/developers/v3/overview.md index 2f762d0518..c56c9d6b05 100644 --- a/docs/developers/v3/overview.md +++ b/docs/developers/v3/overview.md @@ -14,33 +14,31 @@ So whether you are a full-blown gas golfing expert, a degen looking to codify yo ## Definitions -- [Vault](https://github.com/yearn/yearn-vaults-v3/blob/master/contracts/VaultV3.vy): A vault or "Meta Vault" in V3 refers to an ERC-4626 compliant contract that takes in user deposits, mints shares corresponding to the user's share of the underlying assets held in that vault, and then allocates the underlying asset to a range of different "strategies" that earn yield on that asset. +- [Vault](https://github.com/yearn/yearn-vaults-v3/blob/master/contracts/VaultV3.vy): A vault or "Allocator Vault" in V3 refers to an ERC-4626 compliant contract that takes in user deposits, mints shares corresponding to the user's share of the underlying assets held in that vault, and then allocates the underlying asset to a range of different "strategies" that earn yield on that asset. - Strategy: A strategy in V3 refers to a yield-generating contract added to a vault that has the needed [ERC-4626 interface](https://github.com/yearn/yearn-vaults-v3/blob/master/contracts/VaultV3.vy#L39). The strategy takes the underlying asset and deploys it to a single source, generating yield on that asset. -- [TokenizedStrategy](https://github.com/yearn/tokenized-strategy/blob/master/src/TokenizedStrategy.sol#L14-L26) A technical implementation of a Strategy that is also a stand-alone ERC4626 compliant Vault. These are the yield generators in the V3 ecosystem. This pattern can be used so that either Meta Vaults or individual users can deposit directly into and receive shares in return. -- [Vault Factory](https://github.com/yearn/yearn-vaults-v3/blob/master/contracts/VaultFactory.vy): A factory contract deployed by Yearn Governance that all Vaults of a specific version can be easily and trustlessly deployed from. +- [TokenizedStrategy](https://github.com/yearn/tokenized-strategy/blob/master/src/TokenizedStrategy.sol) A technical implementation of a Strategy that is also a stand-alone ERC4626 compliant Vault. These are the yield generators in the V3 ecosystem. This pattern can be used so that either Allocator Vaults or individual users can deposit directly into and receive shares in return. +- [Vault Factory](https://github.com/yearn/yearn-vaults-v3/blob/master/contracts/VaultFactory.vy): A factory contract deployed by Yearn Governance that all Allocator Vaults of a specific version can be easily and trustlessly deployed from. ## Get started -- [Building your own V3 Strategy](https://docs.yearn.fi/developers/v3/strategy_development) +- [Building your own V3 Strategy](https://docs.yearn.fi/developers/v3/strategy_writing_guide) - [Deploying and managing a V3 Vault](https://docs.yearn.fi/developers/v3/vault_management) - [Protocol Fees](https://docs.yearn.fi/developers/v3/protocol_fees) ## Contract Addresses +*Deployments are done using create2 factories and should be stable across all EVM chains the protocol has been deployed on. + ### Core -#### VERSION 3.0.0: +#### VERSION 3.0.1: -- Vault BluePrint: `0xfC49ca826f8C68c0345410fcA0c7d1e0550d9ee9` -- VaultFactory: `0xD1736eBbdefae37503F3eD8D718b61a494F24c1D` -- TokenizedStrategy: `0xAE69a93945133c00B9985D9361A1cd882d107622` +- Vault BluePrint: `0xDE992C652b266AE649FEC8048aFC35954Bee6145` +- VaultFactory: `0xE9E8C89c8Fc7E8b8F23425688eb68987231178e5` +- TokenizedStrategy: `0xDFC8cD9F2f2d306b7C0d109F005DF661E14f4ff2` -### Periphery + - Periphery Address Provider: `0xB1662c1E500610F5D14B8041FD5306bbD3D8EdEe` - - Address Provider: `` - - Router: `0x1112dbCF805682e828606f74AB717abf4b4FD8DE` - - Release Registry: `0x5a6E1eCC767d949D6da74e76b05DBB4870488ef6` - - Registry Factory: `0xe0aFFaC3D8e7CBac1e73FCBD0281C3FD2B2cC2a5` - - Common Report Trigger: `0x4D25b3aed34eC1222846F6C87e2ac4A73f4ab6b6` +**If a contract has not been deployed on a specific chain it can be done permissionlessly using the scripts in the relevant GitHub repo. Or reach out to a Yearn contributor for help.** \ No newline at end of file diff --git a/docs/developers/v3/protocol_fees.md b/docs/developers/v3/protocol_fees.md index 4e84dd812a..5567dd1a30 100644 --- a/docs/developers/v3/protocol_fees.md +++ b/docs/developers/v3/protocol_fees.md @@ -2,23 +2,25 @@ The V3 system sees the introduction of "Protocol Fees" to the stack: a percentage charged each time a V3 vault or strategy "reports". +Protocol fees give the managers of vaults and strategies complete control over the fees charged while rewarding Yearn for supplying the infrastructure those vaults are built on. + +Yearn Governance dictates the amount of the Protocol fee and can be set anywhere between 0 - 50%. Yearn governance also holds the ability to set custom protocol fees for individual vaults and strategies. Allowing full customization of the system. + EXAMPLE: profit = 100 - performance_fee = 10% + performance_fee = 20% protocol_fee = 10% - total_fees = profit * performance_fee = 10 - protocol_fees = total_fees * protocol_fee = 1 - performance_fees = total_fees - protocol_fees = 9 + total_fees = profit * performance_fee = 20 + protocol_fees = total_fees * protocol_fee = 2 + performance_fees = total_fees - protocol_fees = 18 - 9 would get paid to the vault specified as the performance_fee_recipient. - 1 would get paid to the Yearn Treasury. + 18 would get paid to the vault managers performance_fee_recipient. + 2 would get paid to the Yearn Treasury. -Protocol fees give the managers of vaults and strategies complete control over the fees charged while rewarding Yearn for supplying the infrastructure those vaults are built on. -Yearn Governance dictates the amount of the Protocol fee and can be set anywhere between 0 - 50%. Yearn governance also holds the ability to set custom protocol fees for individual vaults and strategies. Allowing full customization of the system. You can retrieve both the default protocol fee as well as if a custom config has been set for a specific vault or strategy using the Vault Factory that corresponds to that vault's API. @@ -30,4 +32,4 @@ You can retrieve both the default protocol fee as well as if a custom config has # Get a custom config if applicable. vaultFactory.custom_protocol_fee(vault_address) - + \ No newline at end of file diff --git a/docs/developers/v3/strategy_development.md b/docs/developers/v3/strategy_development.md deleted file mode 100644 index b27ad1f1ec..0000000000 --- a/docs/developers/v3/strategy_development.md +++ /dev/null @@ -1,441 +0,0 @@ -# Guide to creating V3 "Tokenized Strategies" - -## Overview - -So you wanna be a Yearn strategist? - -Can you write 3 lines of code? - -Well, then you came to the right place. - -The strategy creation process has been very simplified in V3, so it is technically possible to write a Tokenized Strategy with only 3 lines of code. However, most strategies will need more than that. This will guide you through the entire strategy development process from getting your local repo set up through deployment and automation to cashing out those sweet sweet strategist fees. - -## Introduction - -A strategy's job is to hold the logic responsible for generating yield on some underlying asset. It should hold the code to integrate with an external protocol and do any needed asset deployment, withdraw logic, reward selling, reinvesting LTV maintenance etc. to generate the most possible yield from that particular source most efficiently. - -The most significant update to Yearn V3 from V2 was the introduction of "Tokenized Strategies". In V2 strategies were stand-alone contracts that were attached to one specific vault. That vault was the only one who could ever deposit or withdraw funds from the strategy and each had a trusted relationship with the other. - -In V3 strategies are now fully ERC-4626-compliant stand-alone vaults. Though their job remains the same (i.e., generate yield from one external source), strategies can now be connected to many different vaults simultaneously and deposited directly into by an end user. - -This increased functionality not only means strategies have a much larger potential market of depositors but also means anyone (including you) now can build, deploy, market, and maintain your own Yearn strategy without any need for an endorsement or permission from Yearn. - -## Why? - -- *Strategist Fees!* - V3 brings back the ability for developers of strategies to earn the fees generated from their strategy. Meaning your earning potential is unlimited. -- *Codify Your Yield Farming* - Tokenized strategies make it easy for anyone to codify their yield-generating strategies. Wanna keep your alpha private? No problem they come fully customizable to allow for you to be the only one allowed to deposit. -- *Simple 4626 Wrappers* - Tokenized Strategies make a super easy and cheap way to give any previously deployed protocol an ERC-4626 interface. This opens up any protocol to easily integrate into the rapidly growing 4626 ecosystem (including Yearn Meta Vaults). - -## Definitions -- [Strategy](https://github.com/yearn/tokenized-strategy): A strategy or "Tokenized Strategy" in V3 refers to an ERC-4626 compliant contract that utilizes the [TokenizedStrategy](https://github.com/yearn/tokenized-strategy/blob/master/src/TokenizedStrategy.sol#L14-L26) pattern that either meta vaults or individual users can deposit directly into and receive shares in return. The strategy takes the underlying asset and deploys it in a single source in order to generate yield on that asset. -- Asset: Any ERC20-compliant token -- Shares: ERC20-compliant token that tracks the asset balance in the strategy for every depositor. -- [TokenizedStrategy.sol](https://github.com/yearn/tokenized-strategy/blob/master/src/TokenizedStrategy.sol): The implementation contract that all strategies delegateCall to for the standard ERC4626 and profit locking functions. -- [BaseStrategy.sol](https://github.com/yearn/tokenized-strategy/blob/master/src/BaseStrategy.sol): The abstract contract that a strategy should inherit from that handles all communication with the Tokenized Strategy contract. -- Strategist: The developer of a specific strategy. -- Depositor: Account that deposits the asset and holds Shares -- Vault: Or "Meta Vault" is a Yearn ERC4626 compliant Smart contract that receives assets from Depositors to distribute among the different Strategies added to the vault, managing accounting and asset distribution. -- Management: The owner of the specific strategy that can set fees, profit unlocking time etc. -- Keeper: the address of a contract allowed to call report() and tend() on a strategy. -- Factory: The factory that all meta vaults of a specific API version are cloned from that also controls the protocol fee amount and recipient for a strategy. -- Performance Fee: The fee strategies charge during reports based on the yield earned since the last report. -- Performance Fee recipient: The address that receives the shares charged as performance fees. -- Protocol Fee: A fee on the fees charged by strategist sent to the Yearn Treasury. -- Profit Max Unlock Time: Time in seconds over which reported profits will unlock over. -- `totalIdle`: The amount of loose asset sitting in a strategy. -- `totalDebt`: The amount of deployed funds that a strategy has control over. -- `report`: Called by management or keepers to accrue all profits or losses, charge fees, and lock profit to be distributed. -- `tend`: Called by management or keepers between reports for any maintenance that should happen that doesn't require a full report. -- API Version: The version that a specific Strategy is using for its logic. - -## Architecture - -While the complete architecture of the Tokenized Strategy is out of the scope of this document you can read more about how it works [here](https://github.com/yearn/tokenized-strategy/blob/master/SPECIFICATION.md) - -**TLDR**: V3 strategies use an immutable proxy pattern to outsource all of its complex, high risk, and redundant code to one [TokenizedStrategy.sol](https://github.com/yearn/tokenized-strategy/blob/master/src/TokenizedStrategy.sol) implementation contract that is used by every strategy of a specific API version. To use this pattern you simply need to inherit the [BaseStrategy.sol](https://github.com/yearn/tokenized-strategy/blob/master/src/BaseStrategy.sol) contract, that holds all of the logic to communicate with the implementation contract, and then just override a few simple functions with your specific strategy logic. - -## Getting started - -Yearn has base templates made to build off of built-in both [Ape Worx](https://www.apeworx.io/), a python-based development toolkit, and [Foundry](https://book.getfoundry.sh/). - -1. Choose your development framework. - - [Tokenized Strategy Foundry Mix](https://github.com/yearn/tokenized-strategy-foundry-mix) - - [Tokenized Strategy Ape Mix](https://github.com/yearn/tokenized-strategy-ape-mix) - -2. Set up the local environment with the selected mix. Each mix has detailed instructions in the "How To Start" section of the README, of specific requirements, cloning instructions, and needed environment variables. -4. Assure tests pass. Each mix comes with a small set of pre-written tests, to serve as examples and can be used to ensure your local repository is set up correctly before adding your logic. - -## Strategy Writing - -So you have your idea and local environment setup. Now it's time to start writing your actual strategy. - -To create your Tokenized Strategy, you must override at least three functions outlined in the `Strategy.sol`. - -___ - -1. *_deployFunds(uint256 _amount)* - **Purpose**: - - This function is called during every deposit into your strategy to allow it to deploy the underlying asset deposited into the yield source. - - **Parameters**: - - `_amount`: The total amount of underlying assets currently available for the deployment strategy, including the amount deposited and previously idle funds. - - **Returns**: NONE. - - **Good to Know**: - - This function is permissionless, so swaps or LP movements can be sandwiched or manipulated. - - This does not need to deploy the full `_amount` if the strategy doesn't want to. - - **Best Practice**: - - Use the `_amount` parameter passed in rather than relying on .balanceOf(address(this)) - - **Example**: - - function _deployFunds(uint256 _amount) internal override { - yieldSource.deposit(asset, _amount); - } - -2. *_freeFunds(uint256 _amount)* - **Purpose**: - - This function is called during withdraws from your strategy if there is insufficient idle asset to service the full withdrawal. - - **Parameters**: - - `_amount`: The amount of the underlying asset that needs to be pulled from the yield source. - - **Returns**: NONE. - - **Good to Know**: - - The amount of loose assets has already been accounted for. - - This function is also entirely permissionless, so swaps or lp values can be sandwiched or otherwise manipulated. - - **Best Practice**: - - Use the `_amount` parameter passed in rather than relying on .balanceOf(address(this)). - - **Any difference between the `_amount` parameter and the actual amount withdrawn will count as a loss and be passed on to the withdrawer. It may be preferred to revert for temporary issues such as liquidity constraints rather than pass on a loss**. - - If your strategy is illiquid or can not always service full withdraws, you can limit the amount by overriding `availableWithdrawLimt` outlined below. - - **Example**: - - function _freeFunds(uint256 _amount) internal override { - yieldSource.withdraw(asset, _amount); - } - -3. *_harvestAndReport()* - **Purpose**: - - Called during every report. This should harvest and sell any rewards, reinvest any proceeds, perform any position maintenance and return a full accounting of a trusted amount denominated in the underlying asset the strategy holds. - - **Parameters**: NONE - - **Returns**: - - `_totalAssets`: A trusted and accurate account for the total amount of 'asset' the strategy currently holds including loose funds. - - **Good to Know**: - - This can only be called by a permissioned address so if set up correctly, it can be trusted to be through a protected relay to perform swaps, LP movements etc. - - It is safe to account for loose assets in this function since any reported profit is immediately locked and not subject to price-per-share manipulation. - - **Best Practice**: - - The returned value is used to account for all strategy profits, losses and fees so care should be taken when relying on oracle values, LP prices etc. that have the potential to be manipulated. - - This can still be called after a strategy has been shut down so you may want to check if the strategy is shut down before performing certain functions like re-deploying loose funds. - - **Example**: - - function _harvestAndReport() internal override returns (uint256 _totalAssets) { - // Only harvest and redeploy if the strategy is not shutdown. - if(!TokenizedStrategy.isShutdown()) { - // Claim all rewards and sell to asset. - _claminAndSellRewards(); - // Check how much loose asset we have from rewards. - uint256 looseAsset = ERC20(asset).balanceOf(address(this)); - // Deposit the sold amount back into the yield source. - yieldSource.deposit(asset, looseAsset); - } - - // Return full balance no matter what. - _totalAssets = yieldSource.balanceof(address(this)) + ERC20(asset).balanceOf(address(this)); - } - -### Optional Functions -Simply overriding those three functions will make your strategy a fully functional, permissionless, 4626-compliant stand-alone vault. It can work independently or seamlessly into any Yearn V3 vault. - -While that may be all that's necessary for some of the most straightforward strategies, most strategists may want to add more customization or complexity to their strategy. There are five more optional functions that can be overridden by a strategist if desired to continue to build out their Tokenized Strategy. - - -1. *availableDeositLimit(address _owner)* - **Purpose**: - - This is called during any deposits and can be used to enforce any deposit limit or white list that the strategist desires. - - **Parameters**: - - `_owner`: The address receiving the shares minted during the deposit. - - **Returns**: - - The limit if any that should be enforced on the deposit. - - **Good to Know**: - - This will default to return uint256 max. - - This does not need to consider any conversion rates from assets to shares. But you should know that any limit under uint256 max may get converted to shares and should not be high enough to overflow on multiplication. - - **Best Practices**: - - Make sure to implement setter functions for any deposit limit or whitelist that are enforced. - - **Example**: - - function availableDepositLimit( - address _owner - ) public view override returns (uint256) { - uint256 totalAssets = TokenizedStrategy.totalAssets(); - return totalAssets >= depositLimit ? 0 : depositLimit - totalAssets; - } - -1. *availableWithdrawLimit(address _owner)* - **Purpose**: - - This is called during every withdraw and can be used to enforce any withdraw limit the strategist desires. - - **Parameters**: - - `_owner`: The address that owns the shares that would be burnt for the underlying assets. - - **Returns**: - - The limit if any that should be enforced on withdraws. - - **Good to Know**: - - This does not need to consider the balance of the _owner. - - This can be more than the actual amount available to withdraw. - - Defaults to max uint256. - - **Best Practices**: - - This should be overridden for strategies that have illiquid, or sandwichable positions to prevent reporting incorrect losses on withdraws. - - **This should never be lower than TokenizedStrategy.totalIdle()**. - - This does not need to consider conversion rates from assets to shares. But you should know that any limit under uint256 max may get converted to shares and should not be high enough to overflow on multiplication. - - **Example**: - - function availableWithdrawLimit( - address _owner - ) public view override returns (uint256) { - return TokenizedStrategy.totalIdle() + - ERC20(asset).balanceOf(address(yieldSource)); - } - - **Example #2**: - - function availableWithdrawLimit( - address _owner - ) public view override returns (uint256) { - if(positionIsLocked) { - return TokenizedStrategy.totalIdle(); - } else { - return TokenizedStrategy.totalAssets(); - } - } - -1. *_tend(uint256 _totalIdle)* - **Purpose**: - - This would get called during a `tend` call and can be used if a strategy needs to perform any maintenance or other actions that don't require a full report. If used the strategy should also implement a `tendTrigger` that keepers can monitor to know when it should be called. - - **Parameters**: - - `_totalIdle`: The amount of asset currently loose in the strategy. - - **Returns**: NONE - - **Good to Know**: - - The strategies `totalDebt` and `totalIdle` amounts will be automatically updated after this completes based on the end state, but will keep the totalAssets the same as not to have any effect on PPS. - - **Best Practices**: - - This can only be called by the keeper or management so it should be from a trusted source. - - Can be used to perform LTV adjustments on leveraged strategies. - - Can be used to know a trusted relay has been used to deposit idle funds for a strategy that doesn't deploy funds during deposits. - - **Example**: - - function _tend(uint256 _totalIdle) internal override { - if (currentLTV() < targetLTV()) { - _leverUp(_totalIdle); - } else if (currentLTV > warningLTV()) { - _leverDown(_totalIdle); - } - } - -1. *tendTrigger()* - **Purpose**: - - Should return whether or not a keeper should call `tend` on the strategy. This should be implemented if tend is needed to be used. - - **Parameters**: NONE. - - **Returns**: - - Boolean representing if a keeper should call `tend`. - - **Good to Know**: - - Default return value is false. - - **Best Practices**: - - Can implement checks on the current base fee of the chain to ensure the gas cost isn't too high. - - **Example**: - - function tendTrigger() public view override returns (bool) { - if (currentLTV() > warningLTV()) { - return true; - } else if (currentLTV() < lowerBoundLTV()) { - return isBaseFeeAcceptable() ? true : false; - } - } - -1. *_emergencyWithdraw(uint256 _amount)* - **Purpose**: - - Allows management to manually pull funds from the yield source once a strategy has been shut down. - - **Parameters**: - - `_amount`: The specific amount to pull from the yield source - - **Returns**: NONE. - - **Good to Know**: - - This can only be called once a strategy is shut down. - - The `_amount` can be more than is available to pull. - - The totalDebt and totalIdle will be updated based on the end state after the emergencyWithdraw, keeping totalAssets the same. - - **Best Practices**: - - Keep the withdrawal logic as simple as possible. - - Check `_amount` against the available amount to withdraw. - - **Example**: - - function _emergencyWithdraw(uint256 _amount) internal override { - _amount = min(_amount, yieldSource.balanceOf(address(this))); - yieldSource.withdraw(asset, _amount); - } - - - ---- -All other functionality, such as reward selling, upgradability, etc., is up to the strategist to determine what best fits their vision. Due to the ability of strategies to stand alone from a Vault, it is expected and encouraged for strategists to experiment with more complex, risky, or previously unfeasible Strategies. - -### FYI -NOTE: The only default global variables from the BaseStrategy that can be accessed from storage is `asset` and `TokenizedStrategy`. If other global variables are needed for your specific strategy, you can use the `TokenizedStrategy` variable to quickly retrieve any other needed variables, such as `totalAssets`, `totalDebt`, `isShutdown` etc. - -Example: - - require(!TokenizedStrategy.isShutdown(), "strategy is shutdown"); - - -NOTE: Writing to a strategy's default global storage state internally post-deployment is impossible. You must make external calls from the `management` address to configure any desired variables. - -To include permissioned functions such as extra setters, the two modifiers of `onlyManagement` and `onlyManagementAndKeepers` are available by default. - - -The symbol used for each tokenized Strategy is set automatically with a standardized approach based on the `asset`'s symbol. Strategists should use the `name` parameter in the constructor for a unique and descriptive name that encapsulates their specific Strategy. - -## Periphery - -To make Strategy writing as simple as possible, a suite of optional 'Periphery' helper contracts can be inherited by your Strategy to provide standardized and tested functionality for things like swaps. A complete list of the periphery contracts can be viewed here https://github.com/yearn/tokenized-strategy-periphery. - - -*All periphery contracts are optional; strategists can choose if they wish to use them. - -### [Swappers](https://github.com/yearn/tokenized-strategy-periphery/tree/master/src/swappers) - -To make reward swapping as easy and standardized as possible, multiple swapper contracts can be inherited by a strategy to inherit pre-built and tested logic for whichever method of reward swapping is desired. This allows a strategist only to need to set a few global variables and then simply use the default syntax of `_swapFrom(tokenFrom, tokenTo, amountIn, minAmountOut)` to swap any tokens easily during `_harvestAndReport`. - -### [APR Oracles](https://github.com/yearn/tokenized-strategy-periphery/tree/master/src/AprOracle) - -For easy integration with Vaults, front ends, debt allocators, etc. There is the option to create an [APR oracle](https://github.com/yearn/tokenized-strategy-periphery/blob/master/src/AprOracle/AprOracleBase.sol) contract for your specific strategy that should return the expected APR of the Strategy based on some given `debtChange`. - - -### [HealthCheck](https://github.com/Schlagonia/tokenized-strategy-periphery/tree/master/src/HealthCheck) - -To prevent automated reports from reporting losses/excessive profits that may not be accurate, a strategist can inherit and implement the [HealthCheck](https://github.com/yearn/tokenized-strategy-periphery/blob/master/src/HealthCheck/BaseHealthCheck.sol) contract. This can ensure that a keeper will not call a report that may incorrectly realize incorrect losses or excessive gains. It can cause the report to revert if the gain/loss is outside of the desired bounds and will require manual intervention to ensure the strategy is reporting correctly. - -NOTE: It is recommended to implement some checks in `_harvestAndReport` for leveraged or manipulatable strategies that could report incorrect losses due to unforeseen circumstances. - -### [Report Triggers](https://github.com/yearn/tokenized-strategy-periphery/tree/master/src/ReportTrigger) - -The expected behavior is that strategies report profits/losses on a schedule based on their specific `profitMaxUnlockTime` that management can customize. If a custom trigger cycle is desired or extra checks should be added a strategist can create their own [customReportTrigger](https://github.com/yearn/tokenized-strategy-periphery/blob/master/src/ReportTrigger/CustomStrategyTriggerBase.sol) that can be added to the default contract for a specific strategy. - -*More information can be found below in the "Reporting" section. - -## Testing - -Due to the nature of the BaseStrategy utilizing an external contract for most of its logic, the default interface for any strategy will not allow proper testing of all functions. Testing of your Strategy should utilize the pre-built [IStrategyInterface](https://github.com/yearn/tokenized-strategy-foundry-mix/blob/master/src/interfaces/IStrategyInterface.sol) to cast any deployed strategy through for testing, as seen in the testing setups in each mix. You can add any external functions you add for your specific strategy to this interface to test all functions with one variable. - -Foundry Example: - - Strategy _strategy = new Strategy(asset, name); - IStrategyInterface strategy = IStrategyInterface(address(_strategy)); - -Ape Example: - - strategy = management.deploy(project.Strategy, asset, name) - strategy = project.IStrategyInterface.at(strategy.address) - -Due to the permissionless nature of the tokenized Strategies, all tests are written without integration with any meta vault funding it. While those tests can be added, all V3 vaults utilize the ERC-4626 standard for deposit/withdraw and accounting, so they can be plugged in easily to any number of different vaults with the same `asset.` - -## Deployment - -Building a factory that can be deployed once is recommended for strategies that will be used with multiple different assets. Then, all strategies can be deployed on the chain using the factory. - -**Cloning is not recommended for Tokenized Strategies.** - - -#### Contract Verification - -Once the Strategy is deployed and verified, you must also verify the TokenizedStrategy functions. To do this, navigate to the /#code page on Etherscan. - -1. Click on the `More Options` drop-down menu. -2. Click "is this a proxy?". -3. Click the "Verify" button. -4. Click "Save". - -This should add all of the external `TokenizedStrategy` functions to the contract interface on Etherscan. - -## Operating Your strategy - -Once deployed your strategy should be able to be interacted with as any other ERC-4626 vault. - -In addition to the normal 4626 interface, Tokenized Strategies come built-in with some simple functions for management to properly maintain the strategy. - -### Reporting - -The main operational procedure strategists need to take care of is the reporting of a strategy. Calling `report` on a strategy must be done by either the 'management' or 'keeper' address. - -Reporting causes the strategy to accrue rewards, and record any gains or losses, as charges and paid fees. It is needed for the depositors of a vault to earn yield and for the strategist to earn fees. - -It is recommended to build strategies on the assumption that reports will happen based on the strategies specific `profitMaxUnlockTime`. - -Since reports are the only time _harvestAndReport will be called any strategies that need more frequent checks or updates should override the _tend and tendTrigger functions for any needed mid-report maintenance. - -#### Keepers - -The easiest way to ensure regular reports and tends on your strategy is to hook it up with a 3rd party keeper. - -The recommended keeper network is the [Gelato Network](https://www.gelato.network/). - -### Setters - -The strategy comes with some default variables that the management of a strategy can set and update. - -1. Changing management: Changing the strategies management is a two-step process. First, the current management must call `setPendingManagment(address)` with the desired address to transfer the management to. Then, that address must call `acceptManagement()` for the change to go into effect. -2. Keeper. A strategy manager can set a new address to be the keeper at any time with `setKeeper(address)`. -3. Performance Fee. Management can adjust the amount of the gain realized during a report that gets charged as a performance fee with `setPerformanceFee(uint16)`. - **Subject to min and max. -4. Performance Fee Recipient. Set the address to receive the performance fees charged during a report with `setPerformanceFeeRecipient(address)`. -5. Profit Unlocking Period. Profits recorded during reports are slowly unlocked to depositors of a strategy over the strategy-specific 'profitMaxUnlockTime'. This defaults to 10 days and can be changed at any time by the strategist with `setProfitMaxUnlockTime(uint256)`. - - -### Emergencies - -There are two default emergency functions built in. The first of which is `shutdownStrategy()`. This can only ever be called by the management and is non-reversible. - -Once this is called it will stop any further deposits or mints but will not affect any other functionality including withdraw, redeem, report and tend. This allows management to continue recording profits or losses and users to withdraw even post-shutdown. - -This can be used in an emergency or simply to retire a vault. - -Once a strategy is shut down management can also call `emergencyWithdraw(_amount)`, which will tell the strategy to withdraw a specified `_amount` from the yield source and keep it idle in the vault. This function will also do any needed updates to `totalDebt` and `totalIdle`, based on amounts withdrawn to ensure withdraws continue functioning correctly. - -All other emergency functionality is left up to the individual strategist. - -## FAQ - - -- How do I get my strategy added to a Yearn vault? -- How do I get My strategy added to the Yearn UI? - -## Other reading material - diff --git a/docs/developers/v3/strategy_writing_guide.md b/docs/developers/v3/strategy_writing_guide.md index 5903371df6..8056460fd1 100644 --- a/docs/developers/v3/strategy_writing_guide.md +++ b/docs/developers/v3/strategy_writing_guide.md @@ -2,55 +2,58 @@ ## Overview -So you wanna be a Yearn strategist? Can you write 3 lines of code? +So you wanna be a Yearn strategist? -Well then, you came to the right place. +Can you write 3 lines of code? -The strategy creation process has been simplified in V3 so that writing a Tokenized Strategy with only 3 lines of code is technically possible. However, of course, most strategies will need more than that. This will guide you through the full strategy development process from getting your local repository set up, through deployment, automation, and all the way to cashing out those sweet strategist fees. +Well, then you came to the right place. + +The strategy creation process has been very simplified in V3, so it is technically possible to write a Tokenized Strategy with only 3 lines of code. However, most strategies will need more than that. This will guide you through the entire strategy development process from getting your local repo set up through deployment and automation to cashing out those sweet sweet strategist fees. ## Introduction -A strategy's job is to hold the logic responsible for generating yield on some underlying asset. It should hold the code to integrate with an external protocol and do any needed asset deployment, withdraw logic, reward selling, reinvesting Loan-to-Value maintenance, and other such logic to generate the most yield possible from that particular source most efficiently. +A strategy's job is to hold the logic responsible for generating yield on some underlying asset. It should hold the code to integrate with an external protocol and do any needed asset deployment, withdraw logic, reward selling, reinvesting LTV maintenance etc. to generate the most possible yield from that particular source most efficiently. -The biggest update to Yearn Version 3 yVaults from V2 was the introduction of "Tokenized Strategies". In V2, strategies were stand-alone contracts attached to one specific vault. That vault was the only one who could ever deposit or withdraw funds from the strategy and each had a trusted relationship with the other. +The most significant update to Yearn V3 from V2 was the introduction of "Tokenized Strategies". In V2 strategies were stand-alone contracts that were attached to one specific vault. That vault was the only one who could ever deposit or withdraw funds from the strategy and each had a trusted relationship with the other. -In V3 strategies are now, themselves, fully ERC-4626-compliant stand-alone vaults. Though their job remains the same (i.e., generate yield from one external source), strategies can now be connected to many different vaults simultaneously and deposited directly into by an end user. +In V3 strategies are now fully ERC-4626-compliant stand-alone vaults. Though their job remains the same (i.e., generate yield from one external source), strategies can now be connected to many different vaults simultaneously and deposited directly into by an end user. -This increased functionality not only means strategies have a much larger potential market of depositors but also means anyone (including you) now has the ability to build, deploy, market, and maintain your own Yearn strategy without any need for an endorsement or permission from Yearn. +This increased functionality not only means strategies have a much larger potential market of depositors but also means anyone (including you) now can build, deploy, market, and maintain your own Yearn strategy without any need for an endorsement or permission from Yearn. ## Why? -- *Strategist Fees!* - V3 brings back the ability for developers of strategies to earn the fees generated from their strategy: meaning your earning potential is unlimited. -- *Codify Your Yield Farming* - Tokenized Strategies make it easy for anyone to codify their yield-generating strategies. Want to keep your alpha private? No problem they come fully customizable to allow for you to be the only one allowed to deposit. -- *Simple 4626 Wrappers* - Tokenized Strategies offer a super easy and cheap way to give any previously deployed protocol an ERC-4626 interface. This opens up any protocol to easily integrate into the rapidly growing 4626 ecosystem (including Yearn Meta Vaults). +- *Strategist Fees!* - V3 brings back the ability for developers of strategies to earn the fees generated from their strategy. Meaning your earning potential is unlimited. +- *Codify Your Yield Farming* - Tokenized strategies make it easy for anyone to codify their yield-generating strategies. Wanna keep your alpha private? No problem they come fully customizable to allow for you to be the only one allowed to deposit. +- *Simple 4626 Wrappers* - Tokenized Strategies make a super easy and cheap way to give any previously deployed protocol an ERC-4626 interface. This opens up any protocol to easily integrate into the rapidly growing 4626 ecosystem (including Yearn Allocator vaults). ## Definitions -- [Strategy](https://github.com/yearn/tokenized-strategy): A strategy or "Tokenized Strategy" in V3 refers to an ERC-4626 compliant contract that utilizes the [TokenizedStrategy](https://github.com/yearn/tokenized-strategy/blob/master/src/TokenizedStrategy.sol#L14-L26) pattern that either Meta Vaults or individual users can deposit directly into and receive shares in return. The strategy takes the underlying asset and deploys it into a single source to generate yield on that asset. -- asset: Any ERC20-compliant token. +- [Strategy](https://github.com/yearn/tokenized-strategy) : A strategy or "Tokenized Strategy" in V3 refers to an ERC-4626 compliant contract that utilizes the [TokenizedStrategy](https://github.com/yearn/tokenized-strategy/blob/master/src/TokenizedStrategy.sol) pattern that either allocator vaults or individual users can deposit directly into and receive shares in return. The strategy takes the underlying asset and deploys it in a single source in order to generate yield on that asset. +- Vault: Or "Allocator Vault" is a Yearn ERC4626 compliant Smart contract that receives assets from Depositors to distribute among the different Strategies added to the vault, managing accounting and asset distribution. +- Asset: Any ERC20-compliant token - Shares: ERC20-compliant token that tracks the asset balance in the strategy for every depositor. - [TokenizedStrategy.sol](https://github.com/yearn/tokenized-strategy/blob/master/src/TokenizedStrategy.sol): The implementation contract that all strategies delegateCall to for the standard ERC4626 and profit locking functions. -- [BaseStrategy.sol](https://github.com/yearn/tokenized-strategy/blob/master/src/BaseStrategy.sol): The abstract contract that a strategy should inherit from, which handles all communication with the Tokenized Strategy contract. +- [BaseStrategy.sol](https://github.com/yearn/tokenized-strategy/blob/master/src/BaseStrategy.sol): The abstract contract that a strategy should inherit from that handles all communication with the Tokenized Strategy contract. - Strategist: The developer of a specific strategy. -- Depositor: Account that deposits the asset and holds Shares. -- Vault: Or also called "Meta Vault", is a Yearn ERC4626 compliant Smart Contract that receives assets from Depositors to then distribute them among the different Strategies added to the vault, managing accounting and asset distribution. -- Management: The owner of the specific strategy that can set fees, profit unlock time, etc. -- Keeper: The address of a contract allowed to call report() and tend() on a strategy. -- Factory: The factory that all Meta Vaults of a specific API version are deployed from which also controls the protocol fee amount and protocol fee recipient. +- Depositor: Account that deposits the asset and holds Shares +- Management: The owner of the specific strategy that can set fees, profit unlocking time etc. +- Emergency Admin: Address that can call emergency functions on the strategy. +- Keeper: the address of a contract allowed to call report() and tend() on a strategy. +- Factory: The factory that all Allocator Vaults of a specific API version are cloned from that also controls the protocol fee amount and recipient for a strategy. - Performance Fee: The fee strategies charge during reports based on the yield earned since the last report. -- Performance Fee Recipient: The address that receives the shares charged as performance fees. -- Protocol Fee: A fee on the earnings of the strategists, sent to the Protocol Fee Recipient. -- Profit Max Unlock Time: Time in seconds over which reported profits will unlock. -- `totalIdle`: The amount of loose assets that are sitting in a strategy. -- `totalDebt`: The amount of deployed funds that a strategy has control over. -- `report`: Called by Management or Keepers to accrue all profits or losses, charge fees, and lock profit to be distributed. -- `tend`: Called by Management or Keepers between reports for any maintenance that should happen that doesn't require a full report. -- API Version: The version that a specific strategy is using for its logic. +- Performance Fee recipient: The address that receives the shares charged as performance fees. +- Protocol Fee: A fee on the fees charged by strategist sent to the Yearn Treasury. +- Profit Max Unlock Time: Time in seconds over which reported profits will unlock over. +- `totalIdle` : The amount of loose asset sitting in a strategy. +- `totalDebt` : The amount of deployed funds that a strategy has control over. +- `report`: Called by management or keepers to accrue all profits or losses, charge fees, and lock profit to be distributed. +- `tend`: Called by management or keepers between reports for any maintenance that should happen that doesn't require a full report. +- API Version: The version that a specific Strategy is using for its logic. ## Architecture While the complete architecture of the Tokenized Strategy is out of the scope of this document you can read more about how it works [here](https://github.com/yearn/tokenized-strategy/blob/master/SPECIFICATION.md) -**TLDR**: V3 strategies use an immutable proxy pattern to outsource all of its complex, high-risk, and redundant code to one [TokenizedStrategy.sol](https://github.com/yearn/tokenized-strategy/blob/master/src/TokenizedStrategy.sol) implementation contract that is used by every strategy of a specific API version. To use this pattern you simply need to inherit the [BaseStrategy.sol](https://github.com/yearn/tokenized-strategy/blob/master/src/BaseStrategy.sol) contract, which holds all of the logic to communicate with the implementation contract, and then just override a few simple functions with your specific strategy logic. +**TLDR**: V3 strategies use an immutable proxy pattern to outsource all of its complex, high risk, and redundant code to one [TokenizedStrategy.sol](https://github.com/yearn/tokenized-strategy/blob/master/src/TokenizedStrategy.sol) implementation contract that is used by every strategy of a specific API version. To use this pattern you simply need to inherit the [BaseStrategy.sol](https://github.com/yearn/tokenized-strategy/blob/master/src/BaseStrategy.sol) contract, that holds all of the logic to communicate with the implementation contract, and then just override a few simple functions with your specific strategy logic. ## Getting started @@ -59,256 +62,248 @@ Yearn has base templates made to build off of built-in both [Ape Worx](https://w 1. Choose your development framework. - [Tokenized Strategy Foundry Mix](https://github.com/yearn/tokenized-strategy-foundry-mix) - [Tokenized Strategy Ape Mix](https://github.com/yearn/tokenized-strategy-ape-mix) - -2. Set up a local environment with the selected mix. Each mix has detailed instructions in the "How To Start" section of the README, specific requirements needed, cloning instructions, and required environment variables. - -3. Assure the tests pass. Each mix comes with a small set of pre-written tests which serve as examples. They can be used to make sure your local repository is set up correctly before adding your logic. + +2. Set up the local environment with the selected mix. Each mix has detailed instructions in the "How To Start" section of the README, of specific requirements, cloning instructions, and needed environment variables. +4. Assure tests pass. Each mix comes with a small set of pre-written tests, to serve as examples and can be used to ensure your local repository is set up correctly before adding your logic. ## Strategy Writing -So you have your idea and local environment setup. Now it's time to start writing your actual strategy. +So you have your idea and local environment setup. Now its time to start writing your actual strategy. To create your Tokenized Strategy, you must override at least three functions outlined in the `Strategy.sol`. ___ -1. *_deployFunds(uint256 _amount)* - -**Purpose**: -- This function is called during every deposit into your strategy to allow it to deploy the underlying asset deposited into the yield source. - -**Parameters**: -- `_amount`: The total amount of underlying assets currently available for the strategy to deploy including the amount deposited and previously idle funds. +The only default global variables from the BaseStrategy that can be accessed from storage is `asset` and `TokenizedStrategy`. -**Returns**: NONE. +If other global variables are needed for your specific strategy, you can use the `TokenizedStrategy` variable to quickly retrieve any other needed variables, such as `totalAssets`, `totalDebt`, `isShutdown` etc. -**Good to Know**: -- This function is entirely permissionless, so swaps or LP movements can be sandwiched or otherwise manipulated. -- This does not need to deploy the full `_amount` if the strategy doesn't want to. +Example: -**Best Practice**: -- Use the `_amount` parameter passed in rather than relying on .balanceOf(address(this)). + require(!TokenizedStrategy.isShutdown(), "strategy is shutdown"); +___ -**Example**: +1. *_deployFunds(uint256 _amount)* + **Purpose**: + - This function is called during every deposit into your strategy to allow it to deploy the underlying asset deposited into the yield source. + + **Parameters**: + - `_amount`: The total amount of underlying assets currently available for the deployment strategy, including the amount deposited and previously idle funds. + + **Returns**: NONE. + + **Good to Know**: + - This function is permissionless, so swaps or LP movements can be sandwiched or manipulated. + - This does not need to deploy the full `_amount` if the strategy doesn't want to. - function _deployFunds(uint256 _amount) internal override { - yieldSource.deposit(asset, _amount); - } + **Best Practice**: + - Use the `_amount` parameter passed in rather than relying on .balanceOf(address(this)) + + **Example**: + + function _deployFunds(uint256 _amount) internal override { + yieldSource.deposit(address(asset), _amount); + } 2. *_freeFunds(uint256 _amount)* - -**Purpose**: -- This function is called during withdraws from your strategy if there are insufficient idle assets to service the full withdrawal. - -**Parameters**: -- `_amount`: The amount of the underlying assets that need to be pulled from the yield source. - -**Returns**: NONE. - -**Good to Know**: -- The amount of loose assets has already been accounted for. -- This function is also permissionless, so swaps or LP values can be sandwiched or manipulated. - -**Best Practice**: -- Use the `_amount` parameter passed in rather than relying on .balanceOf(address(this)). -- **Any difference between the `_amount` parameter and the actual amount withdrawn will count as a loss and be passed on to the withdrawer. It may be preferred to revert for temporary issues such as liquidity constraints rather than pass on a loss**. -- If your strategy is illiquid or can not always service full withdraws, you can limit the amount by overriding `availableWithdrawLimt` outlined below. - -**Example**: + **Purpose**: + - This function is called during withdraws from your strategy if there is insufficient idle asset to service the full withdrawal. + + **Parameters**: + - `_amount`: The amount of the underlying asset that needs to be pulled from the yield source. + + **Returns**: NONE. + + **Good to Know**: + - **The amount of loose assets has already been accounted for**. + - This function is also entirely permissionless, so swaps or lp values can be sandwiched or otherwise manipulated. + + **Best Practice**: + - Use the `_amount` parameter passed in rather than relying on .balanceOf(address(this) since idle has already been accounted for. + - **Any difference between the `_amount` parameter and the actual amount withdrawn will count as a loss and be passed on to the withdrawer. It may be preferred to revert for temporary issues such as liquidity constraints rather than pass on a loss**. + - If your strategy is illiquid or can not always service full withdraws, you can limit the amount by overriding `availableWithdrawLimit` outlined below. - function _freeFunds(uint256 _amount) internal override { - yieldSource.withdraw(asset, _amount); - } + **Example**: + + function _freeFunds(uint256 _amount) internal override { + yieldSource.withdraw(address(asset), _amount); + } 3. *_harvestAndReport()* - -**Purpose**: -- Called during every report. This should harvest and sell any rewards, reinvest any proceeds, perform any position maintenance and return a full accounting of a trusted amount denominated in the underlying asset the strategy holds. - -**Parameters**: NONE - -**Returns**: -- `_totalAssets`: A trusted and accurate account for the total amount of 'asset' the strategy currently holds including loose funds. - -**Good to Know**: -- This can only be called by a permissioned address so if set up correctly, it can be trusted to be through a protected relay to perform swaps, LP movements etc. -- It is safe to account for loose `asset` in this function since any profit reported is immediately locked and, therefore is not subject to price per share manipulation. - -**Best Practice**: -- The returned value accounts for all the strategy's profits, losses, and fees, so care should be taken when relying on oracle values, LP prices, etc. that can be manipulated. -- This can still be called after a strategy has been shut down, so you may want to check if the strategy is shut down before performing certain functions like re-deploying loose funds. - -**Example**: - - function _harvestAndReport() internal override returns (uint256 _totalAssets) { - // Only harvest and redeploy if strategy is not shutdown. - if(!TokenizedStrategy.isShutdown()) { - // Claim all rewards and sell to asset. - _claminAndSellRewards(); - // Check how much loose asset we have from rewards. - uint256 looseAsset = ERC20(asset).balanceOf(address(this)); - // Deposit the sold amount back into the yield source. - yieldSource.deposit(asset, looseAsset); + **Purpose**: + - Called during every report. This should harvest and sell any rewards, reinvest any proceeds, perform any position maintenance and return a full accounting of a trusted amount denominated in the underlying asset the strategy holds. + + **Parameters**: NONE + + **Returns**: + - `_totalAssets`: A trusted and accurate account for the total amount of 'asset' the strategy currently holds including loose funds. + + **Good to Know**: + - This can only be called by a permissioned address so if set up correctly, it can be trusted to be through a protected relay to perform swaps, LP movements etc. + - It is safe to account for loose assets in this function since any reported profit is immediately locked and not subject to price-per-share manipulation. + + **Best Practice**: + - The returned value is used to account for all strategy profits, losses and fees so care should be taken when relying on oracle values, LP prices etc. that have the potential to be manipulated. + - This can still be called after a strategy has been shut down so you may want to check if the strategy is shut down before performing certain functions like re-deploying loose funds. + + **Example**: + + function _harvestAndReport() internal override returns (uint256 _totalAssets) { + // Only harvest and redeploy if the strategy is not shutdown. + if(!TokenizedStrategy.isShutdown()) { + // Claim all rewards and sell to asset. + _claimAndSellRewards(); + // Check how much loose asset we have from rewards. + uint256 looseAsset = asset.balanceOf(address(this)); + // Deposit the sold amount back into the yield source. + yieldSource.deposit(address(asset), looseAsset); + } + + // Return full balance no matter what. + _totalAssets = yieldSource.balanceOf(address(this)) + asset.balanceOf(address(this)); } - // Return full balance no matter what. - _totalAssets = yieldSource.balanceof(address(this)) + ERC20(asset).balanceOf(address(this)); - } - ### Optional Functions -Simply overriding those three functions will make your strategy a fully functional, permissionless, 4626-compliant, stand-alone vault. It can work entirely on its own or be seamlessly integrated into any Yearn V3 vault. - -While that may be all that's necessary for some of the most simple strategies, generally most strategists will add a bit more customization or complexity to their strategy. Five more optional functions can be overridden by a strategist to continue to build out their Tokenized Strategy. - - -1. *availableDeositLimit(address _owner)* - -**Purpose**: -- This is called during any deposits and can be used to enforce any deposit limit or white list that the strategist desires. +Simply overriding those three functions will make your strategy a fully functional, permissionless, 4626-compliant stand-alone vault. It can work independently or seamlessly into any Yearn V3 vault. -**Parameters**: -- `_owner`: The address receiving the shares minted during the deposit. +While that may be all that's necessary for some of the most straightforward strategies, most strategists may want to add more customization or complexity to their strategy. There are five more optional functions that can be overridden by a strategist if desired to continue to build out their Tokenized Strategy. -**Returns**: -- The limit if any that should be enforced on the deposit. -**Good to Know**: -- This will default to return uint256 max. -- This does not need to consider conversion rates from assets to shares. But should know that any limit under uint256 max may get converted to shares and should not be high enough to overflow on multiplication. - -**Best Practices**: -- Make sure to implement setter functions for any deposit limit or whitelist that is enforced. - -**Example**: - - function availableDepositLimit( - address _owner - ) public view override returns (uint256) { - uint256 totalAssets = TokenizedStrategy.totalAssets(); - return totalAssets >= depositLimit ? 0 : depositLimit - totalAssets; - } +1. *availableDepositLimit(address _owner)* + **Purpose**: + - This is called during any deposits and can be used to enforce any deposit limit or white list that the strategist desires. -2. *availableWithdrawLimit(address _owner)* - -**Purpose**: -- This is called during every withdraw and can be used to enforce any withdraw limit the strategist desires. - -**Parameters**: -- `_owner`: The address that owns the shares that would be burnt for the underlying assets. - -**Returns**: -- The limit if any that should be enforced on withdraws. - -**Good to Know**: -- This does not need to consider the balance of the _owner. -- This can be more than the actual amount available to withdraw. -- Defaults to max uint256. - -**Best Practices**: -- This should be overridden for strategies that have illiquid, or sandwichable positions to prevent reporting incorrect losses on withdraws. -- **This should never be lower than TokenizedStrategy.totalIdle()**. -- This does not need to consider any conversion rates from assets to shares. But should know that any limit under uint256 max may get converted to shares and should not be high enough to overflow on multiplication. - -**Example**: - - function availableWithdrawLimit( - address _owner - ) public view override returns (uint256) { - return TokenizedStrategy.totalIdle() + - ERC20(asset).balanceOf(address(yieldSource)); - } + **Parameters**: + - `_owner`: The address receiving the shares minted during the deposit. -**Example #2**: - - function availableWithdrawLimit( - address _owner - ) public view override returns (uint256) { - if(positionIsLocked) { - return TokenizedStrategy.totalIdle(); - } else { - return TokenizedStrategy.totalAssets(); + **Returns**: + - The limit if any that should be enforced on the deposit. + + **Good to Know**: + - This will default to return uint256 max. + - This does not need to consider any conversion rates from assets to shares. But you should know that any limit under uint256 max may get converted to shares and should not be high enough to overflow on multiplication. + + **Best Practices**: + - Make sure to implement setter functions for any deposit limit or whitelist that are enforced. + + **Example**: + + function availableDepositLimit( + address _owner + ) public view override returns (uint256) { + uint256 totalAssets = TokenizedStrategy.totalAssets(); + return totalAssets >= depositLimit ? 0 : depositLimit - totalAssets; + } + +1. *availableWithdrawLimit(address _owner)* + **Purpose**: + - This is called during every withdraw and can be used to enforce any withdraw limit the strategist desires. + + **Parameters**: + - `_owner`: The address that owns the shares that would be burnt for the underlying assets. + + **Returns**: + - The limit if any that should be enforced on withdraws. + + **Good to Know**: + - This does not need to consider the balance of the _owner. + - This can be more than the actual amount available to withdraw. + - Defaults to max uint256. + + **Best Practices**: + - This should be overridden for strategies that have illiquid, or sandwichable positions to prevent reporting incorrect losses on withdraws. + - This should not be used if the expectation is the strategy is always fully liquid. + - To just allow the idle funds to be withdrawn use `TokenizedStrategy.totalIdle()`. + - This does not need to consider conversion rates from assets to shares. But you should know that any limit under uint256 max may get converted to shares and should not be high enough to overflow on multiplication. + + **Example #2**: + + function availableWithdrawLimit( + address _owner + ) public view override returns (uint256) { + if(positionIsLocked) { + return TokenizedStrategy.totalIdle(); + } else { + return type(uint256).max; + } } - } - -3. *_tend(uint256 _totalIdle)* - -**Purpose**: -- This would get called during a `tend` call and can be used if a strategy needs to perform any maintenance or other actions that don't require a full report. If used the strategy should also implement a `tendTrigger` that keepers can monitor to know when it should be called. - -**Parameters**: -- `_totalIdle`: The amount of asset that is currently loose in the strategy. - -**Returns**: NONE - -**Good to Know**: -- The strategies `totalDebt` and `totalIdle` amounts will be automatically updated after this completes based on the end state, but will keep the totalAssets the same so as to not have any effect on PPS. - -**Best Practices**: -- This can only be called by the keeper or management so it should be from a trusted source. -- Can be used to perform LTV adjustments on leveraged strategies. -- Can be used to know a trusted relay has been used to deposit idle funds for a strategy that doesn't deploy funds during deposits. - -**Example**: - function _tend(uint256 _totalIdle) internal override { - if (currentLTV() < targetLTV()) { - _leverUp(_totalIdle); - } else if (currentLTV > warningLTV()) { - _leverDown(_totalIdle); +1. *_tend(uint256 _totalIdle)* + **Purpose**: + - This would get called during a `tend` call and can be used if a strategy needs to perform any maintenance or other actions that don't require a full report. If used the strategy should also implement a `_tendTrigger` that keepers can monitor to know when it should be called. + + **Parameters**: + - `_totalIdle`: The amount of asset currently loose in the strategy. + + **Returns**: NONE + + **Good to Know**: + - The strategies `totalDebt` and `totalIdle` amounts will be automatically updated after this completes based on the end state, but will keep the totalAssets the same as not to have any effect on PPS. + + **Best Practices**: + - This can only be called by the keeper or management so it should be from a trusted source. + - Can be used to perform LTV adjustments on leveraged strategies. + - Can be used to know a trusted relay has been used to deposit idle funds for a strategy that doesn't deploy funds during deposits. + + **Example**: + + function _tend(uint256 _totalIdle) internal override { + if (currentLTV() < targetLTV()) { + _leverUp(_totalIdle); + } else if (currentLTV > warningLTV()) { + _leverDown(_totalIdle); + } } - } -4. *tendTrigger()* - -**Purpose**: -- Should return whether or not a keeper should call `tend` on the strategy. This should be implemented if tend is needed to be used. - -**Parameters**: NONE. - -**Returns**: -- Boolean representing if a keeper should call `tend`. - -**Good to Know**: -- Default return value is false. - -**Best Practices**: -- Can implement checks on the current base fee of the chain to ensure the gas cost isn't too high. - -**Example**: +1. *_tendTrigger()* + **Purpose**: + - Should return whether or not a keeper should call `tend` on the strategy. This should be implemented if tend is needed to be used. + + **Parameters**: NONE. - function tendTrigger() public view override returns (bool) { - if (currentLTV() > warningLTV()) { - return true; - } else if (currentLTV() < lowerBoundLTV()) { - return isBaseFeeAcceptable() ? true : false; + **Returns**: + - Boolean representing if a keeper should call `tend`. + + **Good to Know**: + - Default return value is false. + + **Best Practices**: + - Can implement checks on the current base fee of the chain to ensure the gas cost isn't too high. + + **Example**: + + function _tendTrigger() public view override returns (bool) { + if (currentLTV() > warningLTV()) { + return true; + } else if (currentLTV() < lowerBoundLTV()) { + return isBaseFeeAcceptable() ? true : false; + } } - } -5. *_emergencyWithdraw(uint256 _amount)* - -**Purpose**: -- Allows management to manually pull funds from the yield source once a strategy has been shut down. - -**Parameters**: -- `_amount`: The specific amount to pull from the yield source - -**Returns**: NONE. - -**Good to Know**: -- This can only be called once a strategy is shut down. -- The `_amount` can be more than is available to pull. -- The totalDebt and totalIdle will be updated based on the end state after the emergencyWithdraw keeping totalAssets the same. - -**Best Practices**: -- Keep the withdrawal logic as simple as possible. -- Check `_amount` against the available amount to withdraw. - -**Example**: - - function _emergencyWithdraw(uint256 _amount) internal override { - _amount = min(_amount, yieldSource.balanceOf(address(this))); - yieldSource.withdraw(asset, _amount); - } +1. *_emergencyWithdraw(uint256 _amount)* + **Purpose**: + - Allows management to manually pull funds from the yield source once a strategy has been shut down. + + **Parameters**: + - `_amount`: The specific amount to pull from the yield source + + **Returns**: NONE. + + **Good to Know**: + - This can only be called once a strategy is shut down. + - The `_amount` can be more than is available to pull. + - The totalDebt and totalIdle will be updated based on the end state after the emergencyWithdraw, keeping totalAssets the same. + + **Best Practices**: + - Keep the withdrawal logic as simple as possible. + - Check `_amount` against the available amount to withdraw. + + **Example**: + + function _emergencyWithdraw(uint256 _amount) internal override { + _amount = min(_amount, yieldSource.balanceOf(address(this))); + yieldSource.withdraw(asset, _amount); + } @@ -316,16 +311,12 @@ While that may be all that's necessary for some of the most simple strategies, g All other functionality, such as reward selling, upgradability, etc., is up to the strategist to determine what best fits their vision. Due to the ability of strategies to stand alone from a Vault, it is expected and encouraged for strategists to experiment with more complex, risky, or previously unfeasible Strategies. ### FYI -NOTE: The only default global variables from the BaseStrategy that can be accessed from storage is `asset` and `TokenizedStrategy`. If other global variables are needed for your specific strategy, you can use the `TokenizedStrategy` variable to quickly retrieve any other needed variables within the strategy, such as `totalAssets`, `totalDebt`, `isShutdown` etc. -Example: - - require(!TokenizedStrategy.isShutdown(), "strategy is shutdown"); +The Tokenized Strategy contract manually track totalAssets using totalIdle and totalDebt. Meaning any functions that change the balance of loose and deployed asset should go through or end with one of the core functions such as report or tend so that the balances will be updated to the correct amounts and withdraws can still function. +**NOTE**: Writing to a strategy's default global storage state internally post-deployment is impossible. You must make external calls from the `management` address to configure any desired variables. -NOTE: Writing to a strategy's default global storage state internally post-deployment is impossible. You must make external calls from the `management` address to configure any desired variables. - -To include permissioned functions such as extra setters, the two modifiers of `onlyManagement` and `onlyManagementAndKeepers` are available by default. +To include extra permissioned functions such as setters, there are modifiers for `onlyManagement`, `onlyKeepers` and `onlyEmergencyAuthorized` available by default. The symbol used for each tokenized Strategy is set automatically with a standardized approach based on the `asset`'s symbol. Strategists should use the `name` parameter in the constructor for a unique and descriptive name that encapsulates their specific Strategy. @@ -335,22 +326,22 @@ The symbol used for each tokenized Strategy is set automatically with a standard To make Strategy writing as simple as possible, a suite of optional 'Periphery' helper contracts can be inherited by your Strategy to provide standardized and tested functionality for things like swaps. A complete list of the periphery contracts can be viewed here https://github.com/yearn/tokenized-strategy-periphery. -*All periphery contracts are optional, and strategists can choose if they wish to use them. +*All periphery contracts are optional; strategists can choose if they wish to use them. ### [Swappers](https://github.com/yearn/tokenized-strategy-periphery/tree/master/src/swappers) -To make reward swapping as easy and standardized as possible multiple swapper contracts can be inherited by a strategy to inherit pre-built and tested logic for whichever method of reward swapping is desired. This allows a strategist to only need to set a few global variables and then simply use the default syntax of `_swapFrom(tokenFrom, tokenTo, amountIn, minAmountOut)` to swap any tokens easily during `_harvestAndReport`. +To make reward swapping as easy and standardized as possible, multiple swapper contracts can be inherited by a strategy to inherit pre-built and tested logic for whichever method of reward swapping is desired. This allows a strategist only to need to set a few global variables and then simply use the default syntax of `_swapFrom(tokenFrom, tokenTo, amountIn, minAmountOut)` to swap any tokens easily during `_harvestAndReport`. ### [APR Oracles](https://github.com/yearn/tokenized-strategy-periphery/tree/master/src/AprOracle) -For easy integration with Vaults, front ends, debt allocators etc. There is the option to create an [APR oracle](https://github.com/yearn/tokenized-strategy-periphery/blob/master/src/AprOracle/AprOracleBase.sol) contract for your specific strategy that should return the expected APR of the Strategy based on some given `debtChange`. +For easy integration with Vaults, front ends, debt allocators, etc. There is the option to create an [APR oracle](https://github.com/yearn/tokenized-strategy-periphery/blob/master/src/AprOracle/AprOracleBase.sol) contract for your specific strategy that should return the expected APR of the Strategy based on some given `debtChange`. ### [HealthCheck](https://github.com/Schlagonia/tokenized-strategy-periphery/tree/master/src/HealthCheck) To prevent automated reports from reporting losses/excessive profits that may not be accurate, a strategist can inherit and implement the [HealthCheck](https://github.com/yearn/tokenized-strategy-periphery/blob/master/src/HealthCheck/BaseHealthCheck.sol) contract. This can ensure that a keeper will not call a report that may incorrectly realize incorrect losses or excessive gains. It can cause the report to revert if the gain/loss is outside of the desired bounds and will require manual intervention to ensure the strategy is reporting correctly. -NOTE: It is recommended to implement some checks in `_harvestAndReport` for leveraged or manipulatable strategies that could report incorrect losses due to unforeseen circumstances. +**NOTE**: It is recommended to implement some checks in `_harvestAndReport` for leveraged or manipulatable strategies that could report incorrect losses due to unforeseen circumstances. ### [Report Triggers](https://github.com/yearn/tokenized-strategy-periphery/tree/master/src/ReportTrigger) @@ -360,7 +351,7 @@ The expected behavior is that strategies report profits/losses on a schedule bas ## Testing -Due to the nature of the BaseStrategy utilizing an external contract for most of its logic, the default interface for any strategy will not allow proper testing of all functions. Testing of your Strategy should utilize the pre-built [IStrategyInterface](https://github.com/yearn/tokenized-strategy-foundry-mix/blob/master/src/interfaces/IStrategyInterface.sol) to cast any deployed strategy through for testing, as seen in the testing setups in each mix. To test all functions with one variable, you can add any external functions you add for your specific strategy to this interface. +Due to the nature of the BaseStrategy utilizing an external contract for most of its logic, the default interface for any strategy will not allow proper testing of all functions. Testing of your Strategy should utilize the pre-built [IStrategyInterface](https://github.com/yearn/tokenized-strategy-foundry-mix/blob/master/src/interfaces/IStrategyInterface.sol) to cast any deployed strategy through for testing, as seen in the testing setups in each mix. You can add any external functions you add for your specific strategy to this interface to test all functions with one variable. Foundry Example: @@ -372,18 +363,18 @@ Ape Example: strategy = management.deploy(project.Strategy, asset, name) strategy = project.IStrategyInterface.at(strategy.address) -Due to the permissionless nature of the tokenized Strategies, all tests are written without integration with any meta vault funding it. While those tests can be added, all V3 vaults utilize the ERC-4626 standard for deposit/withdraw and accounting so that they can be plugged in easily to any number of different vaults with the same `asset.` +Due to the permissionless nature of the Tokenized Strategies, all tests are written without integration with any allocator vault funding it. While those tests can be added, all V3 vaults utilize the ERC-4626 standard for deposit/withdraw and accounting, so they can be plugged in easily to any number of different vaults with the same `asset.` ## Deployment -It is recommended to build a factory that can be deployed once for strategies that will be used with multiple different assets. Then, all strategies can be deployed on the chain using the factory. +Building a factory that can be deployed once is recommended for strategies that will be used with multiple different assets. Then, all strategies can be deployed on the chain using the factory. **Cloning is not recommended for Tokenized Strategies.** #### Contract Verification -Once the Strategy is deployed and verified, you must verify the TokenizedStrategy functions. To do this, navigate to the /#code page on Etherscan. +Once the Strategy is deployed and verified, you must also verify the TokenizedStrategy functions. To do this, navigate to the /#code page on Etherscan. 1. Click on the `More Options` drop-down menu. 2. Click "is this a proxy?". @@ -396,13 +387,13 @@ This should add all of the external `TokenizedStrategy` functions to the contrac Once deployed your strategy should be able to be interacted with as any other ERC-4626 vault. -In addition to the normal 4626 interface Tokenized Strategies come built-in with some simple functions for management to properly maintain the strategy. +In addition to the normal 4626 interface, Tokenized Strategies come built-in with some simple functions for management to properly maintain the strategy. ### Reporting The main operational procedure strategists need to take care of is the reporting of a strategy. Calling `report` on a strategy must be done by either the 'management' or 'keeper' address. -Reporting causes the strategy to accrue rewards, record any gains or losses as well and charge and pay fees. It is needed for the depositors of a vault to earn yield as well as for the strategist to earn fees. +Reporting causes the strategy to accrue rewards, and record any gains or losses, as well as charge and pay fees. It is needed for the depositors of a vault to earn yield and for the strategist to earn fees. It is recommended to build strategies on the assumption that reports will happen based on the strategies specific `profitMaxUnlockTime`. @@ -412,37 +403,29 @@ Since reports are the only time _harvestAndReport will be called any strategies The easiest way to ensure regular reports and tends on your strategy is to hook it up with a 3rd party keeper. -The recommended keeper network to use is the [Gelato Network](https://www.gelato.network/). +The recommended keeper network is the [Gelato Network](https://www.gelato.network/). ### Setters The strategy comes with some default variables that the management of a strategy can set and update. -1. Changing management: Changing the strategies management is a two-step process. First, the current management must call `setPendingManagment(address)` with the desired address to transfer the management to. Then that address must call `acceptManagement()` for the change to go into effect. +1. Changing management: Changing the strategies management is a two-step process. First, the current management must call `setPendingManagement(address)` with the desired address to transfer the management to. Then, that address must call `acceptManagement()` for the change to go into effect. 2. Keeper. A strategy manager can set a new address to be the keeper at any time with `setKeeper(address)`. -3. Performance Fee. Management can adjust the amount of the gain realized during a report that gets charged as a performance fee with `setPerformanceFee(uint16)`. +3. Emergency Admin. This will default to address(0) and can be set by the management with `setEmergencyAdmin(address)`. +4. Performance Fee. Management can adjust the amount of the gain realized during a report that gets charged as a performance fee with `setPerformanceFee(uint16)`. **Subject to min and max. -4. Performance Fee Recipient. Can set the address that will receive the performance fees charged during a report with `setPerformanceFeeRecipient(address)`. +4. Performance Fee Recipient. Set the address to receive the performance fees charged during a report with `setPerformanceFeeRecipient(address)`. 5. Profit Unlocking Period. Profits recorded during reports are slowly unlocked to depositors of a strategy over the strategy-specific 'profitMaxUnlockTime'. This defaults to 10 days and can be changed at any time by the strategist with `setProfitMaxUnlockTime(uint256)`. ### Emergencies -There are two default emergency functions built in. The first of which is `shutdownStrategy()`. This can only ever be called by the management and is non-reversible. +There are two default emergency functions built in. The first of which is `shutdownStrategy()`. This can only ever be called by the management or emergencyAdmin and is non-reversible. -Once this is called it will stop any further deposits or mints but will have no effect on any other functionality including withdraw, redeem, report and tend. This is to allow management to continue potentially recording profits or losses and users to withdraw even post-shutdown. +Once this is called it will stop any further deposits or mints but will not affect any other functionality including withdraw, redeem, report and tend. This allows management to continue recording profits or losses and users to withdraw even post-shutdown. This can be used in an emergency or simply to retire a vault. -Once a strategy is shut down management can also call `emergencyWithdraw(_amount)`. Which will tell the strategy to withdraw a specified `_amount` from the yield source and keep it idle in the vault. This function will also do any needed updates to `totalDebt` and `totalIdle`, based on amounts withdrawn to ensure withdraws continue functioning properly. +Once a strategy is shut down management or the emergencyAdmin can also call `emergencyWithdraw(_amount)`, which will tell the strategy to withdraw a specified `_amount` from the yield source and keep it idle in the vault. This function will also do any needed updates to `totalDebt` and `totalIdle`, based on amounts withdrawn to ensure withdraws continue functioning correctly. All other emergency functionality is left up to the individual strategist. - -## FAQ - - -- How do I get my strategy added to a Yearn vault? -- How do I get My strategy added to the Yearn UI? - -## Other reading material - diff --git a/docs/developers/v3/vault_management.md b/docs/developers/v3/vault_management.md index bc25f9a1d4..f3085c2256 100644 --- a/docs/developers/v3/vault_management.md +++ b/docs/developers/v3/vault_management.md @@ -1,17 +1,17 @@ # Deploying and Managing a V3 Vault -V3 makes it as simple as possible for anyone to deploy and manage their Vaults. No longer will Yearn be the only manager of Vaults, gate-keeping who can be debt allocator, or what strategies should be added to a vault. Now, anyone can deploy, manage, and earn fees from their vision and preferences for everything from risk profile, fee model, decentralization, etc. +V3 makes it as simple as possible for anyone to deploy and manage their Vaults. No longer will Yearn be the only manager of Vaults, gate-keeping who can be a debt allocator, or what strategies should be added to a vault. Now, anyone can deploy, manage, and earn fees from their vision and preferences on everything from risk profile, fee model, decentralization, etc. -In V3 our "Meta Vaults" or "Vaults of Vaults" are designed to be efficient 4626 compliant debt allocators that can have many different "strategies" attached to them and will direct funds to these strategies based on the vault's management choice. The vaults are built to be "plug and play" meaning managers can simply deploy, add their strategies, and start the yield generation. But they also hold many customization factors, allowing different managers to differentiate themselves and experiment with different optionalities. +In V3 our "Allocator Vaults" or "Meta of Vaults" are designed to be efficient 4626 compliant debt allocators that can have many different "strategies" attached to them and will direct funds to these strategies based on the vault's management choice. The vaults are built to be "plug and play" meaning managers can simply deploy, add their strategies, and start the yield generation. But they also hold many customization factors, allowing different managers to differentiate themselves and experiment with different optionality. Running your vault requires no need to know how to code. Anyone desiring to manage their strategies and allocations can simply deploy and run their vault. ## Definitions -- **vault**: ERC-4626 compliant contract that accepts deposits, issues shares, and allocates funds to different strategies to earn yield. +- **vault (allocator)**: ERC-4626 compliant contract that accepts deposits, issues shares, and allocates funds to different strategies to earn yield. - **shares**: A tokenized representation of a depositor's share of the underlying balance of a vault. -- **strategy**: Any ERC-4626 compliant contract that can be added to a vault that earns yield on an underlying asset. -- **debt**: The amount of the underlying asset that a vault has sent to a strategy to earn yield. +- **strategy**: Any ERC-4626 compliant contract that can be added to an allocator vault that earns yield on an underlying asset. +- **debt**: The amount of the underlying asset that an allocator vault has sent to a strategy to earn yield. - **report**: The function where a vault accounts for any profits or losses a strategy has accrued, charges applicable fees, and locks profit to be distributed to depositors. @@ -19,9 +19,9 @@ Running your vault requires no need to know how to code. Anyone desiring to mana Each release of the vaults will have its own "Vault Factory" deployed to make it as simple and trustless as possible to deploy your vault. The vault factory utilizes [ERC-5202](https://eips.ethereum.org/EIPS/eip-5202) blueprint pattern to allow anyone to trustlessly deploy their vault which is an exact copy of the previously deployed "blueprint" vault for that specific version. -** Vaults not deployed through the factory will not be recognized as part of the Yearn ecosystem and may cause issues during runtime. +**Vaults not deployed through the factory will not be recognized as part of the Yearn ecosystem and may experience issues during runtime. -To deploy your vault, simply find the factory's address for the most recent release here and call `Factory.deploy_new_vault(params)`. +To deploy your vault, simply find the factory's address for the most recent release [here](https://docs.yearn.fi/developers/v3/overview) and call `Factory.deploy_new_vault(params)`. The needed parameters are: @@ -37,7 +37,7 @@ Once deployed, you can get your vault's address from either the Factory function The vault should be automatically verified when deployed. However, if it is not you can follow the [verification steps](https://etherscan.io/verifyContract) on Etherscan using the [VaultV3.vy](https://github.com/yearn/yearn-vaults-v3/blob/master/contracts/VaultV3.vy) for your specific API Version. -NOTE: The vault factory utilizes [create2](https://eips.ethereum.org/EIPS/eip-1014) opcode to deploy vaults to deterministic addresses. This means the same address can not deploy two vaults with the same default parameters for 'asset', 'name' and 'symbol'. +**NOTE**: The vault factory utilizes [create2](https://eips.ethereum.org/EIPS/eip-1014) opcode to deploy vaults to deterministic addresses. This means the same address can not deploy two vaults with the same default parameters for 'asset', 'name' and 'symbol'. ## Setup @@ -45,27 +45,36 @@ Once deployed, additional setup steps and variables can be configured if desired #### Roles --- -The first is to set up the Roles for your specific vault. The vaults use a role-based system for access control to the permissioned functions. The roles are a [Vyper Enumerator](https://docs.vyperlang.org/en/stable/types.html#enums) pattern based on Python. +The first is to set up the Roles for your specific vault. The vaults use a role-based system for access control to the permissioned functions. The roles are a [Vyper Enumerator](https://docs.vyperlang.org/en/stable/types.html#enums) pattern based on Pythons. Each permissioned function in the Vaults has its own "role" that can call that specific function. For example, to call `add_strategy(new_strategy: address)` the address must have the `ADD_STRATEGY_MANAGER` role. Roles can be held by any number of addresses or by no address. There is also the option to "open" any role, meaning the function becomes permissionless. (This is not recommended for most permissioned functions and should be done carefully.) -The same address can hold every role, each role can be helped by a different address or any combination desired. +The same address can hold every role, each role can be held by a different address or any combination desired. -To give an account a specific role can simply call `vault.set_role(account, role)` where 'role' is the int representing all the roles you would like the 'account' to hold. A full explanation of [python enumerators](https://docs.python.org/3/howto/enum.html) is beyond the scope of this doc, but the corresponding int to each role can be viewed [here](https://github.com/yearn/yearn-vaults-v3/blob/master/tests/utils/constants.py#L12) +The deployer of a vault will be the default holder of the `role_manager` that is in charge of assigning roles to different addresses. + +A full explanation of [python enumerators](https://docs.python.org/3/howto/enum.html) is beyond the scope of this doc, but the corresponding int to each role can be viewed [here](https://github.com/yearn/yearn-vaults-v3/blob/master/tests/utils/constants.py#L12) + +To give an account a specific role you can simply call `vault.set_role(account, role)` where 'role' is the int representing all the roles you would like the 'account' to hold. This will override all roles previously held by the address. + +The role manager can also use `vault.add_role(account, role_to_add)` to only add 1 new role to the existing roles that account already has. Or `vault.remove_role(account, role_to_remove)` to remove just one role without overriding the full bitmap. Example: # Set `account` to be the ADD_STRATEGY_MANAGER vault.set_role(account, 1) - # Set `account` to be the REVOKE_STRATEGY_MANAGER - vault.set_role(account, 2) - # Set `account` to be both the ADD_STRATEGY_MANAGER and REVOKE_STRATEGY_MANAGER vault.set_role(account, 3) + # Add the REPORTING_MANAGER role to the accounts already held roles. + vault.add_role(account, 32) + + # Remove just the REVOKE_STRATEGY_MANAGER role. + vault.remove_role(account, 2) + # Set `account` to hold every role - vault.set_role(account, 8191) + vault.set_role(account, 16383) # Set `account` to hold no roles vault.set_role(account, 0) @@ -77,7 +86,8 @@ NOTE: The vault `role_manager` can not call any permissioned function by default --- Each vault will default to have a deposit limit set to 0. Which means all deposits will revert. -Once ready, the address with the DEPOSIT_LIMIT_MANAGER will need to call `vault.set_deposit_limit(deposit_limit)` +Once ready, the address with the DEPOSIT_LIMIT_MANAGER will need to either set a deposit_limit > 0 or add a deposit_limit_module. + #### Miscellaneous --- @@ -91,7 +101,7 @@ There are other options that a vault manager can set that are not necessary for #### Strategy Management The job of a vault is to manage debt between strategies that do the yield generation. 3 roles control what strategies are added to the vault, ADD_STRATEGY_MANAGER, REVOKE_STRATEGY_MANAGER, and FORCE_REVOKE_MANAGER. -A strategy can be any contract that has the needed [4626 interface](https://github.com/yearn/yearn-vaults-v3/blob/master/contracts/VaultV3.vy#L39) for the vault to interact with it. This includes Tokenized Strategies, 3rd party 4626 vaults, and other Yearn meta vaults. +A strategy can be any contract that has the needed [4626 interface](https://github.com/yearn/yearn-vaults-v3/blob/master/contracts/VaultV3.vy#L39) for the vault to interact with it. This includes Tokenized Strategies, 3rd party 4626 vaults, and other allocator vaults. To add a strategy first call `vault.add_strategy(strategy_address)`. Each strategy gets added with a default 'max_debt' of 0. This means the MAX_DEBT_MANAGER will need to call `vault.update_max_debt_for_strategy(strategy, max_debt)`. @@ -108,12 +118,13 @@ The DEBT_MANAGER role is in charge of allocating funds between the strategies ad All debt updates are denominated in the underlying asset and are restricted by the `max_debt` for each strategy and the `minimum_total_idle` for the specific vault. -Debt updates will also respect the strategies specific `maxWithdraw` and `maxDeposit`. +Debt updates will also respect the strategies specific `maxRedeem` and `maxDeposit`. To deposit or withdraw vault funds from a strategy simply call `vault.update_debt(strategy, desired_debt)` where desired debt is the end amount denominated in the underlying asset that the strategy should have after the full debt update. -NOTE: If a strategy has unrealized losses you cannot lower its debt. -NOTE: It is recommended to report a strategy's gain before withdrawing 100% of debt from the strategy. +**NOTE**: If a strategy has unrealized losses you cannot lower its debt. + +**NOTE**: It is recommended to report a strategy's gain before withdrawing 100% of debt from the strategy. #### Reporting @@ -133,12 +144,22 @@ You will need to add a separate contract as the vault's 'accountant' to charge f `vault.set_accountant(accountant)` -The accountant is [called by the vault](https://github.com/yearn/yearn-vaults-v3/blob/master/contracts/VaultV3.vy#L1070) during every `report` with the strategy that is reporting and the gain or loss it's reporting. The accountant will then return the total fees or refunds that should be charged by the vault during that report and paid to the accountant. +The accountant is called by the vault during every `report` with the strategy that is reporting and the gain or loss it's reporting. The accountant will then return the total fees or refunds that should be charged by the vault during that report and paid to the accountant. Accountants can hold any logic that vault managers want to dictate fees or simply charge normal performance or management fees. A ready-to-use Generic Accountant can easily be used with any vault for those who wish just to charge standard fees. See Here: https://github.com/yearn/vault-periphery/blob/master/contracts/accountants/GenericAccountant.vy +#### Deposit/Withdraw Limit Modules + +Each vault comes with default deposit and withdraw limits that can be used for out of the box 4626 compliant functionality. However, these limits can also be completely customized for full programmability by adding a deposit_limit_module or withdraw_limit_module respectively. + +This modules are stand alone smart contracts similar to the accountant that if added will be used by the vault to enforce any custom deposit or withdraw limits. + +This can be used to enforce a whitelist of depositors, minimum or maximum deposit sizes, liquidity constraints etc. + +**Note**: To set a deposit_limit_module the DEPOSIT_LIMIT_MANAGER will first need to set the vaults default `deposit_limit` to uint256 max. And will not be able to change the `deposit_limit` unless the deposit_limit_module is removed and set to address(0). + #### Default Queue Each vault has a `default_queue` based on the strategies added and removed from the vault. The `default_queue` is used to service withdraws when no custom queue is passed. This queue is simply ordered by the time when strategies were added: where the oldest strategy is at the beginning of the queue. @@ -149,6 +170,8 @@ If a different ordering is desired or management wants to remove a certain strat Where `new_default_queue` is an array of strategies with a max length of 10, in which all strategies are currently active in the vault. +The vaults QUEUE_MANAGER can also choose to not allow custom queues to be passed into the vault on withdraws at any time by turning on the 'use_default_queue' flag by calling, `vault.set_use_default_queue(True)`. + ## Good to Know ### What Tokens Not to Use @@ -160,4 +183,4 @@ A few examples of this are: - Fee on Transfer - Re-entrancy Tokens (ERC-777) -Any token whose normal functionality breaks the ERC-20 standard may not be compatible with V3 vaults. +Any token whose normal functionality breaks the ERC-20 standard may not be compatible with V3 vaults. \ No newline at end of file diff --git a/docs/getting-started/products/yvaults/v3.md b/docs/getting-started/products/yvaults/v3.md index 008133ffee..68d3993156 100644 --- a/docs/getting-started/products/yvaults/v3.md +++ b/docs/getting-started/products/yvaults/v3.md @@ -1,9 +1,9 @@ # Vaults Version 3 Version 3 yVaults iterates on Version 2 by increasing robustness and developing Yearn’s path towards further decentralization, while keeping the same proven product (yield-bearing tokens) that abstract builders and users from the hard work of yield farming. Version 3 will both be able to have the same functionality as Version 2, but with many more added benefits and improvements to continue to grow the Yearn ecosystem. -V3 also sees the introduction of "Tokenized Strategies". In V3 the strategies are now capable of being standalone 4626 vaults themselves. These single-strategy vaults can be used as stand-alone vaults or as a function of the 4626 standard easily added as a strategy to any of the multi-strategy "Meta Vaults". +V3 also sees the introduction of "Tokenized Strategies". In V3 the strategies are now capable of being standalone 4626 vaults themselves. These single-strategy vaults can be used as stand-alone vaults or as a function of the 4626 standard easily added as a strategy to any of the multi-strategy "Allocator Vaults". -Version 3 yVaults improves on Version 2 by: +Version 3 of yVaults improves on Version 2 by: - Increasing vault modularity, allowing for smaller and safer pieces of code. - Simplify strategy creation, empowering strategists and reducing the chance for errors. - More strategy functionality by implementing the Tokenized Strategy. @@ -17,15 +17,15 @@ Extensible code that can be attached to a yVault to extend functionality over an ### Smart Modules Smart Modules implement core vault logic that will be iterated until they can be made immutable. If any Smart Module fails, the vault can live without them just enough to return funds to depositors. **When Version 3 launches, the Smart Modules will replicate Version 2 vault behavior.** -- **Debt Allocator**: Can efficiently allocate debt to different strategies. Added to a Meta Vault for the best yield opportunities. +- **Debt Allocator**: Can efficiently allocate debt to different strategies. Added to a Allocator Vault for the best yield opportunities. - **Accountant**: Handles changing fees for vault operations. +- **Deposit/Withdraw Limit Modules**: Allows dynamic control over a vaults deposit or withdraw limits for full customization. ### Periphery Modules Periphery Modules are a separate layer of optional contracts to use with vaults and strategies. They are not required but facilitate building around yVaults. - **Router**: Wrapper that handles deposits and withdrawals to/from all vaults and strategies. - **Yearn Lens**: Information aggregator for off-chain apps. -- **APY TWAP Oracle**: Reliable source of Yearn vaults’ past performance. - **ySwaps**: Internal swap system. Reduces slippage thus improving net APY. - **Registry**: Handles adding and tracking strategies and vaults. - **HealthCheck**: Guardrail vault operations so that profit & loss reporting is always under acceptable values. @@ -33,409 +33,17 @@ Periphery Modules are a separate layer of optional contracts to use with vaults - **APR Oracles**: Retrieve the expected current APY on-chain for different strategies to properly allocate debt. - And any others you can come up with! -# Version 3 yVault Specification -## Overview +[VaultV3 Specification](https://github.com/yearn/yearn-vaults-v3/blob/master/TECH_SPEC.md) -The Vault code has been designed as a non-opinionated system to distribute funds of depositors into different opportunities (aka Strategies) and robustly manage accounting. Depositors receive shares (aka vaults tokens) proportional to their deposited amount. Vault tokens are yield-bearing and can be redeemed, to get back the initial deposit plus or minus any yield or losses realized by the vault. +[Tokenized Strategy Specification](https://github.com/yearn/tokenized-strategy/blob/master/SPECIFICATION.md) -Vaults do not have a preference on any of the dimensions that can be considered when creating and operating them: -- **Decentralization**: Roles can be filled by any address (e.g. EOA, smart contract, multi-sig). -- **Liquidity**: the Vault can have 0 liquidity or be fully liquid. It will depend on the parameters and strategies added. -- **Security**: Vault managers can choose which strategies to add. -- **Automation**: All the required actions to maintain the Vault can be called by bots or manually, depending on periphery implementation. +**For More information visit the [V3 section](https://docs.yearn.fi/developers/v3/overview) of the docs.** -Trade-offs will come with the implementation of Periphery Modules fulfilling different roles in the Vault. This allows different Vault creators to deploy their own versions and implement custom Periphery Modules (or not use any at all). - -### Examples of Periphery Modules: -- **Emergency Module**: Receives deposits of Vault Shares and allows the contract to call the shutdown function after a certain % of total Vault Shares have been deposited. -- **Debt Allocator**: Module which incentivizes APY / debt allocation optimization by rewarding the best debt allocator (see [yStarkDebtAllocator](https://github.com/jmonteer/ystarkdebtallocator)). -- **Strategy Staking Module**: Module allows people to sponsor specific strategies (so that they are added to the vault) by staking their YFI, making money if they do well and losing money if they don't. - -## Definitions -- **Account**: Any blockchain address for the network the vault is deployed on. -- **Asset**: Any ERC20-compliant token. -- **Shares**: ERC20-compliant token that tracks Asset balance in the vault for every distributor. Named yv. -- **Depositor**: Account that holds Shares. -- **Strategy**: Smart contract that is used to deposit in protocols to generate yield. -- **Vault**: ERC4626 compliant smart contract which receives Assets from Depositors to then distribute them among the different Strategies added to the Vault, managing accounting and Assets distribution. -- **Role**: Different flags an Account can have in the Vault so that the Account can do certain specific actions. Can be fulfilled by a smart contract or an EOA. -- **Accountant**: Smart contract which receives P&L reporting and returns shares/refunds to the strategy. - -## Deployment -We expect all the vaults available to be deployed from a Factory Contract, publicly available and callable. People deploying "branded" vaults (e.g. Yearn) will use a separate registry to allow permissioned endorsement of vaults for their product. - -When deploying a new Vault, the following parameters are required: -- `asset`: Address of the ERC20 token that can be deposited into the vault -- `name`: Name of Shares as described in ERC20 -- `symbol`: Symbol of Shares ERC20 -- `role_manager`: Account that can assign and revoke Roles -- `profit_max_unlock_time`: Max amount of time profit will be locked before being distributed - -## Normal Operation -### Deposits / Mints -Users can deposit asset tokens (Asset) to receive yvTokens (Shares). - -Deposits are limited under `depositLimit` and shutdown parameters. - -### Withdrawals / Redeems -Users can redeem their Shares at any point in time if there is liquidity available. Optionally, a user can specify a list of strategies to withdraw from. If a list of strategies is passed, the vault will try to withdraw from them. If a user-passed array is not defined, the redeem function will use the default_queue. - -To properly comply with the ERC-4626 standard and still allow losses, both withdraw and redeem have an additional optional parameter of 'maxLoss' that can be used. The default for 'maxLoss' is 0 (i.e. revert if any loss) for withdraws, and 10_000 (100%) for redeems. - -If not enough funds can be withdrawn to honor the full request within the maxLoss parameter given, the transaction will revert. - -### Vault Shares -Vault shares are ERC20 transferable yield-bearing tokens and ERC4626 compliant. Please read [Ethereum Improvement Proposal: ERC-4626](https://eips.ethereum.org/EIPS/eip-4626) for more information about ERC4626. - -### Accounting -The vault will evaluate profit and losses from the strategies. This is done by comparing the current debt of the strategy with the total assets the strategy is reporting to have. - -If `totalAssets` < `currentDebt`: the vault will record a loss -If `totalAssets` > `currentDebt`: the vault will record a profit - -Both loss and profit will impact a strategy's debt. Debt will be increased (current debt + profit) if there are profits and decreased (current debt - loss) if there are losses. - -#### Fees -Fee assessment and distribution are handled by the Accountant Module. It will report the amount of fees that need to be charged and the vault will issue shares for that amount of fees. There is also an optional protocol_fee that can be charged based on the configuration of the VaultFactory.vy - -### Profit Distribution -Profit from different process_report calls will accumulate in a buffer. This buffer will be linearly unlocked over the locking period seconds at profit_distribution_rate. - -Profits will be locked for a max time of profit_max_unlock_time seconds and will be gradually distributed. To avoid spending too much gas for profit unlock, the amount of time a profit will be locked is the weighted average between the new profit and the previous profit. - -`new_locking_period = locked_profit * pending_time_to_unlock + new_profit * PROFIT_MAX_UNLOCK_TIME / (locked_profit + new_profit)` - -`new_profit_distribution_rate = (locked_profit + new_profit) / new_locking_period` - -Losses will be offset by locked profit, if possible. The issuance of new shares due to fees will also unlock profit so that pricePerShare does not go down. Both of these offsets will prevent front running (as the profit was already earned and was not distributed yet). - -## Vault Management -Vault management is split into function-specific roles. Each permissioned function has its own corresponding role. This means roles can be combined all to one address, distributed to separate addresses, or any combination in between. - -### Roles -Vault functions that are permissioned will be callable by accounts that hold specific roles. - -These are: -- `ADD_STRATEGY_MANAGER`: Role that adds strategies to the vault -- `REVOKE_STRATEGY_MANAGER`: Role that removes strategies from the vault -- `FORCE_REVOKE_MANAGER`: Role that can forcefully remove a strategy causing a loss -- `ACCOUNTANT_MANAGER`: Role that can set the Accountant which assesses fees -- `QUEUE_MANAGER`: Role that sets the default withdrawal queue -- `REPORTING_MANAGER`: Role that calls report for strategies -- `DEBT_MANAGER`: Role that adds and removes debt from strategies -- `MAX_DEBT_MANAGER`: Role that sets the max debt for a strategy -- `DEPOSIT_LIMIT_MANAGER`: Role that sets the deposit limit for the vault -- `MINIMUM_IDLE_MANAGER`: Role that sets the minimum_total_idle the vault should keep -- `PROFIT_UNLOCK_MANAGER`: Role that sets the profit_max_unlock_time -- `DEBT_PURCHASER`: Role that purchases bad debt from the vault -- `EMERGENCY_MANAGER`: Role that can shut down the vault in an emergency - -Every role can be filled by an EOA, multi-sig or other smart contracts. Each role can be filled by several accounts. The account that manages roles is a single account, set in `role_manager`. This `role_manager` can be an EOA, a multi-sig or a Governance Module that relays calls. - -### Strategy Management -This responsibility is taken by callers with `ADD_STRATEGY_MANAGER`, `REVOKE_STRATEGY_MANAGER` and `FORCE_REVOKE_MANAGER` roles. A vault can have strategies added, removed, or forcefully removed. Added strategies will be eligible to receive funds from the vault, when the max_debt is set to > 0. Revoked strategies will return all debt and stop being eligible to receive more. It can only be done when the strategy's current_debt is 0. Force revoking a strategy is only used in cases of a faulty strategy that cannot otherwise have its current_debt reduced to 0. Force revoking a strategy will result in a loss being reported by the vault. - -#### Setting the Accountant Module -The Accountant can be set by the `ACCOUNTANT_MANAGER`. The contract is not needed for the vault to function but is recommended for optimal use. - -#### Reporting Profits -The `REPORTING_MANAGER` is in charge of calling process_report() for each strategy in the vault according to its own timeline. This call will do the necessary accounting and profit locking for the individual strategy as well as charge fees. - -### Debt Management -This responsibility is taken by callers with the `DEBT_MANAGER` role. This role can increase or decrease strategy-specific debt. The function updateDebt(strategy, target_debt) 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. - -The vault checks that the `minimumTotalIdle` parameter is respected (i.e. there's at least a certain amount of funds in the vault). If the strategy has more debt than the max_debt, the vault will request the funds back. These funds may be locked in the strategy, which will result in the strategy returning less funds than requested by the vault. - -#### Setting Maximum Debt for a Specific Strategy -The `MAX_DEBT_MANAGER` can set the maximum amount of tokens the vault will allow a strategy to owe at any moment in time. Stored in `strategies[strategy].max_debt`. When a debt re-balance is triggered, the Vault will cap the new target debt to this number (max_debt). - -#### Setting the Deposit Limit -The `DEPOSIT_LIMIT_MANAGER` is in charge of setting the deposit_limit for the vault. On deployment deposit_limit defaults to 0 and will need to be increased to make the vault functional. - -#### Setting Minimum Idle Funds -The `MINIMUM_IDLE_MANAGER` can specify how much funds the vault should try to have reserved to serve withdrawal requests. These funds will remain in the vault unless requested by a depositor. It is recommended that if no queue_manager is set, some amount of funds should remain idle to service withdrawals. - -#### Setting the Profit Unlock Period -The `PROFIT_UNLOCK_MANAGER` is in charge of updating and setting the profit_max_unlock_time which controls how fast profits unlock. This can be customized based on aspects such as the number of strategies, total value locked, expected returns, etc. - -#### Setting the Default Queue -The `QUEUE_MANAGER` can set a custom default_queue if desired. The vault will arrange the default queue automatically based on the order in which strategies were added to the vault. If a different order is desired, the `QUEUE_MANAGER` can set a custom queue. All strategies in the default queue must have been previously added to the vault. - -#### Buying Debt -The `DEBT_PURCHASER` role can buy debt from the vault in return for an equal amount of `asset`. This should only ever be used in the case when governance wants to purchase a set amount of bad debt from the vault to not report a loss. It still relies on `convertToShares()` so will only be viable if the conversion does not reflect a large negative realized loss from the strategy. - -#### Shutting Down the Vault -In an emergency, the `EMERGENCY_MANAGER` can shut down the vault. This will also give the `EMERGENCY_MANAGER` and the `DEBT_MANAGER` role as well so funds can start to be returned from the strategies. - -## Strategy Minimum API -Strategies are completely independent smart contracts that can be implemented following the proposed template. - -To be compatible with the vault, they need to implement the following functions, which are a subset of ERC4626 vaults: -- `asset()`: View returning underlying asset -- `totalAssets()`: View returning the current amount of assets. It can include rewards valued in `asset` -- `maxDeposit(address)`: View returning the amount max that the strategy can take safely -- `deposit(assets, receiver)`: Deposits `assets` amount of tokens into the strategy, can be restricted to the Vault only or be open -- `maxWithdraw(address)`: View returning how many assets can the vault take from the strategy at any given point in time -- `withdraw(assets, receiver, owner)`: Withdraws `assets` amount of tokens from the strategy -- `redeem(shares, receiver, owner)`: Redeems `shares` of the strategy for the underlying asset -- `balanceOf(address)`: Return the number of shares of the strategy that the address has -- `convertToAssets(shares: uint256)`: Converts `shares` into the corresponding amount of asset -- `convertToShares(assets: uint256)`: Converts `assets` into the corresponding amount of shares -- `previewWithdraw(assets: uint256)`: Converts `assets` into the corresponding amount of shares rounded up - -This means that the vault can deposit into any ERC4626 vault but also that a non-compliant strategy can be implemented provided that these functions have been implemented (even if in a non-ERC4626 compliant way). - -## ERC4626 Compliance -Vault Shares are ERC4626 compliant. - -The most important implication is that `withdraw` and `redeem` functions are as presented in ERC4626, with the ability to add two additional non-standard options. - -1. `max_loss`: The amount in basis points that the withdrawer will accept as a loss. ie, 100 = 1% loss accepted -2. `strategies`: This is an array of strategies to use as the withdrawal queue instead of the default queue - -### Deposits -`_Light emergency_`: Deposits can be paused by setting `depositLimit` to 0 - -`_Shutdown mode_`: Deposits are not allowed - -### Withdrawals -Withdrawals cannot be paused under any circumstance. - -### Accounting -Shutdown mode does not affect accounting. - -### Debt Rebalancing -`_Light emergency_`: Setting minimumTotalIdle to MAX_UINT256 will result in the vault requesting the debt back from strategies. This would stop new strategies from getting funded as well, as the vault prioritizes minimumTotalIdle - -`_Shutdown mode_`: All strategies maxDebt is set to 0. Strategies will return funds as soon as they can - -### Shutdown Mode -In case the current Roles stop fulfilling their responsibilities or something else happens, the `EMERGENCY_MANAGER` can shut down the vault. The shutdown mode should be the last option in an emergency as it is irreversible. During shutdown mode, the vault will try to get funds back from every strategy as soon as possible. No strategies can be added during shutdown. Any relevant Role will start pointing to the `EMERGENCY_MANAGER` in case new permissioned allowed actions need to be taken. - -# Yearn V3 Tokenized Strategy Specification -## Overview -The Yearn V3 "Tokenized Strategy" goal is to make it as easy as possible for any person or protocol to create and deploy their own ERC-4626 compliant single-strategy vault. It uses an immutable proxy pattern to outsource all of the standardized 4626 and other vault logic to one implementation contract that all strategies deployed on a specific chain use through delegatecall. - -This makes the strategy-specific contract as simple and specific to that yield-generating task as possible which allows for anyone to simply plug their version into a permissionless, secure, and optimized 4626-compliant base that handles all risky and complicated code. - -### Definitions -- **Asset**: Any ERC20-compliant token -- **Shares**: ERC20-compliant token that tracks Asset balance in the strategy for every distributor. -- **Strategy**: ERC4626 compliant smart contract that receives Assets from Depositors (vault or otherwise) to deposit in any external protocol to generate yield. -- **Tokenized Strategy**: The implementation contract that all strategies `delegateCall` to for the standard ERC4626 and profit locking functions. -- **BaseTokenizedStrategy**: The abstract contract that a strategy should inherit from that handles all communication with the Tokenized Strategy contract. -- **Strategist**: The developer of a specific strategy. -- **Depositor**: Account that holds Shares -- **Vault**: Or "Meta Vault" is an ERC4626 compliant Smart contract that receives Assets from Depositors to then distribute them among the different Strategies added to the vault, managing accounting and Assets distribution. -- **Management**: The owner of the specific strategy that can set fees, profit unlocking time etc. -- **Keeper**: the address of a contract allowed to call report() and tend() on a strategy. -- **Factory**: The factory that all meta vaults of a specific API version are cloned from that also controls the protocol fee amount and recipient. - -## Storage -In order to standardize all high-risk and complex logic associated with ERC4626, ERC20 and profit locking, all core logic has been moved to the 'TokenizedStrategy.sol' and is used by each strategy through a fallback function that delegatecall's this contract to do all necessary checks, logic and storage updates for the strategy. - -The TokenizedStrategy will only need to be deployed once on each chain and can then be used by an unlimited number of strategies. Allowing the BaseTokenizedStrategy.sol to be much smaller, simpler and cheaper to deploy. - -Using delegate call the external TokenizedStrategyy will be able to read and write to any and all of the strategies specific storage variables during all calls. This does open the strategy up to the possibility of storage collisions due to non-standardized storage calls and means extra precautions need to be taken when reading and writing to storage. - -In order to limit the strategists need to think about their storage variables all TokenizedStrategy specific variables are held within and controlled by the TokenizedStrategy. A `StrategyData` struct is held at a custom storage location that is high enough that no normal implementation should be worried about hitting. - -This means all high risk storage updates will always be handled by the TokenizedStrategy, should not be able to be overridden by a reckless strategist and will be entirely standardized across every strategy deployed, no matter the chain or specific implementation. - -## BaseTokenizedStrategy -The base tokenized strategy is a simple abstract contract to be inherited by the strategist that handles all communication with the TokenizedStrategy. - -### Modifiers -`onlySelf`: This modifier is placed on any callback functions for the TokenizedStrategy to call during deposits, withdraws, reports and tends. The modifier should revert if msg.sender is not equal to itself. In order for a call to be forwarded to the TokenizedStrategy it must not be defined in the Strategy and hit the fallback function which will delegatecall the TokenizedStrategy. If within that call, the TokenizedStrategy makes an external static call back to the BaseTokenizedStrategy the msg.sender of that call will be the original caller, which should be the Strategy itself. - -`OnlyManagement`: Should be placed on functions that only the Strategies specific management address can call. This uses the isManagement(address) function defined in TokenizedStrategy by sending the original msg.sender address. - -`onlyKeepers`: Should be placed on functions that only the Strategies specific management or keeper can call. This uses the isManagementOrKeeper(address) defined in TokenizedStrategy sending the original msg.sender address. - -### Variables -`tokenizedStrategyAddress`: This is the address the fallback function will use to delegatecall to and is set before deployment to a constant so it can never be changed. - -`TokenizedStrategy`: This is an immutable set on deployment setting an ITokenizedStrategy interface to address(this). The variable should be used in a similar manner as a linked library would be to have a simple method to read from the Strategies storage internally. Setting it to address(this) means anything using this variable will static call itself which should hit the fallback and then delegatecall the TokenizedStrategy retrieving the correct variables. - -`asset`: The immutable address of the underlying asset being used. - -### Functions -The majority of function in the BaseTokenizedStrategy are either external functions with onlySelf modifiers used for the TokenizedStrategy to call. Or the internal functions that correspond to those external functions that should or can be overridden by a strategist with the strategy specific logic. - -`deployFunds(uint256)/_DeployFunds(uint256)`: Called by the TokenizedStrategy during deposits into the strategy to tell the strategy it can deposit up to the amount passed in as a parameter if desired. - -`freeFunds(uint256)/_freeFunds(uint256)`: Called by the TokenizedStrategy during withdraws to get the amount of the uint256 parameter freed up in order to process the withdraw. - -`harvestAndReport()/_harvestAndReport()`: Called during reports to tell the strategy a trusted address has called it and to harvest any rewards re-deploy any loose funds and return the actual amount of funds the strategy holds. - -`tendThis(uint256)/_tend(uint256)`: Called by the TokenizedStrategy during tend calls to tell the strategy a trusted address has called tend and it has the uint256 parameter of loose asset available to deposit. NOTE: we use `tendThis` to avoid function signature collisions so that `tend` will be forwarded to the TokenizedStrategy. - -`tendTrigger()`: View function to return if a tend call is needed. - -`availableDepositLimt(address)/availableWithdrawLimit(address)`: Optional functions a strategist can override that default to uint256 max to implement any deposit or withdraw limits. - -`shutdownWithdraw(uint256)/_emergencyWithdraw(uint256)`: Optional function for a strategist to implement that will allow management to manually withdraw a specified amount from the yield source if a strategy is shutdown in the case of emergencies. - -`_init(...)`: Used only once during initialization to manually delegatecall the TokenizedStrategy to tell it to set up the storage for a new strategy. - -## TokenizedStrategy -The tokenized strategy contract should implement all ERC-4626, ERC-20, ERC-2612 and custom TokenizedStrategy specific report and tending logic within it. - -For deposits, withdraws, report, tend and emergency withdraw calls it casts address(this) into a custom IBaseTokenizedStrategy() interface to static call back the initial calling contract when it needs to interact with the Strategy. - -### Normal Operation - The TokenizedStrategy is responsible for handling the logic associated with all the following functionality. - -#### Deposits / Mints -Users can deposit ASSET tokens to receive shares. - -Deposits are limited by the maxAvailableDeposit function that can be changed by the strategist if non uint256.max values are desired. - -#### Withdrawals / Redeems -Users can redeem their shares at any point in time if there is liquidity available. The amount of a withdraw or redeem can be limited by the strategist by overriding the maxAvailableWithdraw function. - -In order to properly comply with the ERC-4626 standard and still allow losses, both withdraw and redeem have an additional optional parameter of 'maxLoss' that can be used. The default for 'maxLoss' is 0 (i.e. revert if any loss) for withdraws, and 10_000 (100%) for redeems. - -#### Strategy Shares -The strategy issues shares to each depositor to track their relative share of assets. Shares are ERC20 transferable, ERC4626 compliant, yield-bearing tokens. - -#### Accounting -The strategy will evaluate profit and losses from the yield generating activities. - -This is done comparing the current totalAssets of the strategy with the amount returned from _harvestAndReport() - -If `totalAssets` < `newTotalAssets`: the vault will record a loss -If `totalAssets` > `newTotalAssets`: the vault will record a profit - -Both loss and profit will impact strategy's totalAssets, increasing if there are profits, decreasing if there are losses. - -#### Fees -Fee assessment and distribution is handled during each `report` call after profits or losses are recorded. - -It will report the amount of fees that need to be charged and the strategy will issue shares for that amount of fees. - -There are two potential fees. Performance fees and protocol fees. Performance fees are configurable by management of the strategy and payed based on the reported profit during each report with a min of 5% and a max of 50%. - -Protocol fees are configured by Yearn governance through the Factory and are taken as a percent of the performanceFees charged. I.E. profit = 100, performance fees = 20% protocol fees = 10%. Then total fees charged = 100 * .2 = 20 of which 10% is sent to the protocol fee recipient (2) and 90% (18) is sent the strategy specific `performanceFeeRecipient`. - -### Profit Distribution -Profit from report calls will accumulate in a buffer. This buffer will be linearly unlocked over the locking period seconds at profitUnlockingRate. - -Profits will be locked for a max period of time of profitMaxUnlockTime seconds and will be gradually distributed. To avoid spending too much gas for profit unlock, the amount of time a profit will be locked is a weighted average between the new profit and the previous profit. - -new_locking_period = current_locked_profit * pending_time_to_unlock + new_profit * PROFIT_MAX_UNLOCK_TIME / (current_locked_profit + new_profit) -new_profit_unlocking_rate = (locked_profit + new_profit) / new_locking_period - -Losses will be offset by locked profit, if possible. - -The issue of new shares due to fees will also unlock profit so that PPS does not go down. - -Both of this offsets will prevent front running (as the profit was already earned and was not distributed yet) - -### Strategy Management -Strategy management is held by the 'management' address that can be updated by the current 'managment'. Changing 'management' is a two-step process, so first the current management will have to set 'pendingManagement' then that pending management will need to accept the role. - -Management has the ability to set all the configurable variables for their specific Strategy. - -The base strategy has purposely been written to limit the actual control management has over any important functionality. Meaning they are not capable of stealing any funds from the strategy or otherwise tampering with deposited funds, unless purposefully written in within their specific Strategy. - -The configurable variables within managements control are: - -#### Setting Pending Management -This allows the current management to set a new non-zero address to take over as the management of the strategy. - -#### Accepting Management -This allows the current 'pendingManagement' to accept the ownership of the contract. - -#### Setting the Keeper -Setting the address that is also allowed to call report and tend functions. - -#### Setting Performance Fee -Setting the percent in terms of basis points for the amount of profit to be charged as a fee. - -This has a minimum of 5% and a maximum of 50%. - -#### Setting Performance Fee Recipient -Setting the non-zero address that will receive any shares issued as a result of the performance fee. - -#### Setting the Profit Unlock Period -Sets the time in seconds that controls how fast profits will unlock. - -This can be customized based on the strategy. Based on aspects such as TVL, expected returns etc. - -## ERC4626 Compliance -Strategy Shares are ERC4626 compliant. - -## Emergency Operation -There is default emergency functions built in. First of which is `shutdownStrategy`. This can only ever be called by the management address and is non-reversible. - -Once this is called it will stop any further deposit or mints but will have no effect on any other functionality including withdraw, redeem, report and tend. This is to all the management to continue potentially recording profits or losses and users to withdraw even post shut down. - -This can be used in an emergency or simply to retire a vault. - -Once a strategy is shutdown management can also call `emergencyWithdraw(amount)`. Which will tell the strategy to withdraw a specified `amount` from the yield source and keep it as idle in the vault. This function will also do any needed updates to `totalDebt` and `totalIdle`, based on amounts withdrawn to assure withdraws continue to function properly. - -All other emergency functionality is left up to the individual strategist. - -### Withdrawals -Withdrawals can't be paused under any circumstance unless built in a specific implementation. - - -## Use -A strategist can simply inherit the BaseTokenizedStrategy.sol contract and override 3 simple functions with their specific needs. - -The strategies code has been designed as a non-opinionated system to distribute funds of depositors to a single yield generating opportunity while managing accounting in a robust way. - -The depositors receive shares of the strategy representing their relative share that can then be redeemed or used as yield-bearing tokens. - -The Strategy does not have a preference on any of the dimensions that should be considered when operating a strategy: -- **Decentralization**: management and keeper roles can be handled by EOA's, multi-sigs or any other form of governance. -- **Permissionlessness**: The strategies default to be fully permissioned. However, any strategist can easily implement white lists or any other method they desire. -- **Liquidity**: The strategy can be fully liquid at any time or only allow withdraws of idle funds, depending on the strategy implementation. -- **Risk**: Strategy developers can deploy funds into any opportunity they desire no matter the expected risks or returns. -- **Automation**: all the required actions to maintain the vault can be called by bots or manually, depending on periphery implementation - -The compromises will come with the specific yield-generating opportunity and parameters used by the strategies management. - -This allows different players to deploy their own version and implement their own constraints (or not use any at all) - - -### Example constraints -- **Illiquid Strategy**: A strategy must join AMM pools, which can be sandwiched by permissionless deposits/withdraws. So it only deposits during reports or tend calls from a trusted relay and limits withdraws to the amount of asset currently loose within the contract. -- **Permissioned Version**: A strategy decides to only allow a certain address deposit into the vault by overriding maxAvailableDeposit. -- **Risk**: A strategist implements an options strategy that can create large positive gains or potentially loose all deposited funds. - -## Development -Strategists should be able to use a pre-built "Strategy Mix" that will contain the imported BaseTokenizedStrategy.sol as well as standardized tests for any 4626 vault. Developing a strategy can be as simple as overriding three functions, with the potential for any number of other constraints or actions to be built on top of it. The Base implementation is only ~2KB, meaning there is plenty of room for strategists to build complex implementations while not having to be concerned with the generic functionality. - - -### Needed to Override -- `_deployFunds(uint256 _amount)`: This function is called after every deposit or mint. Its only job is to deposit up to the `_amount` of `asset`. -- `_freeFunds(uint256 _amount)`: This function is called during every withdrawal or redemption and should attempt to simply withdraw the '_amount' of 'asset'. Any difference between _amount and whats actually withdrawn will be counted as a loss -- `_harvestandReport()`: This function is used during a report and should accrue all rewards and return the total amount of 'asset' the strategy currently has in its control. - -### Optional to Override -While it can be possible to deploy a completely ERC-4626 compliant vault with just those three functions it does allow for further customization if the strategist desires. - -- `_tend` and `tendTrigger` can be overridden to signal to keepers the need for any sort of maintenance or reward selling between reports. -- `maxAvailableDeposit(address _owner)` can be overridden to implement any type of deposit limit. -- `maxAvailableWithdraw(address _owner)` can be used to limit the amount that a user can withdraw at any given moment. -- `_emergencyWithdraw(uint256 _amount)` can be overridden to provide a manual method for management to pull funds from a yield source in an emergency when the vault is shut down. - -## Deployment -All strategies deployed will have the address of the deployed 'TokenizedStrategy' set as a constant to be used as the address to forward all external calls to that are not defined in the Strategy. - -When deploying a new Strategy, it requires the following parameters: -- `asset`: address of the ERC20 token that can be deposited in the strategy -- `name`: name of Shares as described in ERC20 - -All other parameters will default to generic values and can be adjusted post-deployment by the deployer if desired. # Read More - https://github.com/yearn/yearn-vaults-v3 -- https://github.com/yearn/yearn-vaults-v3/blob/master/TECH_SPEC.md - https://github.com/yearn/tokenized-strategy - https://medium.com/iearn/yearnv3-motivation-and-design-107840cb4844 - https://medium.com/iearn/yearn-vaults-v3-36ce7c468ca0 - https://erc4626.info/ -- https://eips.ethereum.org/EIPS/eip-4626 +- https://eips.ethereum.org/EIPS/eip-4626 \ No newline at end of file diff --git a/sidebars/sidebarsDevelopers.js b/sidebars/sidebarsDevelopers.js index dc1daab0b2..a4a58242e3 100644 --- a/sidebars/sidebarsDevelopers.js +++ b/sidebars/sidebarsDevelopers.js @@ -37,7 +37,7 @@ module.exports = { label: 'V3', items: [ 'v3/overview', - 'v3/strategy_development', + 'v3/strategy_writing_guide', 'v3/protocol_fees', 'v3/vault_management', ],