Skip to content

Commit

Permalink
feat: update to 3.0.1 (#17)
Browse files Browse the repository at this point in the history
* chore: remove old depend

* forge install: tokenized-strategy

v3.0.1

* chore: update strategy depend

* build: update to new version

* forge install: tokenized-strategy-periphery

56a1c6d0cbbccfd7e74ce51b9f050af97cb9ffa7

* test: update trigger

* fix: lint
  • Loading branch information
Schlagonia authored Oct 13, 2023
1 parent e7b8092 commit f34fea4
Show file tree
Hide file tree
Showing 11 changed files with 60 additions and 111 deletions.
3 changes: 2 additions & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
[submodule "lib/tokenized-strategy"]
path = lib/tokenized-strategy
url = https://github.com/yearn/tokenized-strategy
branch = v3.0.0
branch = v3.0.1
[submodule "lib/tokenized-strategy-periphery"]
path = lib/tokenized-strategy-periphery
url = https://github.com/yearn/tokenized-strategy-periphery
branch = 3.0.1
60 changes: 2 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,59 +48,11 @@ make test

## Strategy Writing

### Good to know

To create your tokenized Strategy, you must override at least 3 functions outlined in `Strategy.sol`. An in-depth description for each function is provided above each function in `Strategy.sol`.

It is important to remember the default behavior for any tokenized strategy is to be a permissionless vault, so functions such as _deployFunds and _freeFunds can be called by anyone, and care should be taken when implementing manipulatable logic such as swaps/LP movements. Strategists can choose to limit deposit/withdraws by overriding the `availableWithdrawLimit` and `availableDepositLimit` function if it is needed for safety.

It is recommended to build strategies on the assumption that reports will happen based on the strategies specific `profitMaxUnlockTime`. Since this is 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.

The only default global variables from the BaseTokenizedStrategy 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");

NOTE: It is impossible to write to a strategy's default global storage state internally post-deployment. You must make external calls from the `management` address to configure any of the desired variables.

To include permissioned functions such as extra setters, the two modifiers of `onlyManagement` and `onlyManagementAndKeepers` are available by default.

For strategies that will be used with multiple different asset's it is recommended to build a factory, that can be deployed once and then all strategies can be deployed on chain. Cloning is not recommended for Tokenized Strategies.

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. Standard naming conventions will include the asset name, the protocol used to generate yield, and the method rewards are sold if applicable. I.e., "Weth-AaveV3Lender-UniV3Swapper".

There is an optional `_emergencyWithdraw` function that can be overridden to specify logic to remove funds from the strategy specific yield source in an emergency. This function can only be used if a strategy is shutdown. It is meant to simply withdraw funds and keep them idle in the strategy to service withdraws.

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.

## 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/Schlagonia/tokenized-strategy-periphery.

All periphery contracts are optional, and strategists are free to choose if they wish to use them.

### Swappers

In order to make reward swapping as easy and standardized as possible there are multiple swapper contracts that 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, amount, minAmountOut)` to swap any tokens easily during `_harvestAndReport`.

### APR Oracles

In order for easy integration with Vaults, front ends, debt allocators etc. There is the option to create an APR oracle contract for your specific strategy that should return the expected APR of the Strategy based on some given `debtChange`.

### HealthCheck

In order to prevent automated reports from reporting losses/excessive profits from automated reports that may not be accurate, a strategist can inherit and implement the HealtCheck contract. Using this can assure 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 assure 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

The expected behavior is that strategies report profits/losses on a set 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 that can be added to the default contract for a specific strategy.
For a complete guide to creating a Tokenized Strategy please visit: https://docs.yearn.fi/developers/v3/strategy_development

## Testing

Due to the nature of the BaseTokenizedStrategy utilizing an external contract for the majority of its logic, the default interface for any tokenized strategy will not allow proper testing of all functions. Testing of your Strategy should utilize the pre-built [IStrategyInterface](https://github.com/Schlagonia/tokenized-strategy-foundry-mix/blob/master/src/interfaces/IStrategyInterface.sol) to cast any deployed strategy through for testing, as seen in the Setup example. You can add any external functions that you add for your specific strategy to this interface to be able to test all functions with one variable.
Due to the nature of the BaseStrategy utilizing an external contract for the majority of its logic, the default interface for any tokenized strategy will not allow proper testing of all functions. Testing of your Strategy should utilize the pre-built [IStrategyInterface](https://github.com/Schlagonia/tokenized-strategy-foundry-mix/blob/master/src/interfaces/IStrategyInterface.sol) to cast any deployed strategy through for testing, as seen in the Setup example. You can add any external functions that you add for your specific strategy to this interface to be able to test all functions with one variable.

Example:

Expand Down Expand Up @@ -139,16 +91,8 @@ See here for some tips on testing [`Testing Tips`](https://book.getfoundry.sh/fo

When testing on chains other than mainnet you will need to make sure a valid `CHAIN_RPC_URL` for that chain is set in your .env and that chain's specific api key is set for `ETHERSCAN_API_KEY`. You will then need to simply adjust the variable that RPC_URL is set to in the Makefile to match your chain.

### Errors

To update to a new API version of the TokenizeStrategy you will need to simply remove and reinstall the dependency.

```sh
git rm -r lib/tokenized-strategy/

forge install yearn/tokenized-strategy@API_VERSION
```

### Deployment

#### Contract Verification
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"prettier": "^2.5.1",
"prettier-plugin-solidity": "^1.0.0-beta.19",
"pretty-quick": "^3.1.3",
"solc": "^0.8.12",
"solc": "0.8.18",
"solhint": "^3.3.7",
"solhint-plugin-prettier": "^0.0.5"
},
Expand Down
66 changes: 33 additions & 33 deletions src/Strategy.sol
Original file line number Diff line number Diff line change
@@ -1,54 +1,52 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.18;

import {BaseTokenizedStrategy} from "@tokenized-strategy/BaseTokenizedStrategy.sol";

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {BaseStrategy, ERC20} from "@tokenized-strategy/BaseStrategy.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

// Import interfaces for many popular DeFi projects, or add your own!
//import "../interfaces/<protocol>/<Interface>.sol";

/**
* The `TokenizedStrategy` variable can be used to retrieve the strategies
* specifc storage data your contract.
* specific storage data your contract.
*
* i.e. uint256 totalAssets = TokenizedStrategy.totalAssets()
*
* This can not be used for write functions. Any TokenizedStrategy
* variables that need to be udpated post deployement will need to
* variables that need to be updated post deployment will need to
* come from an external call from the strategies specific `management`.
*/

// NOTE: To implement permissioned functions you can use the onlyManagement and onlyKeepers modifiers
// NOTE: To implement permissioned functions you can use the onlyManagement, onlyEmergencyAuthorized and onlyKeepers modifiers

contract Strategy is BaseTokenizedStrategy {
contract Strategy is BaseStrategy {
using SafeERC20 for ERC20;

constructor(
address _asset,
string memory _name
) BaseTokenizedStrategy(_asset, _name) {}
) BaseStrategy(_asset, _name) {}

/*//////////////////////////////////////////////////////////////
NEEDED TO BE OVERRIDEN BY STRATEGIST
NEEDED TO BE OVERRIDDEN BY STRATEGIST
//////////////////////////////////////////////////////////////*/

/**
* @dev Should deploy up to '_amount' of 'asset' in the yield source.
*
* This function is called at the end of a {deposit} or {mint}
* call. Meaning that unless a whitelist is implemented it will
* be entirely permsionless and thus can be sandwhiched or otherwise
* be entirely permissionless and thus can be sandwiched or otherwise
* manipulated.
*
* @param _amount The amount of 'asset' that the strategy should attemppt
* @param _amount The amount of 'asset' that the strategy should attempt
* to deposit in the yield source.
*/
function _deployFunds(uint256 _amount) internal override {
// TODO: implement deposit logice EX:
// TODO: implement deposit logic EX:
//
// lendingpool.deposit(asset, _amount ,0);
// lendingPool.deposit(address(asset), _amount ,0);
}

/**
Expand All @@ -59,11 +57,11 @@ contract Strategy is BaseTokenizedStrategy {
*
* This function is called during {withdraw} and {redeem} calls.
* Meaning that unless a whitelist is implemented it will be
* entirely permsionless and thus can be sandwhiched or otherwise
* entirely permissionless and thus can be sandwiched or otherwise
* manipulated.
*
* Should not rely on asset.balanceOf(address(this)) calls other than
* for diff accounting puroposes.
* for diff accounting purposes.
*
* Any difference between `_amount` and what is actually freed will be
* counted as a loss and passed on to the withdrawer. This means
Expand All @@ -75,7 +73,7 @@ contract Strategy is BaseTokenizedStrategy {
function _freeFunds(uint256 _amount) internal override {
// TODO: implement withdraw logic EX:
//
// lendingPool.withdraw(asset, _amount);
// lendingPool.withdraw(address(asset), _amount);
}

/**
Expand Down Expand Up @@ -107,9 +105,12 @@ contract Strategy is BaseTokenizedStrategy {
{
// TODO: Implement harvesting logic and accurate accounting EX:
//
// _claminAndSellRewards();
// _totalAssets = aToken.balanceof(address(this)) + ERC20(asset).balanceOf(address(this));
_totalAssets = ERC20(asset).balanceOf(address(this));
// if(!TokenizedStrategy.isShutdown()) {
// _claimAndSellRewards();
// }
// _totalAssets = aToken.balanceOf(address(this)) + asset.balanceOf(address(this));
//
_totalAssets = asset.balanceOf(address(this));
}

/*//////////////////////////////////////////////////////////////
Expand All @@ -122,15 +123,15 @@ contract Strategy is BaseTokenizedStrategy {
*
* If '_tend' is used tendTrigger() will also need to be overridden.
*
* This call can only be called by a persionned role so may be
* This call can only be called by a permissioned role so may be
* through protected relays.
*
* This can be used to harvest and compound rewards, deposit idle funds,
* perform needed poisition maintence or anything else that doesn't need
* perform needed position maintenance or anything else that doesn't need
* a full report for.
*
* EX: A strategy that can not deposit funds without getting
* sandwhiched can use the tend when a certain threshold
* sandwiched can use the tend when a certain threshold
* of idle to totalAssets has been reached.
*
* The TokenizedStrategy contract will do all needed debt and idle updates
Expand All @@ -143,19 +144,18 @@ contract Strategy is BaseTokenizedStrategy {
*/

/**
* @notice Returns wether or not tend() should be called by a keeper.
* @dev Optional trigger to override if tend() will be used by the strategy.
* This must be implemented if the strategy hopes to invoke _tend().
*
* @return . Should return true if tend() should be called by keeper or false if not.
*
function tendTrigger() public view override returns (bool) {}
function _tendTrigger() public view override returns (bool) {}
*/

/**
* @notice Gets the max amount of `asset` that an adress can deposit.
* @notice Gets the max amount of `asset` that an address can deposit.
* @dev Defaults to an unlimited amount for any address. But can
* be overriden by strategists.
* be overridden by strategists.
*
* This function will be called before any deposit or mints to enforce
* any limits desired by the strategist. This can be used for either a
Expand All @@ -171,7 +171,7 @@ contract Strategy is BaseTokenizedStrategy {
* by `totalSupply`.
*
* @param . The address that is depositing into the strategy.
* @return . The avialable amount the `_owner` can deposit in terms of `asset`
* @return . The available amount the `_owner` can deposit in terms of `asset`
*
function availableDepositLimit(
address _owner
Expand All @@ -187,11 +187,11 @@ contract Strategy is BaseTokenizedStrategy {
/**
* @notice Gets the max amount of `asset` that can be withdrawn.
* @dev Defaults to an unlimited amount for any address. But can
* be overriden by strategists.
* be overridden by strategists.
*
* This function will be called before any withdraw or redeem to enforce
* any limits desired by the strategist. This can be used for illiquid
* or sandwhichable strategies. It should never be lower than `totalIdle`.
* or sandwichable strategies. It should never be lower than `totalIdle`.
*
* EX:
* return TokenIzedStrategy.totalIdle();
Expand All @@ -200,7 +200,7 @@ contract Strategy is BaseTokenizedStrategy {
* or conversion rates from shares to assets.
*
* @param . The address that is withdrawing from the strategy.
* @return . The avialable amount that can be withdrawn in terms of `asset`
* @return . The available amount that can be withdrawn in terms of `asset`
*
function availableWithdrawLimit(
address _owner
Expand All @@ -220,7 +220,7 @@ contract Strategy is BaseTokenizedStrategy {
* This should attempt to free `_amount`, noting that `_amount` may
* be more than is currently deployed.
*
* NOTE: This will not realize any profits or losses. A seperate
* NOTE: This will not realize any profits or losses. A separate
* {report} will be needed in order to record any profit/loss. If
* a report may need to be called after a shutdown it is important
* to check if the strategy is shutdown during {_harvestAndReport}
Expand All @@ -237,8 +237,8 @@ contract Strategy is BaseTokenizedStrategy {
TODO: If desired implement simple logic to free deployed funds.
EX:
_amount = min(_amount, atoken.balanceOf(address(this)));
lendingPool.withdraw(asset, _amount);
_amount = min(_amount, aToken.balanceOf(address(this)));
_freeFunds(_amount);
}
*/
Expand Down
2 changes: 1 addition & 1 deletion src/periphery/StrategyAprOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity 0.8.18;
import {AprOracleBase} from "@periphery/AprOracle/AprOracleBase.sol";

contract StrategyAprOracle is AprOracleBase {
constructor() AprOracleBase("Strategy Apr Oracle Example") {}
constructor() AprOracleBase("Strategy Apr Oracle Example", msg.sender) {}

/**
* @notice Will return the expected Apr of a strategy post a debt change.
Expand Down
20 changes: 13 additions & 7 deletions src/test/Operation.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ contract OperationTest is Setup {
vm.assume(_amount > minFuzzAmount && _amount < maxFuzzAmount);
_profitFactor = uint16(bound(uint256(_profitFactor), 10, MAX_BPS));

// Set protofol fee to 0 and perf fee to 10%
// Set protocol fee to 0 and perf fee to 10%
setFees(0, 1_000);

// Deposit into strategy
Expand Down Expand Up @@ -166,31 +166,37 @@ contract OperationTest is Setup {
function test_tendTrigger(uint256 _amount) public {
vm.assume(_amount > minFuzzAmount && _amount < maxFuzzAmount);

assertTrue(!strategy.tendTrigger());
(bool trigger, ) = strategy.tendTrigger();
assertTrue(!trigger);

// Deposit into strategy
mintAndDepositIntoStrategy(strategy, user, _amount);

assertTrue(!strategy.tendTrigger());
(trigger, ) = strategy.tendTrigger();
assertTrue(!trigger);

// Skip some time
skip(1 days);

assertTrue(!strategy.tendTrigger());
(trigger, ) = strategy.tendTrigger();
assertTrue(!trigger);

vm.prank(keeper);
strategy.report();

assertTrue(!strategy.tendTrigger());
(trigger, ) = strategy.tendTrigger();
assertTrue(!trigger);

// Unlock Profits
skip(strategy.profitMaxUnlockTime());

assertTrue(!strategy.tendTrigger());
(trigger, ) = strategy.tendTrigger();
assertTrue(!trigger);

vm.prank(user);
strategy.redeem(_amount, user, user);

assertTrue(!strategy.tendTrigger());
(trigger, ) = strategy.tendTrigger();
assertTrue(!trigger);
}
}
4 changes: 2 additions & 2 deletions src/test/Oracle.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ contract OracleTest is Setup {

mintAndDepositIntoStrategy(strategy, user, _amount);

// TODO: adjust the number to base _perfenctChange off of.
// TODO: adjust the number to base _percentChange off of.
uint256 _delta = (_amount * _percentChange) / MAX_BPS;

checkOracle(address(strategy), _delta);
}

// TODO: Deploy multiple strategies with differen tokens as `asset` to test against the oracle.
// TODO: Deploy multiple strategies with different tokens as `asset` to test against the oracle.
}
2 changes: 1 addition & 1 deletion src/test/Shutdown.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ contract ShutdownTest is Setup {
super.setUp();
}

function test_shudownCanWithdraw(uint256 _amount) public {
function test_shutdownCanWithdraw(uint256 _amount) public {
vm.assume(_amount > minFuzzAmount && _amount < maxFuzzAmount);

// Deposit into strategy
Expand Down
Loading

0 comments on commit f34fea4

Please sign in to comment.