diff --git a/contracts/StrategyConvexFactoryClonable.sol b/contracts/StrategyConvexFactoryClonable.sol index a95af8b..d1aea69 100644 --- a/contracts/StrategyConvexFactoryClonable.sol +++ b/contracts/StrategyConvexFactoryClonable.sol @@ -72,7 +72,7 @@ contract StrategyConvexFactoryClonable is BaseStrategy { bool public isOriginal = true; /// @notice Used to track the deployed version of this contract. Maps to releases in the CurveVaultFactory repo. - string public constant strategyVersion = "3.0.2"; + string public constant strategyVersion = "4.1.0"; /* ========== CONSTRUCTOR ========== */ diff --git a/contracts/StrategyConvexFraxFactoryClonable.sol b/contracts/StrategyConvexFraxFactoryClonable.sol index 26d005b..20fe906 100644 --- a/contracts/StrategyConvexFraxFactoryClonable.sol +++ b/contracts/StrategyConvexFraxFactoryClonable.sol @@ -118,7 +118,7 @@ contract StrategyConvexFraxFactoryClonable is BaseStrategy { KekInfo public kekInfo; /// @notice Used to track the deployed version of this contract. Maps to releases in the CurveVaultFactory repo. - string public constant strategyVersion = "3.0.2"; + string public constant strategyVersion = "4.1.0"; /* ========== CONSTRUCTOR ========== */ diff --git a/contracts/StrategyConvexFxnFactoryClonable.sol b/contracts/StrategyConvexFxnFactoryClonable.sol new file mode 100644 index 0000000..687edd6 --- /dev/null +++ b/contracts/StrategyConvexFxnFactoryClonable.sol @@ -0,0 +1,487 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; + +import {Math} from "@openzeppelin/contracts@4.9.3/utils/math/Math.sol"; +import "github.com/yearn/yearn-vaults/blob/v0.4.6/contracts/BaseStrategy.sol"; +import {IConvexFxn, ITradeFactory, IDetails} from "./interfaces/ConvexFxnInterfaces.sol"; + +contract StrategyConvexFxnFactoryClonable is BaseStrategy { + using SafeERC20 for IERC20; + + /* ========== STATE VARIABLES ========== */ + + /// @notice This is the f(x) Booster. + IConvexFxn public constant fxnBooster = + IConvexFxn(0xAffe966B27ba3E4Ebb8A0eC124C7b7019CC762f8); + + /// @notice This is the f(x) Pool Registry. + IConvexFxn public constant fxnPoolRegistry = + IConvexFxn(0xdB95d646012bB87aC2E6CD63eAb2C42323c1F5AF); + + /// @notice This is a unique numerical identifier for each Convex f(x) pool. + uint256 public fxnPid; + + /// @notice This is the FXN gauge address for our LP token. Different from Curve gauge. + address public fxnGauge; + + /// @notice This is the vault our strategy uses to stake on f(x) and use Convex boost. + IConvexFxn public userVault; + + // this means all of our fee values are in basis points + uint256 internal constant FEE_DENOMINATOR = 10000; + + /// @notice The address of our f(x) token (FXN). + IERC20 public constant fxn = + IERC20(0x365AccFCa291e7D3914637ABf1F7635dB165Bb09); + + // ySwaps stuff + /// @notice The address of our ySwaps trade factory. + address public tradeFactory; + + /// @notice Array of any extra rewards tokens this pool may have. Add CRV and CVX if those rewards start flowing. + address[] public rewardsTokens; + + /// @notice Will only be true on the original deployed contract and not on clones; we do not want to clone a clone. + bool public isOriginal = true; + + /// @notice Used to track the deployed version of this contract. Maps to releases in the CurveVaultFactory repo. + string public constant strategyVersion = "4.1.0"; + + /* ========== CONSTRUCTOR ========== */ + + constructor( + address _vault, + address _tradeFactory, + uint256 _fxnPid + ) BaseStrategy(_vault) { + _initializeStrat(_tradeFactory, _fxnPid); + } + + /* ========== CLONING ========== */ + + event Cloned(address indexed clone); + + /** + * @notice Use this to clone an exact copy of this strategy on another vault. + * @dev In practice, this will only be called by the factory on the template contract. + * @param _vault Vault address we are targeting with this strategy. + * @param _strategist Address to grant the strategist role. + * @param _rewards If we have any strategist rewards, send them here. + * @param _keeper Address to grant the keeper role. + * @param _tradeFactory Our trade factory address. + * @param _fxnPid Our fxn pool id (pid) for this strategy. + */ + function cloneStrategyConvexFxn( + address _vault, + address _strategist, + address _rewards, + address _keeper, + address _tradeFactory, + uint256 _fxnPid + ) external returns (address newStrategy) { + // dont clone a clone + if (!isOriginal) { + revert("Cant clone a clone"); + } + + // Copied from https://github.com/optionality/clone-factory/blob/master/contracts/CloneFactory.sol + bytes20 addressBytes = bytes20(address(this)); + assembly { + // EIP-1167 bytecode + let clone_code := mload(0x40) + mstore( + clone_code, + 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000 + ) + mstore(add(clone_code, 0x14), addressBytes) + mstore( + add(clone_code, 0x28), + 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000 + ) + newStrategy := create(0, clone_code, 0x37) + } + + StrategyConvexFxnFactoryClonable(newStrategy).initialize( + _vault, + _strategist, + _rewards, + _keeper, + _tradeFactory, + _fxnPid + ); + + emit Cloned(newStrategy); + } + + /** + * @notice Initialize the strategy. + * @dev This should only be called by the clone function above. + * @param _vault Vault address we are targeting with this strategy. + * @param _strategist Address to grant the strategist role. + * @param _rewards If we have any strategist rewards, send them here. + * @param _keeper Address to grant the keeper role. + * @param _tradeFactory Our trade factory address. + * @param _fxnPid Our fxn pool id (pid) for this strategy. + */ + function initialize( + address _vault, + address _strategist, + address _rewards, + address _keeper, + address _tradeFactory, + uint256 _fxnPid + ) public { + _initialize(_vault, _strategist, _rewards, _keeper); + _initializeStrat(_tradeFactory, _fxnPid); + } + + // this is called by our original strategy, as well as any clones + function _initializeStrat(address _tradeFactory, uint256 _fxnPid) internal { + // make sure that we havent initialized this before + if (fxnGauge != address(0)) { + revert("Already initialized"); + } + + // use pid to get our staking and lp addresses, check against want + (, address _gauge, address lptoken, , ) = fxnPoolRegistry.poolInfo( + _fxnPid + ); + if (address(lptoken) != address(want)) { + revert("wrong pid"); + } + + // 1:1 assignments + tradeFactory = _tradeFactory; + fxnPid = _fxnPid; + fxnGauge = _gauge; + + // have our strategy deploy our vault from the booster using the fxnPid + userVault = IConvexFxn(fxnBooster.createVault(_fxnPid)); + + // want = Curve LP + want.approve(address(userVault), type(uint256).max); + + // set up our baseStrategy vars + minReportDelay = 3650 days; + creditThreshold = 50_000e18; + + // set up trade factory + _setUpTradeFactory(); + } + + /* ========== VIEWS ========== */ + + /** + * @notice Strategy name. + * @return strategyName Strategy name. + */ + function name() + external + view + override + returns (string memory strategyName) + { + return + string( + abi.encodePacked( + "StrategyConvexFxnFactory-", + IDetails(address(want)).symbol() + ) + ); + } + + /** + * @notice Balance of want staked in Convex f(x). + * @return balanceStaked Balance of want staked in Convex f(x). + */ + function stakedBalance() public view returns (uint256 balanceStaked) { + balanceStaked = IERC20(fxnGauge).balanceOf(address(userVault)); + } + + /** + * @notice Balance of want sitting in our strategy. + * @return wantBalance Balance of want sitting in our strategy. + */ + function balanceOfWant() public view returns (uint256 wantBalance) { + wantBalance = want.balanceOf(address(this)); + } + + /** + * @notice Total assets the strategy holds, sum of loose and staked want. + * @return totalAssets Total assets the strategy holds, sum of loose and staked want. + */ + function estimatedTotalAssets() + public + view + override + returns (uint256 totalAssets) + { + totalAssets = balanceOfWant() + stakedBalance(); + } + + /* ========== CORE STRATEGY FUNCTIONS ========== */ + + function prepareReturn( + uint256 _debtOutstanding + ) + internal + override + returns (uint256 _profit, uint256 _loss, uint256 _debtPayment) + { + // rewards will be converted later with mev protection by yswaps (tradeFactory) + userVault.getReward(); + + // serious loss should never happen, but if it does (for instance, if Curve is hacked), lets record it + uint256 assets = estimatedTotalAssets(); + uint256 debt = vault.strategies(address(this)).totalDebt; + + // if assets are greater than debt, things are working great! + if (assets >= debt) { + _profit = assets - debt; + _debtPayment = _debtOutstanding; + + uint256 toFree = _profit + _debtPayment; + + // freed is math.min(wantBalance, toFree) + (uint256 freed, ) = liquidatePosition(toFree); + + if (toFree > freed) { + if (_debtPayment > freed) { + _debtPayment = freed; + _profit = 0; + } else { + _profit = freed - _debtPayment; + } + } + } + // if assets are less than debt, we are in trouble. dont worry about withdrawing here, just report losses + else { + _loss = debt - assets; + } + } + + function adjustPosition(uint256 _debtOutstanding) internal override { + // if in emergency exit, we dont want to deploy any more funds + if (emergencyExit) { + return; + } + + // Send all of our Curve pool tokens to be deposited + uint256 _toInvest = balanceOfWant(); + + if (_toInvest > 0) { + userVault.deposit(_toInvest); + } + } + + function liquidatePosition( + uint256 _amountNeeded + ) internal override returns (uint256 _liquidatedAmount, uint256 _loss) { + // check our loose want + uint256 _wantBal = balanceOfWant(); + if (_amountNeeded > _wantBal) { + uint256 _stakedBal = stakedBalance(); + if (_stakedBal > 0) { + uint256 _neededFromStaked; + unchecked { + _neededFromStaked = _amountNeeded - _wantBal; + } + // withdraw whatever extra funds we need + userVault.withdraw(Math.min(_stakedBal, _neededFromStaked)); + } + uint256 _withdrawnBal = balanceOfWant(); + _liquidatedAmount = Math.min(_amountNeeded, _withdrawnBal); + unchecked { + _loss = _amountNeeded - _liquidatedAmount; + } + } else { + // we have enough balance to cover the liquidation available + return (_amountNeeded, 0); + } + } + + // fire sale, get rid of it all! + function liquidateAllPositions() internal override returns (uint256) { + uint256 _stakedBal = stakedBalance(); + if (_stakedBal > 0) { + // dont bother withdrawing zero, save gas where we can + userVault.withdraw(_stakedBal); + } + return balanceOfWant(); + } + + // migrate our want token to a new strategy if needed, claim rewards tokens as well unless its an emergency + function prepareMigration(address _newStrategy) internal override { + uint256 _stakedBal = stakedBalance(); + + if (_stakedBal > 0) { + userVault.withdraw(_stakedBal); + } + + uint256 fxnBal = fxn.balanceOf(address(this)); + + if (fxnBal > 0) { + fxn.safeTransfer(_newStrategy, fxnBal); + } + } + + // want is blocked by default, add any other tokens to protect from gov here. + function protectedTokens() + internal + view + override + returns (address[] memory) + {} + + /* ========== YSWAPS ========== */ + + /** + * @notice Use to add or update rewards, rebuilds tradefactory too + * @dev Do this before updating trade factory if we have extra rewards. + * @param _rewards Rewards tokens to add to our trade factory. + */ + function updateRewards( + address[] memory _rewards + ) external onlyVaultManagers { + address tf = tradeFactory; + _removeTradeFactoryPermissions(true); + rewardsTokens = _rewards; + + tradeFactory = tf; + _setUpTradeFactory(); + } + + /** + * @notice Use to update our trade factory. + * @dev Can only be called by governance. + * @param _newTradeFactory Address of new trade factory. + */ + function updateTradeFactory( + address _newTradeFactory + ) external onlyGovernance { + require( + _newTradeFactory != address(0), + "Cant remove with this function" + ); + _removeTradeFactoryPermissions(true); + tradeFactory = _newTradeFactory; + _setUpTradeFactory(); + } + + function _setUpTradeFactory() internal { + // approve and set up trade factory + address _tradeFactory = tradeFactory; + address _want = address(want); + + ITradeFactory tf = ITradeFactory(_tradeFactory); + + // enable if we have anything else + for (uint256 i; i < rewardsTokens.length; ++i) { + address _rewardsToken = rewardsTokens[i]; + require(_rewardsToken != address(want), "not rewards"); + IERC20(_rewardsToken).forceApprove( + _tradeFactory, + type(uint256).max + ); + tf.enable(_rewardsToken, _want); + } + + fxn.approve(_tradeFactory, type(uint256).max); + tf.enable(address(fxn), _want); + } + + /** + * @notice Use this to remove permissions from our current trade factory. + * @dev Once this is called, setUpTradeFactory must be called to get things working again. + * @param _disableTf Specify whether to disable the tradefactory when removing. Option given in case we need to get + * around a reverting disable. + */ + function removeTradeFactoryPermissions( + bool _disableTf + ) external onlyVaultManagers { + _removeTradeFactoryPermissions(_disableTf); + } + + function _removeTradeFactoryPermissions(bool _disableTf) internal { + address _tradeFactory = tradeFactory; + if (_tradeFactory == address(0)) { + return; + } + ITradeFactory tf = ITradeFactory(_tradeFactory); + + address _want = address(want); + + // disable for any other rewards tokens too + for (uint256 i; i < rewardsTokens.length; ++i) { + address _rewardsToken = rewardsTokens[i]; + IERC20(_rewardsToken).approve(_tradeFactory, 0); + if (_disableTf) { + tf.disable(_rewardsToken, _want); + } + } + + fxn.approve(_tradeFactory, 0); + if (_disableTf) { + tf.disable(address(fxn), _want); + } + + tradeFactory = address(0); + } + + /* ========== KEEP3RS ========== */ + + /** + * @notice + * Provide a signal to the keeper that harvest() should be called. + * + * Dont harvest if a strategy is inactive. + * If our profit exceeds our upper limit, then harvest no matter what. For our lower profit limit, credit + * threshold, max delay, and manual force trigger, only harvest if our gas price is acceptable. + * + * @param _callCostinEth The keepers estimated gas cost to call harvest() (in wei). + * @return True if harvest() should be called, false otherwise. + */ + function harvestTrigger( + uint256 _callCostinEth + ) public view override returns (bool) { + // Should not trigger if strategy is not active (no assets and no debtRatio). This means we dont need to adjust + // keeper job. + if (!isActive()) { + return false; + } + + // check if the base fee gas price is higher than we allow. if it is, block harvests. + if (!isBaseFeeAcceptable()) { + return false; + } + + // trigger if we want to manually harvest, but only if our gas price is acceptable + if (forceHarvestTriggerOnce) { + return true; + } + + StrategyParams memory params = vault.strategies(address(this)); + // harvest if we hit our minDelay, but only if our gas price is acceptable + if (block.timestamp - params.lastReport > minReportDelay) { + return true; + } + + // harvest our credit if its above our threshold + if (vault.creditAvailable() > creditThreshold) { + return true; + } + + // otherwise, we dont harvest + return false; + } + + /** + * @notice Convert our keepers eth cost into want + * @dev We dont use this since we dont factor call cost into our harvestTrigger. + * @param _ethAmount Amount of ether spent. + * @return Value of ether in want. + */ + function ethToWant( + uint256 _ethAmount + ) public view override returns (uint256) {} +} diff --git a/contracts/StrategyCurveBoostedFactoryClonable.sol b/contracts/StrategyCurveBoostedFactoryClonable.sol index 883fa16..5b655d1 100644 --- a/contracts/StrategyCurveBoostedFactoryClonable.sol +++ b/contracts/StrategyCurveBoostedFactoryClonable.sol @@ -40,7 +40,7 @@ contract StrategyCurveBoostedFactoryClonable is BaseStrategy { bool public isOriginal = true; /// @notice Used to track the deployed version of this contract. Maps to releases in the CurveVaultFactory repo. - string public constant strategyVersion = "3.0.2"; + string public constant strategyVersion = "4.1.0"; /* ========== CONSTRUCTOR ========== */ @@ -153,12 +153,9 @@ contract StrategyCurveBoostedFactoryClonable is BaseStrategy { proxy = ICurveStrategyProxy(_proxy); // our factory checks the latest proxy from curve voter and passes it here gauge = _gauge; - // want = Curve LP - want.approve(_proxy, type(uint256).max); - // set up our baseStrategy vars - minReportDelay = 21 days; - maxReportDelay = 365 days; + minReportDelay = 3650 days; + maxReportDelay = 36500 days; creditThreshold = 50_000e18; // ySwaps setup @@ -344,10 +341,12 @@ contract StrategyCurveBoostedFactoryClonable is BaseStrategy { /** * @notice Use to add or update rewards, rebuilds tradefactory too - * @dev Do this before updating trade factory if we have extra rewards. Can only be called by governance. + * @dev Do this before updating trade factory if we have extra rewards. * @param _rewards Rewards tokens to add to our trade factory. */ - function updateRewards(address[] memory _rewards) external onlyGovernance { + function updateRewards( + address[] memory _rewards + ) external onlyVaultManagers { address tf = tradeFactory; _removeTradeFactoryPermissions(true); rewardsTokens = _rewards; @@ -385,7 +384,14 @@ contract StrategyCurveBoostedFactoryClonable is BaseStrategy { // enable for all rewards tokens too for (uint256 i; i < rewardsTokens.length; ++i) { address _rewardsToken = rewardsTokens[i]; - IERC20(_rewardsToken).approve(_tradeFactory, type(uint256).max); + require( + _rewardsToken != address(want) && _rewardsToken != gauge, + "not rewards" + ); + IERC20(_rewardsToken).forceApprove( + _tradeFactory, + type(uint256).max + ); tf.enable(_rewardsToken, _want); } } @@ -449,12 +455,6 @@ contract StrategyCurveBoostedFactoryClonable is BaseStrategy { return false; } - StrategyParams memory params = vault.strategies(address(this)); - // harvest no matter what once we reach our maxDelay - if (block.timestamp - params.lastReport > maxReportDelay) { - return true; - } - // check if the base fee gas price is higher than we allow. if it is, block harvests. if (!isBaseFeeAcceptable()) { return false; @@ -466,6 +466,7 @@ contract StrategyCurveBoostedFactoryClonable is BaseStrategy { } // harvest if we hit our minDelay, but only if our gas price is acceptable + StrategyParams memory params = vault.strategies(address(this)); if (block.timestamp - params.lastReport > minReportDelay) { return true; } @@ -509,10 +510,19 @@ contract StrategyCurveBoostedFactoryClonable is BaseStrategy { /** * @notice Use this to set or update our voter contracts. - * @dev For Curve strategies, this is where we send our keepCVX. Only governance can set this. + * @dev For Curve strategies, this is where we send our keepCRV. Only governance can set this. * @param _curveVoter Address of our curve voter. */ function setVoter(address _curveVoter) external onlyGovernance { curveVoter = _curveVoter; } + + /** + * @notice Use this to set or update our strategy proxy. + * @dev Only governance can set this. + * @param _strategyProxy Address of our curve strategy proxy. + */ + function setProxy(address _strategyProxy) external onlyGovernance { + proxy = ICurveStrategyProxy(_strategyProxy); + } } diff --git a/contracts/StrategyPrismaConvexFactoryClonable.sol b/contracts/StrategyPrismaConvexFactoryClonable.sol index 0bba1df..87964e2 100644 --- a/contracts/StrategyPrismaConvexFactoryClonable.sol +++ b/contracts/StrategyPrismaConvexFactoryClonable.sol @@ -11,8 +11,12 @@ contract StrategyPrismaConvexFactoryClonable is BaseStrategy { struct ClaimParams { /// @notice We use this flag to signal a desire to claim even if Yearns locker cant provide max boost. bool forceClaimOnce; - /// @notice Defaults to true, set to false to skip reward claiming altogether. + /// @notice Defaults to false, set to true to always claim rewards using yprisma when max boosted. bool shouldClaimRewards; + /// @notice Address for boost delegate. defaults to address(0), which will use yearn's boost + address boostDelegate; + /// @notice Max fee value to pass in when claiming rewards. uint80 to pack the struct. + uint80 maxFee; } // Fees are in basis points @@ -84,25 +88,17 @@ contract StrategyPrismaConvexFactoryClonable is BaseStrategy { bool public isOriginal = true; /// @notice Used to track the deployed version of this contract. Maps to releases in the CurveVaultFactory repo. - string public constant strategyVersion = "3.1.0"; + string public constant strategyVersion = "4.1.0"; /* ========== CONSTRUCTOR ========== */ constructor( address _vault, address _tradeFactory, - uint256 _harvestProfitMinInUsdc, - uint256 _harvestProfitMaxInUsdc, address _prismaVault, address _prismaReceiver ) BaseStrategy(_vault) { - _initializeStrat( - _tradeFactory, - _harvestProfitMinInUsdc, - _harvestProfitMaxInUsdc, - _prismaVault, - _prismaReceiver - ); + _initializeStrat(_tradeFactory, _prismaVault, _prismaReceiver); } /* ========== CLONING ========== */ @@ -117,8 +113,6 @@ contract StrategyPrismaConvexFactoryClonable is BaseStrategy { * @param _rewards If we have any strategist rewards, send them here. * @param _keeper Address to grant the keeper role. * @param _tradeFactory Our trade factory address. - * @param _harvestProfitMinInUsdc Minimum acceptable profit for a harvest. - * @param _harvestProfitMaxInUsdc Maximum acceptable profit for a harvest. * @param _prismaVault Address of the Prisma vault. * @param _prismaReceiver Address of the Prisma receiver to farm. */ @@ -128,8 +122,6 @@ contract StrategyPrismaConvexFactoryClonable is BaseStrategy { address _rewards, address _keeper, address _tradeFactory, - uint256 _harvestProfitMinInUsdc, - uint256 _harvestProfitMaxInUsdc, address _prismaVault, address _prismaReceiver ) external returns (address newStrategy) { @@ -161,8 +153,6 @@ contract StrategyPrismaConvexFactoryClonable is BaseStrategy { _rewards, _keeper, _tradeFactory, - _harvestProfitMinInUsdc, - _harvestProfitMaxInUsdc, _prismaVault, _prismaReceiver ); @@ -178,8 +168,6 @@ contract StrategyPrismaConvexFactoryClonable is BaseStrategy { * @param _rewards If we have any strategist rewards, send them here. * @param _keeper Address to grant the keeper role. * @param _tradeFactory Our trade factory address. - * @param _harvestProfitMinInUsdc Minimum acceptable profit for a harvest. - * @param _harvestProfitMaxInUsdc Maximum acceptable profit for a harvest. * @param _prismaVault Address of the Prisma vault to claim from. * @param _prismaReceiver Address of the Prisma receiver to farm. */ @@ -189,26 +177,16 @@ contract StrategyPrismaConvexFactoryClonable is BaseStrategy { address _rewards, address _keeper, address _tradeFactory, - uint256 _harvestProfitMinInUsdc, - uint256 _harvestProfitMaxInUsdc, address _prismaVault, address _prismaReceiver ) public { _initialize(_vault, _strategist, _rewards, _keeper); - _initializeStrat( - _tradeFactory, - _harvestProfitMinInUsdc, - _harvestProfitMaxInUsdc, - _prismaVault, - _prismaReceiver - ); + _initializeStrat(_tradeFactory, _prismaVault, _prismaReceiver); } // this is called by our original strategy, as well as any clones via the above function function _initializeStrat( address _tradeFactory, - uint256 _harvestProfitMinInUsdc, - uint256 _harvestProfitMaxInUsdc, address _prismaVault, address _prismaReceiver ) internal { @@ -219,8 +197,10 @@ contract StrategyPrismaConvexFactoryClonable is BaseStrategy { prismaReceiver = IPrismaReceiver(_prismaReceiver); prismaVault = IPrismaVault(_prismaVault); tradeFactory = _tradeFactory; - harvestProfitMinInUsdc = _harvestProfitMinInUsdc; - harvestProfitMaxInUsdc = _harvestProfitMaxInUsdc; + + // set these values high by default so they are essentially turned off + harvestProfitMinInUsdc = 20_000e6; + harvestProfitMaxInUsdc = 200_000e6; // want = Curve LP want.approve(address(_prismaReceiver), type(uint256).max); @@ -273,8 +253,22 @@ contract StrategyPrismaConvexFactoryClonable is BaseStrategy { returns (uint256 _profit, uint256 _loss, uint256 _debtPayment) { // rewards will be converted later with mev protection by yswaps (tradeFactory) + + // store claim params locally to save gas ClaimParams memory _claimParams = claimParams; - if (_claimParams.shouldClaimRewards) { + + // if boostDelegate is set, means this is part of a triggered claim + if (_claimParams.boostDelegate != address(0)) { + _claimRewards( + true, + _claimParams.boostDelegate, + _claimParams.maxFee + ); + // reset local vars to zero and do a single storage write to, you guessed it, save gas + _claimParams.boostDelegate = address(0); + claimParams.maxFee = 0; + claimParams = _claimParams; + } else if (_claimParams.shouldClaimRewards) { _claimRewards(_claimParams.forceClaimOnce, YEARN_LOCKER, 10_000); } @@ -419,6 +413,7 @@ contract StrategyPrismaConvexFactoryClonable is BaseStrategy { uint256 crvBal = crv.balanceOf(address(this)); uint256 cvxBal = convexToken.balanceOf(address(this)); + uint256 yprismaBal = yPrisma.balanceOf(address(this)); if (crvBal > 0) { crv.safeTransfer(_newStrategy, crvBal); @@ -426,6 +421,9 @@ contract StrategyPrismaConvexFactoryClonable is BaseStrategy { if (cvxBal > 0) { convexToken.safeTransfer(_newStrategy, cvxBal); } + if (yprismaBal > 0) { + yPrisma.safeTransfer(_newStrategy, yprismaBal); + } } // want is blocked by default, add any other tokens to protect from gov here. @@ -475,18 +473,36 @@ contract StrategyPrismaConvexFactoryClonable is BaseStrategy { /** * @notice Force a rewards claim from the receiver regardless of max boost. - * @dev Can only be called by managers. + * @dev Can only be called by emergency authorized (gov, mgmt, strategist, guardian). * @param _boostDelegate Address of the boost delegate to use. * @param _maxFee Max we fee are willing to pay for boost rental. */ function claimRewards( address _boostDelegate, uint256 _maxFee - ) external onlyVaultManagers { + ) external onlyEmergencyAuthorized { require(_boostDelegate != address(0)); _claimRewards(true, _boostDelegate, _maxFee); } + /** + * @notice Force a rewards claim by our keeper using the specified boost delegate. + * @dev Can only be called by emergency authorized (gov, mgmt, strategist, guardian). + * @param _boostDelegate Address of the boost delegate to use. + * @param _maxFee Max we fee are willing to pay for boost rental. + */ + function triggerClaimRewards( + address _boostDelegate, + uint80 _maxFee + ) external onlyEmergencyAuthorized { + require(_boostDelegate != address(0)); + ClaimParams memory _claimParams = claimParams; + _claimParams.boostDelegate = _boostDelegate; + _claimParams.maxFee = _maxFee; + claimParams = _claimParams; + forceHarvestTriggerOnce = true; + } + /* ========== YSWAPS ========== */ /** @@ -576,17 +592,21 @@ contract StrategyPrismaConvexFactoryClonable is BaseStrategy { function harvestTrigger( uint256 callCostinEth ) public view override returns (bool) { - // Should not trigger if strategy is not active (no assets and no debtRatio). This means we dont need to adjust keeper job. + // Should not trigger if strategy is inactive (no assets and no debtRatio). if (!isActive()) { return false; } + // store locally + bool _claimsAreMaxBoosted = claimsAreMaxBoosted(); + // harvest if we have a profit to claim at our upper limit without considering gas price ClaimParams memory _claimParams = claimParams; uint256 claimableProfit = claimableProfitInUsdc(); if ( claimableProfit > harvestProfitMaxInUsdc && - _claimParams.shouldClaimRewards + _claimParams.shouldClaimRewards && + (_claimsAreMaxBoosted || _claimParams.forceClaimOnce) ) { return true; } @@ -605,17 +625,15 @@ contract StrategyPrismaConvexFactoryClonable is BaseStrategy { if ( claimableProfit > harvestProfitMinInUsdc && _claimParams.shouldClaimRewards && - (claimsAreMaxBoosted() || _claimParams.forceClaimOnce) + (_claimsAreMaxBoosted || _claimParams.forceClaimOnce) ) { return true; } StrategyParams memory params = vault.strategies(address(this)); - // harvest regardless of profit once we reach our maxDelay and are max boosted. - if ( - block.timestamp - params.lastReport > maxReportDelay && - (claimsAreMaxBoosted() || _claimParams.forceClaimOnce) - ) { + // harvest regardless of profit once we reach our maxDelay + // this will likely be used to take profit on yield already converted to want + if (block.timestamp - params.lastReport > maxReportDelay) { return true; } @@ -749,9 +767,10 @@ contract StrategyPrismaConvexFactoryClonable is BaseStrategy { } /** - * @notice Here we set params to determine if we claim PRISMA emissions. - # @param _forceClaimOnce True if we want to allow claims that are not max boosted. Reset to false on rewards claim. - # @param _shouldClaimRewards Default value true. Set to false if we want to skip reward claims during harvests. + * @notice Here we set params to determine if and how we claim PRISMA emissions. + * @param _forceClaimOnce True if we want force a single rewards claim no matter boost, resets to false after claim. + * @param _shouldClaimRewards Default value false. Set to true if we want to turn on auto-claims using yprisma as + * boost delegate when max boosted. */ function setClaimParams( bool _forceClaimOnce, diff --git a/contracts/interfaces/ConvexFxnInterfaces.sol b/contracts/interfaces/ConvexFxnInterfaces.sol new file mode 100644 index 0000000..a4351b2 --- /dev/null +++ b/contracts/interfaces/ConvexFxnInterfaces.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IConvexFxn { + function poolInfo( + uint pid + ) + external + view + returns ( + address impl, + address gauge, + address token, + address rewards, + uint8 active + ); + + function createVault(uint256 pid) external returns (address); + + function getReward() external; + + function balanceOf(address account) external view returns (uint256); + + function deposit(uint256 _value) external; + + function withdraw(uint256 _value) external; + + function rewards() external view returns (address); + + function rewardTokens(uint256 _rid) external view returns (address); + + function rewardTokenLength() external view returns (uint256); +} + +interface ITradeFactory { + function enable(address, address) external; + + function disable(address, address) external; +} + +interface IDetails { + // get details from curve + function name() external view returns (string memory); + + function symbol() external view returns (string memory); +} diff --git a/interfaces/ICurveGaugeV6.json b/interfaces/ICurveGaugeV6.json new file mode 100644 index 0000000..5904694 --- /dev/null +++ b/interfaces/ICurveGaugeV6.json @@ -0,0 +1,511 @@ +[ + { + "name": "Deposit", + "inputs": [ + { "name": "provider", "type": "address", "indexed": true }, + { "name": "value", "type": "uint256", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "Withdraw", + "inputs": [ + { "name": "provider", "type": "address", "indexed": true }, + { "name": "value", "type": "uint256", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "UpdateLiquidityLimit", + "inputs": [ + { "name": "user", "type": "address", "indexed": false }, + { "name": "original_balance", "type": "uint256", "indexed": false }, + { "name": "original_supply", "type": "uint256", "indexed": false }, + { "name": "working_balance", "type": "uint256", "indexed": false }, + { "name": "working_supply", "type": "uint256", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "CommitOwnership", + "inputs": [{ "name": "admin", "type": "address", "indexed": false }], + "anonymous": false, + "type": "event" + }, + { + "name": "ApplyOwnership", + "inputs": [{ "name": "admin", "type": "address", "indexed": false }], + "anonymous": false, + "type": "event" + }, + { + "name": "Transfer", + "inputs": [ + { "name": "_from", "type": "address", "indexed": true }, + { "name": "_to", "type": "address", "indexed": true }, + { "name": "_value", "type": "uint256", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "Approval", + "inputs": [ + { "name": "_owner", "type": "address", "indexed": true }, + { "name": "_spender", "type": "address", "indexed": true }, + { "name": "_value", "type": "uint256", "indexed": false } + ], + "anonymous": false, + "type": "event" + }, + { + "stateMutability": "nonpayable", + "type": "constructor", + "inputs": [], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "initialize", + "inputs": [{ "name": "_lp_token", "type": "address" }], + "outputs": [], + "gas": 374587 + }, + { + "stateMutability": "view", + "type": "function", + "name": "decimals", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }], + "gas": 318 + }, + { + "stateMutability": "view", + "type": "function", + "name": "integrate_checkpoint", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }], + "gas": 4590 + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "user_checkpoint", + "inputs": [{ "name": "addr", "type": "address" }], + "outputs": [{ "name": "", "type": "bool" }], + "gas": 3123886 + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "claimable_tokens", + "inputs": [{ "name": "addr", "type": "address" }], + "outputs": [{ "name": "", "type": "uint256" }], + "gas": 3038676 + }, + { + "stateMutability": "view", + "type": "function", + "name": "claimed_reward", + "inputs": [ + { "name": "_addr", "type": "address" }, + { "name": "_token", "type": "address" } + ], + "outputs": [{ "name": "", "type": "uint256" }], + "gas": 3036 + }, + { + "stateMutability": "view", + "type": "function", + "name": "claimable_reward", + "inputs": [ + { "name": "_user", "type": "address" }, + { "name": "_reward_token", "type": "address" } + ], + "outputs": [{ "name": "", "type": "uint256" }], + "gas": 20255 + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "set_rewards_receiver", + "inputs": [{ "name": "_receiver", "type": "address" }], + "outputs": [], + "gas": 35673 + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "claim_rewards", + "inputs": [], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "claim_rewards", + "inputs": [{ "name": "_addr", "type": "address" }], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "claim_rewards", + "inputs": [ + { "name": "_addr", "type": "address" }, + { "name": "_receiver", "type": "address" } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "kick", + "inputs": [{ "name": "addr", "type": "address" }], + "outputs": [], + "gas": 3137977 + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "deposit", + "inputs": [{ "name": "_value", "type": "uint256" }], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "deposit", + "inputs": [ + { "name": "_value", "type": "uint256" }, + { "name": "_addr", "type": "address" } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "deposit", + "inputs": [ + { "name": "_value", "type": "uint256" }, + { "name": "_addr", "type": "address" }, + { "name": "_claim_rewards", "type": "bool" } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "withdraw", + "inputs": [{ "name": "_value", "type": "uint256" }], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "withdraw", + "inputs": [ + { "name": "_value", "type": "uint256" }, + { "name": "_claim_rewards", "type": "bool" } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "transfer", + "inputs": [ + { "name": "_to", "type": "address" }, + { "name": "_value", "type": "uint256" } + ], + "outputs": [{ "name": "", "type": "bool" }], + "gas": 18062826 + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "transferFrom", + "inputs": [ + { "name": "_from", "type": "address" }, + { "name": "_to", "type": "address" }, + { "name": "_value", "type": "uint256" } + ], + "outputs": [{ "name": "", "type": "bool" }], + "gas": 18100776 + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "approve", + "inputs": [ + { "name": "_spender", "type": "address" }, + { "name": "_value", "type": "uint256" } + ], + "outputs": [{ "name": "", "type": "bool" }], + "gas": 38151 + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "increaseAllowance", + "inputs": [ + { "name": "_spender", "type": "address" }, + { "name": "_added_value", "type": "uint256" } + ], + "outputs": [{ "name": "", "type": "bool" }], + "gas": 40695 + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "decreaseAllowance", + "inputs": [ + { "name": "_spender", "type": "address" }, + { "name": "_subtracted_value", "type": "uint256" } + ], + "outputs": [{ "name": "", "type": "bool" }], + "gas": 40719 + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "add_reward", + "inputs": [ + { "name": "_reward_token", "type": "address" }, + { "name": "_distributor", "type": "address" } + ], + "outputs": [], + "gas": 115414 + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "set_reward_distributor", + "inputs": [ + { "name": "_reward_token", "type": "address" }, + { "name": "_distributor", "type": "address" } + ], + "outputs": [], + "gas": 43179 + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "deposit_reward_token", + "inputs": [ + { "name": "_reward_token", "type": "address" }, + { "name": "_amount", "type": "uint256" } + ], + "outputs": [], + "gas": 1540067 + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "set_killed", + "inputs": [{ "name": "_is_killed", "type": "bool" }], + "outputs": [], + "gas": 40529 + }, + { + "stateMutability": "view", + "type": "function", + "name": "lp_token", + "inputs": [], + "outputs": [{ "name": "", "type": "address" }], + "gas": 3018 + }, + { + "stateMutability": "view", + "type": "function", + "name": "future_epoch_time", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }], + "gas": 3048 + }, + { + "stateMutability": "view", + "type": "function", + "name": "balanceOf", + "inputs": [{ "name": "arg0", "type": "address" }], + "outputs": [{ "name": "", "type": "uint256" }], + "gas": 3293 + }, + { + "stateMutability": "view", + "type": "function", + "name": "totalSupply", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }], + "gas": 3108 + }, + { + "stateMutability": "view", + "type": "function", + "name": "allowance", + "inputs": [ + { "name": "arg0", "type": "address" }, + { "name": "arg1", "type": "address" } + ], + "outputs": [{ "name": "", "type": "uint256" }], + "gas": 3568 + }, + { + "stateMutability": "view", + "type": "function", + "name": "name", + "inputs": [], + "outputs": [{ "name": "", "type": "string" }], + "gas": 13398 + }, + { + "stateMutability": "view", + "type": "function", + "name": "symbol", + "inputs": [], + "outputs": [{ "name": "", "type": "string" }], + "gas": 11151 + }, + { + "stateMutability": "view", + "type": "function", + "name": "working_balances", + "inputs": [{ "name": "arg0", "type": "address" }], + "outputs": [{ "name": "", "type": "uint256" }], + "gas": 3443 + }, + { + "stateMutability": "view", + "type": "function", + "name": "working_supply", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }], + "gas": 3258 + }, + { + "stateMutability": "view", + "type": "function", + "name": "period", + "inputs": [], + "outputs": [{ "name": "", "type": "int128" }], + "gas": 3288 + }, + { + "stateMutability": "view", + "type": "function", + "name": "period_timestamp", + "inputs": [{ "name": "arg0", "type": "uint256" }], + "outputs": [{ "name": "", "type": "uint256" }], + "gas": 3363 + }, + { + "stateMutability": "view", + "type": "function", + "name": "integrate_inv_supply", + "inputs": [{ "name": "arg0", "type": "uint256" }], + "outputs": [{ "name": "", "type": "uint256" }], + "gas": 3393 + }, + { + "stateMutability": "view", + "type": "function", + "name": "integrate_inv_supply_of", + "inputs": [{ "name": "arg0", "type": "address" }], + "outputs": [{ "name": "", "type": "uint256" }], + "gas": 3593 + }, + { + "stateMutability": "view", + "type": "function", + "name": "integrate_checkpoint_of", + "inputs": [{ "name": "arg0", "type": "address" }], + "outputs": [{ "name": "", "type": "uint256" }], + "gas": 3623 + }, + { + "stateMutability": "view", + "type": "function", + "name": "integrate_fraction", + "inputs": [{ "name": "arg0", "type": "address" }], + "outputs": [{ "name": "", "type": "uint256" }], + "gas": 3653 + }, + { + "stateMutability": "view", + "type": "function", + "name": "inflation_rate", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }], + "gas": 3468 + }, + { + "stateMutability": "view", + "type": "function", + "name": "reward_count", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256" }], + "gas": 3498 + }, + { + "stateMutability": "view", + "type": "function", + "name": "reward_tokens", + "inputs": [{ "name": "arg0", "type": "uint256" }], + "outputs": [{ "name": "", "type": "address" }], + "gas": 3573 + }, + { + "stateMutability": "view", + "type": "function", + "name": "reward_data", + "inputs": [{ "name": "arg0", "type": "address" }], + "outputs": [ + { "name": "token", "type": "address" }, + { "name": "distributor", "type": "address" }, + { "name": "period_finish", "type": "uint256" }, + { "name": "rate", "type": "uint256" }, + { "name": "last_update", "type": "uint256" }, + { "name": "integral", "type": "uint256" } + ], + "gas": 15003 + }, + { + "stateMutability": "view", + "type": "function", + "name": "rewards_receiver", + "inputs": [{ "name": "arg0", "type": "address" }], + "outputs": [{ "name": "", "type": "address" }], + "gas": 3803 + }, + { + "stateMutability": "view", + "type": "function", + "name": "reward_integral_for", + "inputs": [ + { "name": "arg0", "type": "address" }, + { "name": "arg1", "type": "address" } + ], + "outputs": [{ "name": "", "type": "uint256" }], + "gas": 4048 + }, + { + "stateMutability": "view", + "type": "function", + "name": "is_killed", + "inputs": [], + "outputs": [{ "name": "", "type": "bool" }], + "gas": 3648 + }, + { + "stateMutability": "view", + "type": "function", + "name": "factory", + "inputs": [], + "outputs": [{ "name": "", "type": "address" }], + "gas": 3678 + } +] diff --git a/tests/conftest.py b/tests/conftest.py index 4d7f944..43fd211 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,9 @@ import pytest from brownie import web3, config, Contract, ZERO_ADDRESS, chain, interface, accounts -from eth_abi import encode_single import requests import os + # Snapshots the chain before each test and reverts after test completion. @pytest.fixture(scope="function", autouse=True) def isolate(fn_isolation): @@ -28,6 +28,7 @@ def isolate(fn_isolation): ################################################## TENDERLY DEBUGGING ################################################## + # change autouse to True if we want to use this fork to help debug tests @pytest.fixture(scope="session", autouse=use_tenderly) def tenderly_fork(web3, chain): @@ -60,27 +61,88 @@ def tenderly_fork(web3, chain): ################################################ UPDATE THINGS BELOW HERE ################################################ -#################### FIXTURES BELOW NEED TO BE ADJUSTED FOR THIS REPO #################### -# for curve/balancer, we will pull this automatically, so comment this out here (token below in unique fixtures section) -# @pytest.fixture(scope="session") -# def token(): -# token_address = "0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D" # this should be the address of the ERC-20 used by the strategy/vault () -# yield interface.IERC20(token_address) +@pytest.fixture(scope="session") +def which_strategy(): + # vanilla convex: 0 + # curve: 1 + # prisma convex: 2 + # fxn convex: 3 + # Only test 4 (Frax) for pools that actually have frax. + which_strategy = 2 + yield which_strategy @pytest.fixture(scope="session") -def whale(accounts, amount, token): +def token_string(): + id_number = 0 + token_string = "ERROR" + if id_number == 0: + token_string = "yPRISMA" + elif id_number == 1: + token_string = "cvxPRISMA" + elif id_number == 2: + token_string = "cvxCRV New" # working 7/8/24 + elif id_number == 3: + token_string = "stETH" + elif id_number == 4: + token_string = "FRAX-USDC" + elif id_number == 5: + token_string = "frxETH" + elif id_number == 6: + token_string = "eCFX" + elif id_number == 7: + token_string = "eUSD-FRAXBP" + elif id_number == 8: + token_string = "crvUSD-FRAX" + elif id_number == 9: + token_string = "frxETH-ng" + elif id_number == 10: + token_string = "GHO-fxUSD" # working 7/8/24 + elif id_number == 11: + token_string = "CurveLend-WETH" # working 7/8/24 + yield token_string + + +####### GENERALLY SHOULDN'T HAVE TO CHANGE ANYTHING BELOW HERE UNLESS UPDATING/ADDING WHALE/PROFIT AMOUNTS OR ADDRESSES + + +# for curve/balancer, we will pull this automatically via convex_token. set to False to manually set address +@pytest.fixture(scope="session") +def token(token_address_via_convex): + use_convex_token = True + if use_convex_token: + yield token_address_via_convex + else: + token_address = "" + yield interface.IERC20(token_address) + + +@pytest.fixture(scope="session") +def whale_accounts(): + whale_accounts = { + "yPRISMA": "0xf1ce237a1E1a88F6e289CD7998A826138AEB30b0", # gauge + "cvxPRISMA": "0x13E58C7b1147385D735a06D14F0456E54C2dEBC8", # gauge + "cvxCRV New": "0xfB18127c1471131468a1AaD4785c19678e521D86", # gauge, 55M tokens + "stETH": "0x65eaB5eC71ceC12f38829Fbb14C98ce4baD28C46", # 1700 tokens + "FRAX-USDC": "0xE57180685E3348589E9521aa53Af0BCD497E884d", # DOLA Pool, 23.6M tokens + "frxETH": "0x2932a86df44Fe8D2A706d8e9c5d51c24883423F5", # 78k tokens + "eCFX": "0xeCb456EA5365865EbAb8a2661B0c503410e9B347", # only use for factory deployment testing + "eUSD-FRAXBP": "0x8605dc0C339a2e7e85EEA043bD29d42DA2c6D784", # 13M + "crvUSD-FRAX": "0x96424E6b5eaafe0c3B36CA82068d574D44BE4e3c", # 88.5k + "frxETH-ng": "0x4E21418095d32d15c6e2B96A9910772613A50d50", # 40k (gauge, not perfect for strat testing but good for factory testing) + "GHO-fxUSD": "0xec303960CF0456aC304Af45C0aDDe34921a10Fdf", # 5M, gauge + "CurveLend-WETH": "0xF3F6D6d412a77b680ec3a5E35EbB11BbEC319739", # 7.5B, gauge (1000x) + "NEW": "", # + } + yield whale_accounts + + +@pytest.fixture(scope="session") +def whale(accounts, amount, token, whale_accounts, token_string): # Totally in it for the tech # Update this with a large holder of your want token (the largest EOA holder of LP) - # use the FRAX-USDC pool for now - whale = accounts.at("0x13E58C7b1147385D735a06D14F0456E54C2dEBC8", force=True) - # yPRISMA-f LP (gauge) 0xf1ce237a1E1a88F6e289CD7998A826138AEB30b0, cvxPRISMA gauge: 0x13E58C7b1147385D735a06D14F0456E54C2dEBC8 - # cvxCRV new gauge (already deployed, only use for strategy testing): 0xfB18127c1471131468a1AaD4785c19678e521D86, 47m tokens, - # stETH: 0x65eaB5eC71ceC12f38829Fbb14C98ce4baD28C46, 1700 tokens, frax-usdc: 0xE57180685E3348589E9521aa53Af0BCD497E884d, DOLA pool, 23.6m tokens, - # 0x2932a86df44Fe8D2A706d8e9c5d51c24883423F5 frxETH 78k tokens, eCFX 0xeCb456EA5365865EbAb8a2661B0c503410e9B347 (only use for factory deployment testing) - # 0x8605dc0C339a2e7e85EEA043bD29d42DA2c6D784 eUSD-FRAXBP, 13m, 0x96424E6b5eaafe0c3B36CA82068d574D44BE4e3c crvUSD-FRAX, 88.5k - # 0x4E21418095d32d15c6e2B96A9910772613A50d50 frxETH-ng 40k (gauge, not perfect for strat testing but good for factory testing) + whale = accounts.at(whale_accounts[token_string], force=True) if token.balanceOf(whale) < 2 * amount: raise ValueError( "Our whale needs more funds. Find another whale or reduce your amount variable." @@ -88,24 +150,57 @@ def whale(accounts, amount, token): yield whale +@pytest.fixture(scope="session") +def whale_amounts(): + whale_amounts = { + "yPRISMA": 5_000, + "cvxPRISMA": 100_000, + "cvxCRV New": 500_000, + "stETH": 300, + "FRAX-USDC": 50_000, + "frxETH": 5_000, + "eCFX": 5, + "eUSD-FRAXBP": 5_000, + "crvUSD-FRAX": 10_000, + "frxETH-ng": 100, + "GHO-fxUSD": 1_000, + "CurveLend-WETH": 100_000_000, # $100k of crvUSD + "NEW": 0, + } + yield whale_amounts + + # this is the amount of funds we have our whale deposit. adjust this as needed based on their wallet balance @pytest.fixture(scope="session") -def amount(token): - amount = ( - 100_000 * 10 ** token.decimals() - ) # 500k for cvxCRV, 300 for stETH, 50k for frax-usdc, 5k for frxETH, 5 eCFX, 5_000 eUSD-FRAXBP, 10_000 crvUSD-FRAX, 100 frxETH-ng, 5000 yPRISMA, 100k cvxPRISMA +def amount(token, whale_amounts, token_string): + amount = whale_amounts[token_string] * 10 ** token.decimals() yield amount @pytest.fixture(scope="session") -def profit_whale(accounts, profit_amount, token): +def profit_whale_accounts(): + profit_whale_accounts = { + "yPRISMA": "0x6806D62AAdF2Ee97cd4BCE46BF5fCD89766EF246", + "cvxPRISMA": "0x154001A2F9f816389b2F6D9E07563cE0359D813D", + "cvxCRV New": "0x109B3C39d675A2FF16354E116d080B94d238a7c9", # (only use for strategy testing), new cvxCRV 5100 tokens + "stETH": "0x82a7E64cdCaEdc0220D0a4eB49fDc2Fe8230087A", # 500 tokens + "FRAX-USDC": "0x8fdb0bB9365a46B145Db80D0B1C5C5e979C84190", # BUSD Pool, 17M tokens + "frxETH": "0x38a93e70b0D8343657f802C1c3Fdb06aC8F8fe99", # 28 tokens + "eCFX": "0xeCb456EA5365865EbAb8a2661B0c503410e9B347", # only use for factory deployment testing + "eUSD-FRAXBP": "0xf83deAdE1b0D2AfF07700C548a54700a082388bE", # 188 + "crvUSD-FRAX": "0x97283C716f72b6F716D6a1bf6Bd7C3FcD840027A", # 24.5k + "frxETH-ng": "0x4E21418095d32d15c6e2B96A9910772613A50d50", + "GHO-fxUSD": "0xfefB84273A4DEdd40D242f4C007190DE21C9E39e", + "CurveLend-WETH": "0x4Ec3fa22540f841657197440FeE70B5967465AaA", # 5M, but actually $5k since each is 1000x + "NEW": "", # + } + yield profit_whale_accounts + + +@pytest.fixture(scope="session") +def profit_whale(accounts, profit_amount, token, profit_whale_accounts, token_string): # ideally not the same whale as the main whale, or else they will lose money - profit_whale = accounts.at("0x154001A2F9f816389b2F6D9E07563cE0359D813D", force=True) - # 0x109B3C39d675A2FF16354E116d080B94d238a7c9 (only use for strategy testing), new cvxCRV 5100 tokens, stETH: 0x82a7E64cdCaEdc0220D0a4eB49fDc2Fe8230087A, 500 tokens - # frax-usdc 0x8fdb0bB9365a46B145Db80D0B1C5C5e979C84190, BUSD pool, 17m tokens, 0x38a93e70b0D8343657f802C1c3Fdb06aC8F8fe99 frxETH 28 tokens - # eCFX 0xeCb456EA5365865EbAb8a2661B0c503410e9B347 (only use for factory deployment testing), 0xf83deAdE1b0D2AfF07700C548a54700a082388bE eUSD-FRAXBP 188 - # 0x97283C716f72b6F716D6a1bf6Bd7C3FcD840027A crvUSD-FRAX, 24.5k, 0x4E21418095d32d15c6e2B96A9910772613A50d50 frxETH-ng - # 0x6806D62AAdF2Ee97cd4BCE46BF5fCD89766EF246 yPRISMA LP, cvxPRISMA LP 0x154001A2F9f816389b2F6D9E07563cE0359D813D + profit_whale = accounts.at(profit_whale_accounts[token_string], force=True) if token.balanceOf(profit_whale) < 5 * profit_amount: raise ValueError( "Our profit whale needs more funds. Find another whale or reduce your profit_amount variable." @@ -114,10 +209,28 @@ def profit_whale(accounts, profit_amount, token): @pytest.fixture(scope="session") -def profit_amount(token): - profit_amount = ( - 100 * 10 ** token.decimals() - ) # 1k for FRAX-USDC, 2 for stETH, 100 for cvxCRV, 4 for frxETH, 1 eCFX, 25 for eUSD, 50 crvUSD-FRAX, 1 frxETH-ng, 25 yPRISMA, 100 cvxPRISMA +def profit_amounts(): + profit_amounts = { + "yPRISMA": 25, + "cvxPRISMA": 100, + "cvxCRV New": 100, + "stETH": 2, + "FRAX-USDC": 1_000, + "frxETH": 4, + "eCFX": 1, + "eUSD-FRAXBP": 25, + "crvUSD-FRAX": 50, + "frxETH-ng": 1, + "GHO-fxUSD": 50, + "CurveLend-WETH": 500_000, # $500 of crvUSD + "NEW": 0, + } + yield profit_amounts + + +@pytest.fixture(scope="session") +def profit_amount(token, profit_amounts, token_string): + profit_amount = profit_amounts[token_string] * 10 ** token.decimals() yield profit_amount @@ -155,7 +268,7 @@ def contract_name( StrategyConvexFraxFactoryClonable, StrategyCurveBoostedFactoryClonable, StrategyPrismaConvexFactoryClonable, - # StrategyPrismaCurveFactoryClonable, + StrategyConvexFxnFactoryClonable, which_strategy, ): if which_strategy == 0: @@ -164,8 +277,8 @@ def contract_name( contract_name = StrategyCurveBoostedFactoryClonable elif which_strategy == 2: contract_name = StrategyPrismaConvexFactoryClonable - # elif which_strategy == 3: - # contract_name = StrategyPrismaCurveFactoryClonable + elif which_strategy == 3: + contract_name = StrategyConvexFxnFactoryClonable else: contract_name = StrategyConvexFraxFactoryClonable yield contract_name @@ -188,7 +301,7 @@ def is_clonable(): # use this to test our strategy in case there are no profits @pytest.fixture(scope="session") def no_profit(): - no_profit = True + no_profit = False yield no_profit @@ -294,6 +407,10 @@ def prisma_convex_factory(): def yprisma(): yield Contract("0xe3668873D944E4A949DA05fc8bDE419eFF543882") + @pytest.fixture(scope="session") + def fxn(): + yield Contract("0x365AccFCa291e7D3914637ABf1F7635dB165Bb09") + @pytest.fixture(scope="module") def vault(pm, gov, rewards, guardian, management, token, vault_address): @@ -353,9 +470,9 @@ def strategy( frax_pid, staking_address, prisma_convex_factory, - prisma_curve_factory, yprisma, prisma_vault, + fxn_pid, ): if which_strategy == 0: # convex strategy = gov.deploy( @@ -383,23 +500,18 @@ def strategy( contract_name, vault, trade_factory, - 10_000 * 1e6, - 25_000 * 1e6, prisma_vault, prisma_convex_factory.getDeterministicAddress( pid ), # This looks up the prisma receiver for the pool ) - # elif which_strategy == 3: # prisma curve - # strategy = gov.deploy( - # contract_name, - # vault, - # trade_factory, - # 10_000 * 1e6, - # 25_000 * 1e6, - # prisma_vault, - # prisma_curve_factory.getDeterministicAddress(gauge.address), # This looks up the prisma receiver for the pool - # ) + elif which_strategy == 3: # FXN Convex + strategy = gov.deploy( + contract_name, + vault, + trade_factory, + fxn_pid, + ) else: # frax strategy = gov.deploy( contract_name, @@ -418,10 +530,10 @@ def strategy( vault.setManagementFee(0, {"from": gov}) vault.setPerformanceFee(0, {"from": gov}) - # we will be migrating on our live vault instead of adding it directly if which_strategy == 0: # convex - # earmark rewards if we are using a convex strategy - booster.earmarkRewards(pid, {"from": gov}) + # convex implemented a change where you might need to wait until next epoch to earmark to prevent over-harvesting + # chain.sleep(86400 * 7) + # booster.earmarkRewards(pid, {"from": gov}) chain.sleep(1) chain.mine(1) @@ -446,7 +558,7 @@ def strategy( strategy.updateRewards([rewards_token], {"from": gov}) new_proxy.approveRewardToken(strategy.rewardsTokens(0), {"from": gov}) - # approve our new strategy on the proxy + # approve our new strategy on the proxy (if we want to test an existing want, then add more logic here) new_proxy.approveStrategy(strategy.gauge(), strategy, {"from": gov}) assert new_proxy.strategies(gauge.address) == strategy.address assert voter.strategy() == new_proxy.address @@ -459,11 +571,11 @@ def strategy( # set up our claim params; default to always claim strategy.setClaimParams(False, True, {"from": gov}) - elif which_strategy == 3: # Prisma Curve + elif which_strategy == 3: # FXN Convex vault.addStrategy(strategy, 10_000, 0, 2**256 - 1, 0, {"from": gov}) - print("New Vault, Prisma Curve Strategy") + print("New Vault, Convex FXN Strategy") chain.sleep(1) - # chain.mine(1) + chain.mine() else: # frax vault.addStrategy(strategy, 10_000, 0, 2**256 - 1, 0, {"from": gov}) print("New Vault, Frax Strategy") @@ -489,41 +601,123 @@ def strategy( #################### PUT UNIQUE FIXTURES FOR THIS REPO BELOW #################### + +@pytest.fixture(scope="session") +def pid_list(): + pid_list = { + "yPRISMA": 260, + "cvxPRISMA": 258, + "cvxCRV New": 157, + "stETH": 25, + "FRAX-USDC": 100, + "frxETH": 128, # do for frax + "eCFX": 160, + "eUSD-FRAXBP": 156, + "crvUSD-FRAX": 187, + "frxETH-ng": 219, + "GHO-fxUSD": 316, # we don't really need this for FXN strategies, but set to use for token lookup + "CurveLend-WETH": 365, + "NEW": 0, + } + yield pid_list + + # put our test pool's convex pid here # if you change this, make sure to update addresses/values below too @pytest.fixture(scope="session") -def pid(): - pid = 258 # 25 stETH, 157 cvxCRV new, 128 frxETH-ETH (do for frax), eCFX 160, eUSD-FRAXBP 156, crvUSD-FRAX 187, FRAX-USDC 100, frxETH-ng 219 - # 258 cvxPRISMA LP, 260 yPRISMA LP +def pid(pid_list, token_string): + pid = pid_list[token_string] yield pid @pytest.fixture(scope="session") -def prisma_receiver( - pid, gauge, prisma_convex_factory, prisma_curve_factory, which_strategy -): +def prisma_receiver(pid, gauge, prisma_convex_factory, which_strategy): address = ZERO_ADDRESS if which_strategy == 2: address = prisma_convex_factory.getDeterministicAddress(pid) - elif which_strategy == 3: - address = prisma_curve_factory.getDeterministicAddress(gauge) - yield Contract(address) + yield Contract(address) + else: + yield address + + +@pytest.fixture(scope="session") +def frax_pid_list(): + frax_pid_list = { + "yPRISMA": 1_000, # 1_000 is null + "cvxPRISMA": 1_000, + "cvxCRV New": 1_000, + "stETH": 1_000, + "FRAX-USDC": 9, + "frxETH": 36, + "eCFX": 1_000, + "eUSD-FRAXBP": 44, + "crvUSD-FRAX": 49, + "frxETH-ng": 63, + "GHO-fxUSD": 1_000, + "CurveLend-WETH": 1_000, + "NEW": 0, + } + yield frax_pid_list # put our pool's frax pid here @pytest.fixture(scope="session") -def frax_pid(): - frax_pid = 44 # 27 DOLA-FRAXBP, 9 FRAX-USDC, 36 frxETH-ETH, 44 eUSD-FRAXBP, crvUSD-FRAX 49, frxETH-ng 63 +def frax_pid(frax_pid_list, token_string): + frax_pid = frax_pid_list[token_string] yield frax_pid -# put our pool's staking address here @pytest.fixture(scope="session") -def staking_address(): - staking_address = "0x4c9AD8c53d0a001E7fF08a3E5E26dE6795bEA5ac" - # 0xa537d64881b84faffb9Ae43c951EEbF368b71cdA frxETH, 0x963f487796d54d2f27bA6F3Fbe91154cA103b199 FRAX-USDC, - # 0xE7211E87D60177575846936F2123b5FA6f0ce8Ab DOLA-FRAXBP, 0x4c9AD8c53d0a001E7fF08a3E5E26dE6795bEA5ac eUSD-FRAXBP - # 0x67CC47cF82785728DD5E3AE9900873a074328658 crvUSD-FRAX, 0xB4fdD7444E1d86b2035c97124C46b1528802DA35 frxETH-ng +def fxn_pid_list(): + fxn_pid_list = { + "yPRISMA": 1_000, # 1_000 is null + "cvxPRISMA": 1_000, + "cvxCRV New": 1_000, + "stETH": 1_000, + "FRAX-USDC": 1_000, + "frxETH": 1_000, + "eCFX": 1_000, + "eUSD-FRAXBP": 1_000, + "crvUSD-FRAX": 1_000, + "frxETH-ng": 1_000, + "GHO-fxUSD": 14, + "CurveLend-WETH": 1_000, + "NEW": 0, + } + yield fxn_pid_list + + +# put our pool's fxn pid here +@pytest.fixture(scope="session") +def fxn_pid(fxn_pid_list, token_string): + fxn_pid = fxn_pid_list[token_string] + yield fxn_pid + + +@pytest.fixture(scope="session") +def staking_address_list(): + staking_address_list = { + "yPRISMA": "NULL", + "cvxPRISMA": "NULL", + "cvxCRV New": "NULL", + "stETH": "NULL", + "FRAX-USDC": "0x963f487796d54d2f27bA6F3Fbe91154cA103b199", + "frxETH": "0xa537d64881b84faffb9Ae43c951EEbF368b71cdA", + "eCFX": "NULL", + "eUSD-FRAXBP": "0x4c9AD8c53d0a001E7fF08a3E5E26dE6795bEA5ac", + "crvUSD-FRAX": "0x67CC47cF82785728DD5E3AE9900873a074328658", + "frxETH-ng": "0xB4fdD7444E1d86b2035c97124C46b1528802DA35", + "GHO-fxUSD": "NULL", + "CurveLend-WETH": "NULL", + "NEW": "NULL", + } + yield staking_address_list + + +# our pool's staking address +@pytest.fixture(scope="session") +def staking_address(token_string, staking_address_list): + staking_address = staking_address_list[token_string] yield staking_address @@ -548,16 +742,6 @@ def template_staking_address(): yield template_staking_address -@pytest.fixture(scope="session") -def which_strategy(): - # must be 0 or 1 for vanilla convex and curve - # prisma convex: 2 - # prisma curve: 3 - # Only test 4 (Frax) for pools that actually have frax. - which_strategy = 2 - yield which_strategy - - # curve deposit pool for old pools, set to ZERO_ADDRESS otherwise @pytest.fixture(scope="session") def old_pool(): @@ -629,6 +813,7 @@ def has_rewards(): ########## ADDRESSES TO UPDATE FOR BALANCER VS CURVE ########## + # all contracts below should be able to stay static based on the pid @pytest.fixture(scope="session") def booster(): # this is the deposit contract @@ -655,6 +840,11 @@ def crv_whale(): yield accounts.at("0xF977814e90dA44bFA03b6295A0616a897441aceC", force=True) +@pytest.fixture(scope="session") +def fxn_whale(): + yield accounts.at("0x26B2ec4E02ebe2F54583af25b647b1D619e67BbF", force=True) + + @pytest.fixture(scope="session") def convex_token(): yield Contract("0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B") @@ -719,7 +909,7 @@ def pool(token, curve_registry, curve_cryptoswap_registry, old_pool): @pytest.fixture(scope="session") -def token(pid, booster): +def token_address_via_convex(pid, booster): # this should be the address of the ERC-20 used by the strategy/vault token_address = booster.poolInfo(pid)[0] yield Contract(token_address) @@ -742,7 +932,7 @@ def rewards_contract(pid, booster): @pytest.fixture(scope="session") def gauge(pid, booster): gauge = booster.poolInfo(pid)[2] - yield Contract(gauge) + yield interface.ICurveGaugeV6(gauge) @pytest.fixture(scope="function") @@ -830,7 +1020,7 @@ def curve_global(CurveGlobal): @pytest.fixture(scope="session") def new_proxy(): - yield Contract("0xda18f789a1D9AD33E891253660Fcf1332d236b29") + yield Contract("0x78eDcb307AC1d1F8F5Fd070B377A6e69C8dcFC34") @pytest.fixture(scope="session") diff --git a/tests/factory/test_curve_global.py b/tests/factory/test_curve_global.py index 2baa970..c1baf1a 100644 --- a/tests/factory/test_curve_global.py +++ b/tests/factory/test_curve_global.py @@ -3,6 +3,7 @@ import math from utils import harvest_strategy, check_status + # note that because ganache crashes with the try-catch when checking for frax pids, we need to do this test and the next with tenderly # for the vault deployment to not revert. additionally, best to do the first two individually. # also important to note that these tests don't care about which_strategy, but good to test out PIDs with and without frax strategies diff --git a/tests/strategies/test_change_debt.py b/tests/strategies/test_change_debt.py index cbfa130..ae7753d 100644 --- a/tests/strategies/test_change_debt.py +++ b/tests/strategies/test_change_debt.py @@ -2,6 +2,7 @@ from utils import harvest_strategy, check_status from brownie import chain + # test reducing the debtRatio on a strategy and then harvesting it def test_change_debt( gov, diff --git a/tests/strategies/test_cloning.py b/tests/strategies/test_cloning.py index 140a681..f50c676 100644 --- a/tests/strategies/test_cloning.py +++ b/tests/strategies/test_cloning.py @@ -3,6 +3,7 @@ import pytest from utils import harvest_strategy + # make sure cloned strategy works just like normal def test_cloning( gov, @@ -41,6 +42,7 @@ def test_cloning( prisma_receiver, yprisma, RELATIVE_APPROX, + fxn_pid, ): # skip this test if we don't clone @@ -114,7 +116,7 @@ def test_cloning( else: if which_strategy == 0: # convex # Shouldn't be able to call initialize again - with brownie.reverts(): + with brownie.reverts("Strategy already initialized"): strategy.initialize( vault, strategist, @@ -146,7 +148,7 @@ def test_cloning( new_strategy = contract_name.at(tx.events["Cloned"]["clone"]) # Shouldn't be able to call initialize again - with brownie.reverts(): + with brownie.reverts("Strategy already initialized"): new_strategy.initialize( vault, strategist, @@ -162,7 +164,7 @@ def test_cloning( ) ## shouldn't be able to clone a clone - with brownie.reverts(): + with brownie.reverts("Cant clone a clone"): new_strategy.cloneStrategyConvex( vault, strategist, @@ -179,7 +181,7 @@ def test_cloning( elif which_strategy == 1: # curve # Shouldn't be able to call initialize again - with brownie.reverts(): + with brownie.reverts("Strategy already initialized"): strategy.initialize( vault, strategist, @@ -204,7 +206,7 @@ def test_cloning( new_strategy = contract_name.at(tx.events["Cloned"]["clone"]) # Shouldn't be able to call initialize again - with brownie.reverts(): + with brownie.reverts("Strategy already initialized"): new_strategy.initialize( vault, strategist, @@ -217,7 +219,7 @@ def test_cloning( ) ## shouldn't be able to clone a clone - with brownie.reverts(): + with brownie.reverts("Cant clone a clone"): new_strategy.cloneStrategyCurveBoosted( vault, strategist, @@ -231,15 +233,13 @@ def test_cloning( elif which_strategy == 2: # Prisma Convex # Shouldn't be able to call initialize again - with brownie.reverts(): + with brownie.reverts("Strategy already initialized"): strategy.initialize( vault, strategist, rewards, keeper, trade_factory, - 10_000 * 1e6, - 25_000 * 1e6, prisma_vault, prisma_receiver, {"from": gov}, @@ -250,8 +250,6 @@ def test_cloning( rewards, keeper, trade_factory, - 10_000 * 1e6, - 25_000 * 1e6, prisma_vault, prisma_receiver, {"from": gov}, @@ -261,38 +259,83 @@ def test_cloning( print("We have a clone!", new_strategy) # Shouldn't be able to call initialize again - with brownie.reverts(): + with brownie.reverts("Strategy already initialized"): new_strategy.initialize( vault, strategist, rewards, keeper, trade_factory, - 10_000 * 1e6, - 25_000 * 1e6, prisma_vault, prisma_receiver, {"from": gov}, ) ## shouldn't be able to clone a clone - with brownie.reverts(): + with brownie.reverts("Cant clone a clone"): new_strategy.cloneStrategyPrismaConvex( vault, strategist, rewards, keeper, trade_factory, - 10_000 * 1e6, - 25_000 * 1e6, prisma_vault, prisma_receiver, {"from": gov}, ) + elif which_strategy == 3: # FXN Convex + # Shouldn't be able to call initialize again + with brownie.reverts("Strategy already initialized"): + strategy.initialize( + vault, + strategist, + rewards, + keeper, + trade_factory, + fxn_pid, + {"from": gov}, + ) + tx = strategy.cloneStrategyConvexFxn( + vault, + strategist, + rewards, + keeper, + trade_factory, + fxn_pid, + {"from": gov}, + ) + + new_strategy = contract_name.at(tx.events["Cloned"]["clone"]) + print("We have a clone!", new_strategy) + + # Shouldn't be able to call initialize again + with brownie.reverts("Strategy already initialized"): + new_strategy.initialize( + vault, + strategist, + rewards, + keeper, + trade_factory, + fxn_pid, + {"from": gov}, + ) + + ## shouldn't be able to clone a clone + with brownie.reverts("Cant clone a clone"): + new_strategy.cloneStrategyConvexFxn( + vault, + strategist, + rewards, + keeper, + trade_factory, + fxn_pid, + {"from": gov}, + ) + else: # frax # Shouldn't be able to call initialize again - with brownie.reverts(): + with brownie.reverts("Strategy already initialized"): strategy.initialize( vault, strategist, @@ -324,7 +367,7 @@ def test_cloning( new_strategy = contract_name.at(tx.events["Cloned"]["clone"]) # Shouldn't be able to call initialize again - with brownie.reverts(): + with brownie.reverts("Strategy already initialized"): new_strategy.initialize( vault, strategist, @@ -340,7 +383,7 @@ def test_cloning( ) ## shouldn't be able to clone a clone - with brownie.reverts(): + with brownie.reverts("Cant clone a clone"): new_strategy.cloneStrategyConvexFrax( vault, strategist, @@ -358,6 +401,11 @@ def test_cloning( # revoke, get funds back into vault, remove old strat from queue vault.revokeStrategy(strategy, {"from": gov}) + # prisma needs to be told to always claim + if which_strategy == 2: + # set up our claim params; default to always claim + new_strategy.setClaimParams(False, True, {"from": gov}) + if which_strategy == 4: # wait another week so our frax LPs are unlocked, need to do this when reducing debt or withdrawing chain.sleep(86400 * 7) diff --git a/tests/strategies/test_curve_convex_frax_operation.py b/tests/strategies/test_curve_convex_frax_operation.py index a23b97b..0651e61 100644 --- a/tests/strategies/test_curve_convex_frax_operation.py +++ b/tests/strategies/test_curve_convex_frax_operation.py @@ -28,6 +28,11 @@ def test_keep( new_proxy, yprisma, ): + # don't do for FXN, no keep + if which_strategy == 3: + print("\n🚫 FXN strategy has no keep, skipping...\n") + return + ## deposit to the vault after approving token.approve(vault, 2**256 - 1, {"from": whale}) vault.deposit(amount, {"from": whale}) @@ -52,12 +57,6 @@ def test_keep( strategy.setLocalKeepCrvs(1000, 1000, 1000, {"from": gov}) strategy.setVoters(gov, gov, gov, {"from": gov}) strategy.setLocalKeepCrvs(1000, 1000, 1000, {"from": gov}) - elif which_strategy == 3: - # need to set voters first if we're trying to set keep - with brownie.reverts(): - strategy.setLocalKeepCrvs(1000, 1000, 1000, {"from": gov}) - strategy.setVoters(gov, gov, gov, {"from": gov}) - strategy.setLocalKeepCrvs(1000, 1000, {"from": gov}) else: with brownie.reverts(): strategy.setLocalKeepCrvs(1000, 1000, 1000, {"from": gov}) @@ -117,23 +116,7 @@ def test_keep( if not no_profit: assert treasury_after > treasury_before elif which_strategy == 2: - treasury_before = crv.balanceOf(strategy.curveVoter()) - - (profit, loss) = harvest_strategy( - use_yswaps, - strategy, - token, - gov, - profit_whale, - profit_amount, - target, - ) - - treasury_after = crv.balanceOf(strategy.curveVoter()) - if not no_profit: - assert treasury_after > treasury_before - elif which_strategy == 3: - treasury_before = crv.balanceOf(strategy.curveVoter()) + treasury_before = yprisma.balanceOf(strategy.yprismaVoter()) (profit, loss) = harvest_strategy( use_yswaps, @@ -145,7 +128,7 @@ def test_keep( target, ) - treasury_after = crv.balanceOf(strategy.curveVoter()) + treasury_after = yprisma.balanceOf(strategy.yprismaVoter()) if not no_profit: assert treasury_after > treasury_before else: @@ -212,22 +195,6 @@ def test_keep( target, ) - treasury_after = crv.balanceOf(strategy.curveVoter()) - assert treasury_after == treasury_before - elif which_strategy == 3: - strategy.setLocalKeepCrv(0, {"from": gov}) - treasury_before = crv.balanceOf(strategy.curveVoter()) - - (profit, loss) = harvest_strategy( - use_yswaps, - strategy, - token, - gov, - profit_whale, - profit_amount, - target, - ) - treasury_after = crv.balanceOf(strategy.curveVoter()) assert treasury_after == treasury_before else: @@ -1883,3 +1850,303 @@ def test_keks_add_to_existing( # whale should be able to withdraw all of his funds now vault.withdraw({"from": whale}) assert vault.totalAssets() == 0 + + +def test_yprisma_claim( + gov, + token, + vault, + whale, + strategy, + amount, + sleep_time, + profit_whale, + profit_amount, + target, + use_yswaps, + yprisma, + which_strategy, +): + # only for prisma + if which_strategy != 2: + print("\n🚫🌈 Not a PRISMA strategy, skipping...\n") + return + + ## deposit to the vault after approving + token.approve(vault, 2**256 - 1, {"from": whale}) + vault.deposit(amount, {"from": whale}) + + # set this to false so we allow yPRISMA to accumulate in the strategy + use_yswaps = False + + receiver = Contract(strategy.prismaReceiver()) + eid = receiver.emissionId() + prisma_vault = Contract(strategy.prismaVault(), owner=receiver) + (profit, loss) = harvest_strategy( + use_yswaps, + strategy, + token, + gov, + profit_whale, + profit_amount, + target, + force_claim=False, + ) + claimable = receiver.claimableReward(strategy).dict() + # Check if any non-zero values (shouldn't have any, should have small amounts for all assets) + assert any(x for x in (claimable if isinstance(claimable, tuple) else (claimable,))) + + chain.sleep(sleep_time) + + # check that we have claimable profit, need this for min and max profit checks below + claimable_profit = strategy.claimableProfitInUsdc() + assert claimable_profit > 0 + print("šŸ¤‘ Claimable profit >0:", claimable_profit / 1e6) + + # set our max delay to 1 day so we trigger true, then set it back to 21 days + strategy.setMaxReportDelay(1) + tx = strategy.harvestTrigger(0, {"from": gov}) + print("\nShould we harvest? Should be true.", tx) + assert tx == True + strategy.setMaxReportDelay(86400 * 21) + + # we have tiny profit but that's okay; our triggers should be false because we don't have max boost + # update our minProfit so our harvest should trigger true + # will be true/false same as above based on max boost + strategy.setHarvestTriggerParams(1, 1000000e6, {"from": gov}) + tx = strategy.harvestTrigger(0, {"from": gov}) + print("\nShould we harvest? Should be false.", tx) + assert tx == strategy.claimsAreMaxBoosted() + + # update our maxProfit, but should still be false + strategy.setHarvestTriggerParams(1000000e6, 1, {"from": gov}) + tx = strategy.harvestTrigger(0, {"from": gov}) + print("\nShould we harvest? Should be false.", tx) + assert tx == False + + # force claim so we should be true + strategy.setClaimParams(True, True, {"from": vault.governance()}) + tx = strategy.harvestTrigger(0, {"from": gov}) + print("\nShould we harvest? Should be true.", tx) + assert tx == True + + # turn off claiming entirely + strategy.setClaimParams(False, False, {"from": vault.governance()}) + tx = strategy.harvestTrigger(0, {"from": gov}) + print("\nShould we harvest? Should be false.", tx) + assert tx == False + strategy.setClaimParams(False, True, {"from": vault.governance()}) + + strategy.setHarvestTriggerParams(2000e6, 25000e6, {"from": gov}) + + # turn on the force claim + strategy.setClaimParams(True, True, {"from": vault.governance()}) + + # update our minProfit so our harvest triggers true + strategy.setHarvestTriggerParams(1, 1000000e6, {"from": gov}) + tx = strategy.harvestTrigger(0, {"from": gov}) + print("\nShould we harvest? Should be true.", tx) + assert tx == True + + # turn off claiming entirely + strategy.setClaimParams(True, False, {"from": vault.governance()}) + tx = strategy.harvestTrigger(0, {"from": gov}) + print("\nShould we harvest? Should be false.", tx) + assert tx == False + strategy.setClaimParams(True, True, {"from": vault.governance()}) + + # update our maxProfit so harvest triggers true + strategy.setHarvestTriggerParams(1000000e6, 1, {"from": gov}) + tx = strategy.harvestTrigger(0, {"from": gov}) + print("\nShould we harvest? Should be true.", tx) + assert tx == True + + strategy.setHarvestTriggerParams(2000e6, 25000e6, {"from": gov}) + + # set our max delay to 1 day so we trigger true, then set it back to 21 days + strategy.setMaxReportDelay(sleep_time - 1) + tx = strategy.harvestTrigger(0, {"from": gov}) + print("\nShould we harvest? Should be True.", tx) + assert tx == True + strategy.setMaxReportDelay(86400 * 21) + + strategy.setClaimParams(False, True, {"from": vault.governance()}) + + # Now harvest again + (profit, loss) = harvest_strategy( + use_yswaps, + strategy, + token, + gov, + profit_whale, + profit_amount, + target, + force_claim=False, + ) + # This only works if we have exhausted our boost for current week (we won't have claimed any yPRISMA) + if not strategy.claimsAreMaxBoosted(): + assert yprisma.balanceOf(strategy) == 0 + + # sleep to get to the new epoch + chain.sleep(60 * 60 * 24 * 7) + chain.mine() + + claimable_profit = strategy.claimableProfitInUsdc() + assert claimable_profit > 0 + print("šŸ¤‘ Claimable profit next epoch:", claimable_profit / 1e6) + + prisma_vault.allocateNewEmissions(eid) + receiver.claimableReward(strategy) + y = "0x90be6DFEa8C80c184C442a36e17cB2439AAE25a7" + boosted = prisma_vault.getClaimableWithBoost(y) + assert boosted[0] > 0 + assert strategy.claimsAreMaxBoosted() + + # now we should be able to claim without forcing + # update our minProfit so our harvest triggers true + strategy.setHarvestTriggerParams(1, 1000000e6, {"from": gov}) + tx = strategy.harvestTrigger(0, {"from": gov}) + print("\nShould we harvest? Should be true.", tx) + assert tx == True + + # update our maxProfit so harvest triggers true + strategy.setHarvestTriggerParams(1000000e6, 1, {"from": gov}) + tx = strategy.harvestTrigger(0, {"from": gov}) + print("\nShould we harvest? Should be true.", tx) + assert tx == True + + # we shouldn't get any more yPRISMA if we turn off claims, but we may have received some above if we were max boosted + before = yprisma.balanceOf(strategy) + strategy.setClaimParams(False, False, {"from": vault.governance()}) + + (profit, loss) = harvest_strategy( + use_yswaps, + strategy, + token, + gov, + profit_whale, + profit_amount, + target, + force_claim=False, + ) + assert yprisma.balanceOf(strategy) == before + + # turn claiming back on + strategy.setClaimParams(False, True, {"from": vault.governance()}) + + (profit, loss) = harvest_strategy( + use_yswaps, + strategy, + token, + gov, + profit_whale, + profit_amount, + target, + force_claim=False, + ) + assert yprisma.balanceOf(strategy) > 0 + +# ADD SOME MORE STUFF HERE W/ NEW TRIGGER CLAIM!!!!!! TEST TRIGGER FLIPS AND NEXT HARVEST DOES WHAT WE EXPECT IT TO +def test_yprisma_force_claim( + gov, + token, + vault, + whale, + strategy, + amount, + sleep_time, + profit_whale, + profit_amount, + target, + use_yswaps, + yprisma, + which_strategy, +): + # only for prisma + if which_strategy != 2: + print("\n🚫🌈 Not a PRISMA strategy, skipping...\n") + return + + ## deposit to the vault after approving + token.approve(vault, 2**256 - 1, {"from": whale}) + vault.deposit(amount, {"from": whale}) + + # set this to false so we allow yPRISMA to accumulate in the strategy + use_yswaps = False + + receiver = Contract(strategy.prismaReceiver()) + eid = receiver.emissionId() + prisma_vault = Contract(strategy.prismaVault(), owner=receiver) + (profit, loss) = harvest_strategy( + use_yswaps, + strategy, + token, + gov, + profit_whale, + profit_amount, + target, + force_claim=False, + ) + claimable = receiver.claimableReward(strategy).dict() + # Check if any non-zero values (shouldn't have any, should have small amounts for all assets) + assert any(x for x in (claimable if isinstance(claimable, tuple) else (claimable,))) + + chain.sleep(sleep_time) + + # force claim from convex's receiver + assert yprisma.balanceOf(strategy) == 0 + convex_delegate = "0x8ad7a9e2B3Cd9214f36Cb871336d8ab34DdFdD5b" + strategy.claimRewards(convex_delegate, 5000, {"from": vault.governance()}) + assert yprisma.balanceOf(strategy) > 0 + balance_1 = yprisma.balanceOf(strategy) + + # sleep to get to the new epoch + chain.sleep(60 * 60 * 24 * 7) + chain.mine() + + # turn off claims to not add any more yprisma with the next harvest + strategy.setClaimParams(False, False, {"from": vault.governance()}) + + (profit, loss) = harvest_strategy( + use_yswaps, + strategy, + token, + gov, + profit_whale, + profit_amount, + target, + force_claim=False, + ) + + # make sure we didn't add more yprisma + balance_2 = yprisma.balanceOf(strategy) + assert balance_1 == balance_2 + + # turn claiming back on + strategy.setClaimParams(False, True, {"from": vault.governance()}) + + claimable_profit = strategy.claimableProfitInUsdc() + assert claimable_profit > 0 + print("šŸ¤‘ Claimable profit next epoch:", claimable_profit / 1e6) + + prisma_vault.allocateNewEmissions(eid) + receiver.claimableReward(strategy) + y = "0x90be6DFEa8C80c184C442a36e17cB2439AAE25a7" + boosted = prisma_vault.getClaimableWithBoost(y) + assert boosted[0] > 0 + assert strategy.claimsAreMaxBoosted() + assert yprisma.balanceOf(strategy) == balance_1 == balance_2 + + (profit, loss) = harvest_strategy( + use_yswaps, + strategy, + token, + gov, + profit_whale, + profit_amount, + target, + force_claim=False, + ) + + # now we should have more yprisma claimed + assert yprisma.balanceOf(strategy) > balance_2 diff --git a/tests/strategies/test_emergency_exit.py b/tests/strategies/test_emergency_exit.py index c824240..8dcd574 100644 --- a/tests/strategies/test_emergency_exit.py +++ b/tests/strategies/test_emergency_exit.py @@ -3,6 +3,7 @@ from brownie import Contract, chain, interface from utils import harvest_strategy, check_status + # test that emergency exit works properly def test_emergency_exit( gov, @@ -95,7 +96,8 @@ def test_emergency_exit( # for some reason withdrawing via our user vault doesn't include the same getReward() call that the staking pool does natively # since emergencyExit doesn't enter prepareReturn, we have to manually claim these rewards # also, FXS profit accrues every block, so we will still get some dust rewards after we exit as well if we were to call getReward() again - if which_strategy == 4: + # similarly, for FXN strategies, we need to manually claim rewards from the user vault + if which_strategy in [3, 4]: user_vault = interface.IFraxVault(strategy.userVault()) user_vault.getReward({"from": gov}) @@ -277,7 +279,8 @@ def test_emergency_exit_with_profit( # for some reason withdrawing via our user vault doesn't include the same getReward() call that the staking pool does natively # since emergencyExit doesn't enter prepareReturn, we have to manually claim these rewards # also, FXS profit accrues every block, so we will still get some dust rewards after we exit as well if we were to call getReward() again - if which_strategy == 4: + # similarly, for FXN strategies, we need to manually claim rewards from the user vault + if which_strategy in [3, 4]: user_vault = interface.IFraxVault(strategy.userVault()) user_vault.getReward({"from": gov}) @@ -443,11 +446,18 @@ def test_emergency_exit_with_loss( print("Gauge Balance of Vault", to_send) gauge.transfer(gov, to_send, {"from": voter}) assert strategy.estimatedTotalAssets() == 0 - elif which_strategy in [2, 3]: + elif which_strategy == 2: to_send = prisma_receiver.balanceOf(strategy) prisma_receiver.withdraw(gov, to_send, {"from": strategy}) print("Balance of Strategy", to_send) assert strategy.estimatedTotalAssets() == 0 + elif which_strategy == 3: + # userVault sends away all of the gauge tokens + user_vault = strategy.userVault() + fxn_gauge = Contract(strategy.fxnGauge()) + to_send = fxn_gauge.balanceOf(user_vault) + fxn_gauge.transfer(gov, to_send, {"from": user_vault}) + assert strategy.estimatedTotalAssets() == 0 elif which_strategy == 4: # wait another week so our frax LPs are unlocked chain.sleep(86400 * 7) @@ -515,7 +525,8 @@ def test_emergency_exit_with_loss( # for some reason withdrawing via our user vault doesn't include the same getReward() call that the staking pool does natively # since emergencyExit doesn't enter prepareReturn, we have to manually claim these rewards # also, FXS profit accrues every block, so we will still get some dust rewards after we exit as well if we were to call getReward() again - if which_strategy == 4: + # similarly, for FXN strategies, we need to manually claim rewards from the user vault + if which_strategy in [3, 4]: user_vault = interface.IFraxVault(strategy.userVault()) user_vault.getReward({"from": gov}) @@ -696,10 +707,19 @@ def test_emergency_exit_with_no_loss( print("Gauge Balance of Vault", to_send / 1e18) gauge.transfer(gov, to_send, {"from": voter}) assert strategy.estimatedTotalAssets() == 0 - elif which_strategy in [2, 3]: + # gov withdraws + gauge.withdraw(to_send, {"from": gov}) + elif which_strategy == 2: # send away all funds, will need to alter this based on strategy to_send = prisma_receiver.balanceOf(strategy) prisma_receiver.withdraw(gov, to_send, {"from": strategy}) + elif which_strategy == 3: + # userVault sends away all of the gauge tokens + user_vault = strategy.userVault() + fxn_gauge = Contract(strategy.fxnGauge()) + to_send = fxn_gauge.balanceOf(user_vault) + fxn_gauge.transfer(gov, to_send, {"from": user_vault}) + assert strategy.estimatedTotalAssets() == 0 elif which_strategy == 4: # wait another week so our frax LPs are unlocked chain.sleep(86400 * 7) @@ -737,7 +757,7 @@ def test_emergency_exit_with_no_loss( assert vault.debtOutstanding(strategy) == 0 ################# GOV SENDS IT BACK, ADJUST AS NEEDED. ################# - if which_strategy in [0, 2, 3]: + if which_strategy in [0, 1]: # gov sends it back, glad someone was watching! token.transfer(strategy, to_send, {"from": gov}) assert strategy.estimatedTotalAssets() > 0 @@ -746,6 +766,13 @@ def test_emergency_exit_with_no_loss( gauge.withdraw(to_send, {"from": gov}) token.transfer(strategy, to_send, {"from": gov}) assert strategy.estimatedTotalAssets() > 0 + elif which_strategy == 3: + # gov sends it back + user_vault = strategy.userVault() + fxn_gauge = Contract(strategy.fxnGauge()) + to_send = fxn_gauge.balanceOf(gov) + fxn_gauge.transfer(user_vault, to_send, {"from": gov}) + assert strategy.estimatedTotalAssets() > 0 elif which_strategy == 4: # gov unwraps and sends it back, glad someone was watching! to_unwrap = staking_token.balanceOf(gov) @@ -773,7 +800,8 @@ def test_emergency_exit_with_no_loss( # for some reason withdrawing via our user vault doesn't include the same getReward() call that the staking pool does natively # since emergencyExit doesn't enter prepareReturn, we have to manually claim these rewards # also, FXS profit accrues every block, so we will still get some dust rewards after we exit as well if we were to call getReward() again - if which_strategy == 4: + # similarly, for FXN strategies, we need to manually claim rewards from the user vault + if which_strategy in [3, 4]: user_vault = interface.IFraxVault(strategy.userVault()) user_vault.getReward({"from": gov}) diff --git a/tests/strategies/test_migration.py b/tests/strategies/test_migration.py index f0d5beb..a8eb2ee 100644 --- a/tests/strategies/test_migration.py +++ b/tests/strategies/test_migration.py @@ -3,6 +3,7 @@ from brownie import accounts, interface, chain import brownie + # test migrating a strategy def test_migration( gov, @@ -35,6 +36,8 @@ def test_migration( yprisma, prisma_vault, RELATIVE_APPROX, + fxn_pid, + fxn, ): ## deposit to the vault after approving @@ -76,18 +79,23 @@ def test_migration( new_proxy, gauge, ) - elif which_strategy in [2, 3]: # prisma + elif which_strategy == 2: # prisma convex new_strategy = gov.deploy( contract_name, vault, trade_factory, - 10_000 * 1e6, - 25_000 * 1e6, prisma_vault, prisma_convex_factory.getDeterministicAddress( pid ), # This looks up the prisma receiver for the pool ) + elif which_strategy == 3: # fxn convex + new_strategy = gov.deploy( + contract_name, + vault, + trade_factory, + fxn_pid, + ) elif which_strategy == 4: # frax new_strategy = gov.deploy( contract_name, @@ -108,23 +116,21 @@ def test_migration( print("\nShould we harvest? Should be False.", tx) assert tx == False - if which_strategy != 4: - # can we harvest an unactivated strategy? should be no - tx = new_strategy.harvestTrigger(0, {"from": gov}) - print("\nShould we harvest? Should be False.", tx) - assert tx == False + # had it skipping for FRAX here too, not sure why tho ######### ADD LOGIC TO TEST CLAIMING OF ASSETS FOR TRANSFER TO NEW STRATEGY AS NEEDED ######### # for some reason withdrawing via our user vault doesn't include the same getReward() call that the staking pool does natively # since emergencyExit doesn't enter prepareReturn, we have to manually claim these rewards # also, FXS profit accrues every block, so we will still get some dust rewards after we exit as well if we were to call getReward() again - if which_strategy == 4: - with brownie.reverts(): - vault.migrateStrategy(strategy, new_strategy, {"from": gov}) - # wait another week so our frax LPs are unlocked, need to do this when reducing debt or withdrawing - chain.sleep(86400 * 7) - chain.mine(1) - + if which_strategy in [3, 4]: + if which_strategy == 4: + with brownie.reverts(): + vault.migrateStrategy(strategy, new_strategy, {"from": gov}) + # wait another week so our frax LPs are unlocked, need to do this when reducing debt or withdrawing + chain.sleep(86400 * 7) + chain.mine(1) + + # do the claim for both FXN and FRAX strategies user_vault = interface.IFraxVault(strategy.userVault()) user_vault.getReward({"from": gov}) @@ -135,6 +141,12 @@ def test_migration( # migrate our old strategy, need to claim rewards for convex when withdrawing for convex if which_strategy == 0: strategy.setClaimRewards(True, {"from": gov}) + + # ADD MANUAL CLAIM FOR YPRISMA HERE *********** + if which_strategy == 2: + strategy.claimRewards(True, {"from": gov}) + + vault.migrateStrategy(strategy, new_strategy, {"from": gov}) # if a curve strat, whitelist on our strategy proxy @@ -143,10 +155,10 @@ def test_migration( ####### ADD LOGIC TO MAKE SURE ASSET TRANSFER WENT AS EXPECTED ####### assert crv.balanceOf(strategy) == 0 - if which_strategy != 2: + if which_strategy not in [2, 3]: assert crv.balanceOf(new_strategy) > 0 - if which_strategy != 1 and which_strategy not in [2, 3]: + if which_strategy not in [1, 2, 3]: assert convex_token.balanceOf(strategy) == 0 assert convex_token.balanceOf(new_strategy) > 0 @@ -154,6 +166,14 @@ def test_migration( assert fxs.balanceOf(strategy) == 0 assert fxs.balanceOf(new_strategy) > 0 + if which_strategy == 2: + assert yprisma.balanceOf(strategy) == 0 + assert yprisma.balanceOf(new_strategy) > 0 + + if which_strategy == 3: + assert fxn.balanceOf(strategy) == 0 + assert fxn.balanceOf(new_strategy) > 0 + # assert that our old strategy is empty updated_total_old = strategy.estimatedTotalAssets() assert updated_total_old == 0 @@ -234,6 +254,7 @@ def test_empty_migration( prisma_vault, prisma_convex_factory, yprisma, + fxn_pid, ): ## deposit to the vault after approving @@ -275,18 +296,23 @@ def test_empty_migration( new_proxy, gauge, ) - elif which_strategy in [2, 3]: # prisma + elif which_strategy == 2: # prisma convex new_strategy = gov.deploy( contract_name, vault, trade_factory, - 10_000 * 1e6, - 25_000 * 1e6, prisma_vault, prisma_convex_factory.getDeterministicAddress( pid ), # This looks up the prisma receiver for the pool ) + elif which_strategy == 3: # fxn convex + new_strategy = gov.deploy( + contract_name, + vault, + trade_factory, + fxn_pid, + ) elif which_strategy == 4: # frax new_strategy = gov.deploy( contract_name, diff --git a/tests/strategies/test_misc.py b/tests/strategies/test_misc.py index e54cc38..6a77a73 100644 --- a/tests/strategies/test_misc.py +++ b/tests/strategies/test_misc.py @@ -5,289 +5,6 @@ from utils import harvest_strategy -def test_yprisma_claim( - gov, - token, - vault, - whale, - strategy, - amount, - sleep_time, - profit_whale, - profit_amount, - target, - use_yswaps, - yprisma, -): - ## deposit to the vault after approving - token.approve(vault, 2**256 - 1, {"from": whale}) - vault.deposit(amount, {"from": whale}) - - # set this to false so we allow yPRISMA to accumulate in the strategy - use_yswaps = False - - receiver = Contract(strategy.prismaReceiver()) - eid = receiver.emissionId() - prisma_vault = Contract(strategy.prismaVault(), owner=receiver) - (profit, loss) = harvest_strategy( - use_yswaps, - strategy, - token, - gov, - profit_whale, - profit_amount, - target, - force_claim=False, - ) - claimable = receiver.claimableReward(strategy).dict() - # Check if any non-zero values (shouldn't have any, should have small amounts for all assets) - assert any(x for x in (claimable if isinstance(claimable, tuple) else (claimable,))) - - chain.sleep(sleep_time) - - # check that we have claimable profit, need this for min and max profit checks below - claimable_profit = strategy.claimableProfitInUsdc() - assert claimable_profit > 0 - print("šŸ¤‘ Claimable profit >0:", claimable_profit / 1e6) - - # set our max delay to 1 day so we trigger true, then set it back to 21 days - # but will be false because no max boost (I originally wrote this late in a week, will just be equal to if max boosted or not) - strategy.setMaxReportDelay(sleep_time - 1) - tx = strategy.harvestTrigger(0, {"from": gov}) - print("\nShould we harvest? Should be False.", tx) - assert tx == strategy.claimsAreMaxBoosted() - strategy.setMaxReportDelay(86400 * 21) - - # we have tiny profit but that's okay; our triggers should be false because we don't have max boost - # update our minProfit so our harvest should trigger true - # will be true/false same as above based on max boost - strategy.setHarvestTriggerParams(1, 1000000e6, {"from": gov}) - tx = strategy.harvestTrigger(0, {"from": gov}) - print("\nShould we harvest? Should be false.", tx) - assert tx == strategy.claimsAreMaxBoosted() - - # update our maxProfit so harvest should trigger true (max profit ignores whether we have full boost or not) - strategy.setHarvestTriggerParams(1000000e6, 1, {"from": gov}) - tx = strategy.harvestTrigger(0, {"from": gov}) - print("\nShould we harvest? Should be true.", tx) - assert tx == True - - # turn off claiming entirely - strategy.setClaimParams(False, False, {"from": vault.governance()}) - tx = strategy.harvestTrigger(0, {"from": gov}) - print("\nShould we harvest? Should be false.", tx) - assert tx == False - strategy.setClaimParams(False, True, {"from": vault.governance()}) - - strategy.setHarvestTriggerParams(2000e6, 25000e6, {"from": gov}) - - # turn on the force claim - strategy.setClaimParams(True, True, {"from": vault.governance()}) - - # update our minProfit so our harvest triggers true - strategy.setHarvestTriggerParams(1, 1000000e6, {"from": gov}) - tx = strategy.harvestTrigger(0, {"from": gov}) - print("\nShould we harvest? Should be true.", tx) - assert tx == True - - # turn off claiming entirely - strategy.setClaimParams(True, False, {"from": vault.governance()}) - tx = strategy.harvestTrigger(0, {"from": gov}) - print("\nShould we harvest? Should be false.", tx) - assert tx == False - strategy.setClaimParams(True, True, {"from": vault.governance()}) - - # update our maxProfit so harvest triggers true - strategy.setHarvestTriggerParams(1000000e6, 1, {"from": gov}) - tx = strategy.harvestTrigger(0, {"from": gov}) - print("\nShould we harvest? Should be true.", tx) - assert tx == True - - strategy.setHarvestTriggerParams(2000e6, 25000e6, {"from": gov}) - - # set our max delay to 1 day so we trigger true, then set it back to 21 days - strategy.setMaxReportDelay(sleep_time - 1) - tx = strategy.harvestTrigger(0, {"from": gov}) - print("\nShould we harvest? Should be True.", tx) - assert tx == True - strategy.setMaxReportDelay(86400 * 21) - - strategy.setClaimParams(False, True, {"from": vault.governance()}) - - # Now harvest again - (profit, loss) = harvest_strategy( - use_yswaps, - strategy, - token, - gov, - profit_whale, - profit_amount, - target, - force_claim=False, - ) - # This only works if we have exhausted our boost for current week (we won't have claimed any yPRISMA) - if not strategy.claimsAreMaxBoosted(): - assert yprisma.balanceOf(strategy) == 0 - - # sleep to get to the new epoch - chain.sleep(60 * 60 * 24 * 7) - chain.mine() - - claimable_profit = strategy.claimableProfitInUsdc() - assert claimable_profit > 0 - print("šŸ¤‘ Claimable profit next epoch:", claimable_profit / 1e6) - - prisma_vault.allocateNewEmissions(eid) - receiver.claimableReward(strategy) - y = "0x90be6DFEa8C80c184C442a36e17cB2439AAE25a7" - boosted = prisma_vault.getClaimableWithBoost(y) - assert boosted[0] > 0 - assert strategy.claimsAreMaxBoosted() - - # now we should be able to claim without forcing - # update our minProfit so our harvest triggers true - strategy.setHarvestTriggerParams(1, 1000000e6, {"from": gov}) - tx = strategy.harvestTrigger(0, {"from": gov}) - print("\nShould we harvest? Should be true.", tx) - assert tx == True - - # update our maxProfit so harvest triggers true - strategy.setHarvestTriggerParams(1000000e6, 1, {"from": gov}) - tx = strategy.harvestTrigger(0, {"from": gov}) - print("\nShould we harvest? Should be true.", tx) - assert tx == True - - # we shouldn't get any more yPRISMA if we turn off claims, but we may have received some above if we were max boosted - before = yprisma.balanceOf(strategy) - strategy.setClaimParams(False, False, {"from": vault.governance()}) - - (profit, loss) = harvest_strategy( - use_yswaps, - strategy, - token, - gov, - profit_whale, - profit_amount, - target, - force_claim=False, - ) - assert yprisma.balanceOf(strategy) == before - - # turn claiming back on - strategy.setClaimParams(False, True, {"from": vault.governance()}) - - (profit, loss) = harvest_strategy( - use_yswaps, - strategy, - token, - gov, - profit_whale, - profit_amount, - target, - force_claim=False, - ) - assert yprisma.balanceOf(strategy) > 0 - - -def test_yprisma_force_claim( - gov, - token, - vault, - whale, - strategy, - amount, - sleep_time, - profit_whale, - profit_amount, - target, - use_yswaps, - yprisma, -): - ## deposit to the vault after approving - token.approve(vault, 2**256 - 1, {"from": whale}) - vault.deposit(amount, {"from": whale}) - - # set this to false so we allow yPRISMA to accumulate in the strategy - use_yswaps = False - - receiver = Contract(strategy.prismaReceiver()) - eid = receiver.emissionId() - prisma_vault = Contract(strategy.prismaVault(), owner=receiver) - (profit, loss) = harvest_strategy( - use_yswaps, - strategy, - token, - gov, - profit_whale, - profit_amount, - target, - force_claim=False, - ) - claimable = receiver.claimableReward(strategy).dict() - # Check if any non-zero values (shouldn't have any, should have small amounts for all assets) - assert any(x for x in (claimable if isinstance(claimable, tuple) else (claimable,))) - - chain.sleep(sleep_time) - - # force claim from convex's receiver - assert yprisma.balanceOf(strategy) == 0 - convex_delegate = "0x8ad7a9e2B3Cd9214f36Cb871336d8ab34DdFdD5b" - strategy.claimRewards(convex_delegate, 5000, {"from": vault.governance()}) - assert yprisma.balanceOf(strategy) > 0 - balance_1 = yprisma.balanceOf(strategy) - - # sleep to get to the new epoch - chain.sleep(60 * 60 * 24 * 7) - chain.mine() - - # turn off claims to not add any more yprisma with the next harvest - strategy.setClaimParams(False, False, {"from": vault.governance()}) - - (profit, loss) = harvest_strategy( - use_yswaps, - strategy, - token, - gov, - profit_whale, - profit_amount, - target, - force_claim=False, - ) - - # make sure we didn't add more yprisma - balance_2 = yprisma.balanceOf(strategy) - assert balance_1 == balance_2 - - # turn claiming back on - strategy.setClaimParams(False, True, {"from": vault.governance()}) - - claimable_profit = strategy.claimableProfitInUsdc() - assert claimable_profit > 0 - print("šŸ¤‘ Claimable profit next epoch:", claimable_profit / 1e6) - - prisma_vault.allocateNewEmissions(eid) - receiver.claimableReward(strategy) - y = "0x90be6DFEa8C80c184C442a36e17cB2439AAE25a7" - boosted = prisma_vault.getClaimableWithBoost(y) - assert boosted[0] > 0 - assert strategy.claimsAreMaxBoosted() - assert yprisma.balanceOf(strategy) == balance_1 == balance_2 - - (profit, loss) = harvest_strategy( - use_yswaps, - strategy, - token, - gov, - profit_whale, - profit_amount, - target, - force_claim=False, - ) - - # now we should have more yprisma claimed - assert yprisma.balanceOf(strategy) > balance_2 - - # test removing a strategy from the withdrawal queue def test_remove_from_withdrawal_queue( gov, @@ -582,14 +299,6 @@ def test_setters( with brownie.reverts(): strategy.setLocalKeepCrvs(0, 100000000, 0, {"from": gov}) - if which_strategy == 3: - strategy.setVoter(gov, {"from": gov}) - strategy.setLocalKeepCrv(10, {"from": gov}) - if not tests_using_tenderly: - # test our reverts as well - with brownie.reverts(): - strategy.setLocalKeepCrv(1000000, 0, 0, {"from": gov}) - elif which_strategy == 4: strategy.setVoters(gov, gov, gov, {"from": gov}) strategy.setLocalKeepCrvs(10, 10, 10, {"from": gov}) @@ -645,11 +354,11 @@ def test_sweep( assert token.balanceOf(strategy) > 0 if not tests_using_tenderly: - with brownie.reverts(): + with brownie.reverts("!want"): strategy.sweep(token, {"from": gov}) with brownie.reverts(): strategy.sweep(to_sweep, {"from": whale}) # Vault share token doesn't work - with brownie.reverts(): + with brownie.reverts("!shares"): strategy.sweep(vault.address, {"from": gov}) diff --git a/tests/strategies/test_odds_and_ends.py b/tests/strategies/test_odds_and_ends.py index d999918..acda3d1 100644 --- a/tests/strategies/test_odds_and_ends.py +++ b/tests/strategies/test_odds_and_ends.py @@ -3,6 +3,7 @@ import pytest from utils import harvest_strategy, check_status + # this module includes other tests we may need to generate, for instance to get best possible coverage on prepareReturn or liquidatePosition # do any extra testing here to hit all parts of liquidatePosition # generally this involves sending away all assets and then withdrawing before another harvest @@ -70,7 +71,7 @@ def test_liquidatePosition( print("Gauge Balance of Vault", to_send) gauge.transfer(gov, to_send, {"from": voter}) assert strategy.estimatedTotalAssets() == 0 - elif which_strategy in [2, 3]: + elif which_strategy == 2: # send all funds out of the gauge to_send = prisma_receiver.balanceOf(strategy) @@ -78,6 +79,13 @@ def test_liquidatePosition( print("Gauge Balance of Vault", to_send) assert strategy.estimatedTotalAssets() == 0 + elif which_strategy == 3: + # userVault sends away all of the gauge tokens + user_vault = strategy.userVault() + fxn_gauge = Contract(strategy.fxnGauge()) + to_send = fxn_gauge.balanceOf(user_vault) + fxn_gauge.transfer(gov, to_send, {"from": user_vault}) + assert strategy.estimatedTotalAssets() == 0 else: # wait another week so our frax LPs are unlocked chain.sleep(86400 * 7) @@ -278,7 +286,7 @@ def test_rekt( print("Gauge Balance of Vault", to_send) gauge.transfer(gov, to_send, {"from": voter}) assert strategy.estimatedTotalAssets() == 0 - elif which_strategy in [2, 3]: + elif which_strategy == 2: # send all funds out of the gauge to_send = prisma_receiver.balanceOf(strategy) @@ -286,6 +294,13 @@ def test_rekt( print("Gauge Balance of Vault", to_send) assert strategy.estimatedTotalAssets() == 0 + elif which_strategy == 3: + # userVault sends away all of the gauge tokens + user_vault = strategy.userVault() + fxn_gauge = Contract(strategy.fxnGauge()) + to_send = fxn_gauge.balanceOf(user_vault) + fxn_gauge.transfer(gov, to_send, {"from": user_vault}) + assert strategy.estimatedTotalAssets() == 0 elif which_strategy == 4: # wait another week so our frax LPs are unlocked chain.sleep(86400 * 7) @@ -366,7 +381,7 @@ def test_weird_reverts( vault.migrateStrategy(strategy, other_strategy, {"from": gov}) # can't withdraw from a non-vault address - with brownie.reverts(): + with brownie.reverts("!vault"): strategy.withdraw(1e18, {"from": gov}) @@ -444,13 +459,20 @@ def test_empty_strat( # curve needs a little push to manually get that small amount of yield earned if which_strategy == 1: new_proxy.harvest(gauge, {"from": strategy}) - elif which_strategy in [2, 3]: + elif which_strategy == 2: # send all funds out of the gauge to_send = prisma_receiver.balanceOf(strategy) prisma_receiver.withdraw(gov, to_send, {"from": strategy}) print("Gauge Balance of Vault", to_send) assert strategy.estimatedTotalAssets() == 0 + elif which_strategy == 3: + # userVault sends away all of the gauge tokens + user_vault = strategy.userVault() + fxn_gauge = Contract(strategy.fxnGauge()) + to_send = fxn_gauge.balanceOf(user_vault) + fxn_gauge.transfer(gov, to_send, {"from": user_vault}) + assert strategy.estimatedTotalAssets() == 0 elif which_strategy == 4: # wait another week so our frax LPs are unlocked chain.sleep(86400 * 7) diff --git a/tests/strategies/test_simple_harvest.py b/tests/strategies/test_simple_harvest.py index ac8cc07..75f716b 100644 --- a/tests/strategies/test_simple_harvest.py +++ b/tests/strategies/test_simple_harvest.py @@ -2,6 +2,7 @@ from utils import harvest_strategy import pytest + # test the our strategy's ability to deposit, harvest, and withdraw, with different optimal deposit tokens if we have them def test_simple_harvest( gov, @@ -108,9 +109,12 @@ def test_simple_harvest( receiver = Contract(strategy.prismaReceiver()) print("Claimable from receiver:", receiver.claimableReward(strategy)) - print( - "šŸ¤‘ Claimable profit for second harvest:", strategy.claimableProfitInUsdc() / 1e6 - ) + # curve and FXN don't have a claimable amount readable + if which_strategy not in [1, 3]: + print( + "šŸ¤‘ Claimable profit for second harvest:", + strategy.claimableProfitInUsdc() / 1e6, + ) # harvest, store new asset amount (profit, loss) = harvest_strategy( diff --git a/tests/strategies/test_triggers.py b/tests/strategies/test_triggers.py index f3809be..ee83045 100644 --- a/tests/strategies/test_triggers.py +++ b/tests/strategies/test_triggers.py @@ -3,6 +3,7 @@ import pytest from utils import harvest_strategy, check_status + # test our harvest triggers # for frax, skip this when trying coverage @pytest.mark.skip_coverage @@ -213,8 +214,9 @@ def test_triggers( # simulate earnings chain.sleep(sleep_time) + chain.mine() - # the rest of our trigger tests for the prisma receiver strategy is in test_misc + # the rest of our trigger tests for the prisma receiver strategy is in test_curve_convex_frax_operation if which_strategy == 2: return @@ -223,11 +225,13 @@ def test_triggers( # for curve vaults, we shouldn't have to worry about a lack of claimable profit, should auto-generate # set our max delay to 1 day so we trigger true, then set it back to 21 days - strategy.setMaxReportDelay(sleep_time - 1) - tx = strategy.harvestTrigger(0, {"from": gov}) - print("\nShould we harvest? Should be True.", tx) - assert tx == True - strategy.setMaxReportDelay(86400 * 21) + # we only use min delay in Curve and FXN strategies + if which_strategy not in [1, 3]: + strategy.setMaxReportDelay(1) + tx = strategy.harvestTrigger(0, {"from": gov}) + print("\nShould we harvest? Should be True.", tx) + assert tx == True + strategy.setMaxReportDelay(86400 * 21) # only convex does this mess with earmarking if which_strategy == 0: @@ -281,8 +285,8 @@ def test_triggers( ) assert tx == False strategy.setHarvestTriggerParams(90000e6, 150000e6, False, {"from": gov}) - else: # curve uses minDelay as well - strategy.setMinReportDelay(sleep_time - 1) + else: # curve and FXN use minDelay as well + strategy.setMinReportDelay(1) tx = strategy.harvestTrigger(0, {"from": gov}) print("\nShould we harvest? Should be True.", tx) assert tx == True diff --git a/tests/strategies/test_withdraw_after_donation.py b/tests/strategies/test_withdraw_after_donation.py index 63b58e0..140e647 100644 --- a/tests/strategies/test_withdraw_after_donation.py +++ b/tests/strategies/test_withdraw_after_donation.py @@ -2,6 +2,7 @@ import pytest from utils import harvest_strategy, check_status + # these tests all assess whether a strategy will hit accounting errors following donations to the strategy. # lower debtRatio to 50%, donate, withdraw less than the donation, then harvest def test_withdraw_after_donation_1( diff --git a/tests/strategies/test_yswaps.py b/tests/strategies/test_yswaps.py index 7392efa..8c753ff 100644 --- a/tests/strategies/test_yswaps.py +++ b/tests/strategies/test_yswaps.py @@ -2,6 +2,7 @@ from brownie import ZERO_ADDRESS, interface, chain from utils import harvest_strategy + # test our permissionless swaps and our trade handler functions as intended def test_keepers_and_trade_handler( gov, @@ -21,6 +22,7 @@ def test_keepers_and_trade_handler( which_strategy, tests_using_tenderly, yprisma, + fxn_whale, ): # no testing needed if we're not using yswaps if not use_yswaps: @@ -67,52 +69,94 @@ def test_keepers_and_trade_handler( ####### ADD LOGIC AS NEEDED FOR SENDING REWARDS TO STRATEGY ####### # send our strategy some CRV. normally it would be sitting waiting for trade handler but we automatically process it - crv = interface.IERC20(strategy.crv()) - crv.transfer(strategy, 100e18, {"from": crv_whale}) + if which_strategy != 3: + crv = interface.IERC20(strategy.crv()) + crv.transfer(strategy, 100e18, {"from": crv_whale}) - # whale can't sweep, but trade handler can - if not tests_using_tenderly: - with brownie.reverts(): - crv.transferFrom( - strategy, whale, crv.balanceOf(strategy) / 2, {"from": whale} + # whale can't sweep, but trade handler can + if not tests_using_tenderly: + with brownie.reverts(): + crv.transferFrom( + strategy, whale, crv.balanceOf(strategy) / 2, {"from": whale} + ) + + crv.transferFrom( + strategy, whale, crv.balanceOf(strategy) / 2, {"from": trade_factory} + ) + + if which_strategy == 2: + yprisma.transferFrom( + strategy, + whale, + yprisma.balanceOf(strategy) / 2, + {"from": trade_factory}, ) - crv.transferFrom( - strategy, whale, crv.balanceOf(strategy) / 2, {"from": trade_factory} - ) + # remove our trade handler + strategy.removeTradeFactoryPermissions(True, {"from": gov}) + assert strategy.tradeFactory() == ZERO_ADDRESS + assert crv.balanceOf(strategy) > 0 - if which_strategy in [2, 3]: - yprisma.transferFrom( - strategy, whale, yprisma.balanceOf(strategy) / 2, {"from": trade_factory} + # trade factory now cant sweep + if not tests_using_tenderly: + with brownie.reverts(): + crv.transferFrom( + strategy, + whale, + crv.balanceOf(strategy) / 2, + {"from": trade_factory}, + ) + if which_strategy == 2: + assert yprisma.allowance(strategy, trade_factory) == 0 + if yprisma.balanceOf(strategy) > 0: + with brownie.reverts(): + yprisma.transferFrom( + strategy, + whale, + yprisma.balanceOf(strategy) / 2, + {"from": trade_factory}, + ) + + # give back those permissions, now trade factory can sweep + strategy.updateTradeFactory(trade_factory, {"from": gov}) + crv.transferFrom( + strategy, whale, crv.balanceOf(strategy) / 2, {"from": trade_factory} ) + else: + fxn = interface.IERC20(strategy.fxn()) + fxn.transfer(strategy, 100e18, {"from": fxn_whale}) - # remove our trade handler - strategy.removeTradeFactoryPermissions(True, {"from": gov}) - assert strategy.tradeFactory() == ZERO_ADDRESS - assert crv.balanceOf(strategy) > 0 + # whale can't sweep, but trade handler can + if not tests_using_tenderly: + with brownie.reverts(): + fxn.transferFrom( + strategy, whale, fxn.balanceOf(strategy) / 2, {"from": whale} + ) - # trade factory now cant sweep - if not tests_using_tenderly: - with brownie.reverts(): - crv.transferFrom( - strategy, whale, crv.balanceOf(strategy) / 2, {"from": trade_factory} - ) - if which_strategy in [2, 3]: - assert yprisma.allowance(strategy, trade_factory) == 0 - if yprisma.balanceOf(strategy) > 0: - with brownie.reverts(): - yprisma.transferFrom( - strategy, - whale, - yprisma.balanceOf(strategy) / 2, - {"from": trade_factory}, - ) - - # give back those permissions, now trade factory can sweep - strategy.updateTradeFactory(trade_factory, {"from": gov}) - crv.transferFrom( - strategy, whale, crv.balanceOf(strategy) / 2, {"from": trade_factory} - ) + fxn.transferFrom( + strategy, whale, fxn.balanceOf(strategy) / 2, {"from": trade_factory} + ) + + # remove our trade handler + strategy.removeTradeFactoryPermissions(True, {"from": gov}) + assert strategy.tradeFactory() == ZERO_ADDRESS + assert fxn.balanceOf(strategy) > 0 + + # trade factory now cant sweep + if not tests_using_tenderly: + with brownie.reverts(): + fxn.transferFrom( + strategy, + whale, + fxn.balanceOf(strategy) / 2, + {"from": trade_factory}, + ) + + # give back those permissions, now trade factory can sweep + strategy.updateTradeFactory(trade_factory, {"from": gov}) + fxn.transferFrom( + strategy, whale, fxn.balanceOf(strategy) / 2, {"from": trade_factory} + ) # remove again! strategy.removeTradeFactoryPermissions(False, {"from": gov}) @@ -126,7 +170,7 @@ def test_keepers_and_trade_handler( # can't set trade factory to zero if not tests_using_tenderly: - with brownie.reverts(): + with brownie.reverts("Cant remove with this function"): strategy.updateTradeFactory(ZERO_ADDRESS, {"from": gov}) # remove again! @@ -150,15 +194,12 @@ def test_keepers_and_trade_handler( # for convex, 0 position may be occupied by wrapped CVX token with brownie.reverts(): strategy.rewardsTokens(1) - if which_strategy == 1: - with brownie.reverts(): - strategy.rewardsTokens(0) - if which_strategy == 4: + if which_strategy in [1, 3, 4]: with brownie.reverts(): strategy.rewardsTokens(0) - # only gov can update rewards - if which_strategy not in [2, 3]: + # only vault managers can update rewards, prisma doesn't have extra rewards + if which_strategy != 2: if which_strategy != 1: with brownie.reverts(): strategy.updateRewards({"from": whale}) diff --git a/tests/utils.py b/tests/utils.py index 7a3405c..839b94a 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -2,6 +2,7 @@ import brownie, time from brownie import interface, chain, accounts, ZERO_ADDRESS + # returns (profit, loss) of a harvest def harvest_strategy( use_yswaps, @@ -16,7 +17,7 @@ def harvest_strategy( # reset everything with a sleep and mine chain.sleep(1) - # chain.mine(1) + chain.mine() # add in any custom logic needed here, for instance with router strategy (also reason we have a destination strategy). # also add in any custom logic needed to get raw reward assets to the strategy (like for liquity) @@ -31,8 +32,9 @@ def harvest_strategy( # this should only happen with convex strategies if target == 0: - booster = interface.IConvexBooster(strategy.depositContract()) - booster.earmarkRewards(strategy.pid(), {"from": profit_whale}) + # we used to earmark each time, but don't anymore since convex added logic to prevent over-harvesting + # booster = interface.IConvexBooster(strategy.depositContract()) + # booster.earmarkRewards(strategy.pid(), {"from": profit_whale}) # when in emergency exit we don't enter prepare return, so we should manually claim rewards when withdrawing if strategy.emergencyExit(): @@ -48,7 +50,7 @@ def harvest_strategy( print("\nTurned off health check!\n") # for PRISMA, force claims by default - if target in [2, 3]: + if target == 2: claim_or_not = strategy.claimParams()["shouldClaimRewards"] print("Claim or not:", claim_or_not) strategy.setClaimParams(force_claim, claim_or_not, {"from": vault.governance()}) @@ -99,44 +101,60 @@ def trade_handler_action( crvBalance = 0 cvxBalance = 0 yprismaBalance = 0 + fxnBalance = 0 - crv = interface.IERC20(strategy.crv()) - if target != 1: - cvx = interface.IERC20(strategy.convexToken()) - cvxBalance = cvx.balanceOf(strategy) + if target != 3: # fxn doesn't know anything but FXN + crv = interface.IERC20(strategy.crv()) + crvBalance = crv.balanceOf(strategy) - if target == 4: - fxs = interface.IERC20(strategy.fxs()) - fxsBalance = fxs.balanceOf(strategy) + if target != 1: + cvx = interface.IERC20(strategy.convexToken()) + cvxBalance = cvx.balanceOf(strategy) - if target in [2, 3]: - yprisma = interface.IERC20(strategy.yPrisma()) - yprismaBalance = yprisma.balanceOf(strategy) + if target == 4: + fxs = interface.IERC20(strategy.fxs()) + fxsBalance = fxs.balanceOf(strategy) - crvBalance = crv.balanceOf(strategy) + if target == 2: + yprisma = interface.IERC20(strategy.yPrisma()) + yprismaBalance = yprisma.balanceOf(strategy) + else: + fxn = interface.IERC20(strategy.fxn()) + fxnBalance = fxn.balanceOf(strategy) if crvBalance > 0: crv.transfer(token, crvBalance, {"from": strategy}) - print("CRV rewards present:", crvBalance / 1e18) + print("\nšŸŒ€ CRV rewards present:", crvBalance / 1e18, "\n") assert crv.balanceOf(strategy) == 0 if cvxBalance > 0: cvx.transfer(token, cvxBalance, {"from": strategy}) - print("CVX rewards present:", cvxBalance / 1e18) + print("\nšŸ¦ CVX rewards present:", cvxBalance / 1e18, "\n") assert cvx.balanceOf(strategy) == 0 if fxsBalance > 0: fxs.transfer(token, fxsBalance, {"from": strategy}) - print("FXS rewards present:", fxsBalance / 1e18) + print("\nšŸŒ— FXS rewards present:", fxsBalance / 1e18, "\n") assert fxs.balanceOf(strategy) == 0 if yprismaBalance > 0: yprisma.transfer(token, yprismaBalance, {"from": strategy}) - print("yPRISMA rewards present:", yprismaBalance / 1e18) + print("\n🌈 yPRISMA rewards present:", yprismaBalance / 1e18, "\n") assert yprisma.balanceOf(strategy) == 0 + if fxnBalance > 0: + fxn.transfer(token, fxnBalance, {"from": strategy}) + print("\n🧮 FXN rewards present:", fxnBalance / 1e18, "\n") + assert fxn.balanceOf(strategy) == 0 + # send our profits back in - if crvBalance > 0 or cvxBalance > 0 or fxsBalance > 0 or yprismaBalance > 0: + if ( + crvBalance > 0 + or cvxBalance > 0 + or fxsBalance > 0 + or yprismaBalance > 0 + or fxnBalance > 0 + ): token.transfer(strategy, profit_amount, {"from": profit_whale}) print("Rewards converted into profit and returned") assert strategy.balanceOfWant() > 0