diff --git a/chains/PolygonLib.sol b/chains/PolygonLib.sol index 9cf93331..5161c079 100644 --- a/chains/PolygonLib.sol +++ b/chains/PolygonLib.sol @@ -58,6 +58,7 @@ library PolygonLib { address public constant POOL_UNISWAPV3_USDCe_WETH_500 = 0x45dDa9cb7c25131DF268515131f647d726f50608; address public constant POOL_UNISWAPV3_PROFIT_WETH_100 = 0xE5e70cb76446BEE0053b1EdF22CaDa861c80D51F; address public constant POOL_UNISWAPV3_WETH_COMP_3000 = 0x2260E0081A2A042DC55A07D379eb3c18bE28A1F2; + address public constant POOL_UNISWAPV3_WMATIC_COMP_3000 = 0x495b3576e2f67fa870e14d0996433FbdB4015794; address public constant POOL_QUICKSWAPV3_USDCe_USDT = 0x7B925e617aefd7FB3a93Abe3a701135D7a1Ba710; address public constant POOL_QUICKSWAPV3_USDCe_DAI = 0xe7E0eB9F6bCcCfe847fDf62a3628319a092F11a2; address public constant POOL_QUICKSWAPV3_USDCe_WETH = 0x55CAaBB0d2b704FD0eF8192A7E35D8837e678207; @@ -153,6 +154,13 @@ library PolygonLib { address public constant CONVEX_REWARD_POOL_crvUSD_DAI = 0xaCb744c7e7C95586DB83Eda3209e6483Fb1FCbA4; address public constant CONVEX_REWARD_POOL_crvUSD_USDC = 0x11F2217fa1D5c44Eae310b9b985E2964FC47D8f9; + // Yearn V3 + address public constant YEARN_DAI = 0x90b2f54C6aDDAD41b8f6c4fCCd555197BC0F773B; + address public constant YEARN_USDT = 0xBb287E6017d3DEb0e2E65061e8684eab21060123; + address public constant YEARN_USDCe = 0xA013Fbd4b711f9ded6fB09C1c0d358E2FbC2EAA0; + address public constant YEARN_WMATIC = 0x28F53bA70E5c8ce8D03b1FaD41E9dF11Bb646c36; + address public constant YEARN_WETH = 0x305F25377d0a39091e99B975558b1bdfC3975654; + function runDeploy(bool showLog) internal returns (address platform) { //region ----- DeployPlatform ----- uint[] memory buildingPrice = new uint[](3); @@ -160,7 +168,7 @@ library PolygonLib { buildingPrice[1] = 50_000e18; buildingPrice[2] = 100_000e18; platform = DeployLib.deployPlatform( - "24.01.1-alpha", + "24.05.0-alpha", MULTISIG, TOKEN_PM, TOKEN_SDIV, @@ -260,6 +268,19 @@ library PolygonLib { DeployLib.logAddedFarms(address(factory), showLog); //endregion -- Add farms ----- + //region ----- Add strategy available init params ----- + IFactory.StrategyAvailableInitParams memory p; + p.initAddresses = new address[](5); + p.initAddresses[0] = YEARN_USDCe; + p.initAddresses[1] = YEARN_USDT; + p.initAddresses[2] = YEARN_DAI; + p.initAddresses[3] = YEARN_WETH; + p.initAddresses[4] = YEARN_WMATIC; + p.initNums = new uint[](0); + p.initTicks = new int24[](0); + factory.setStrategyAvailableInitParams(StrategyIdLib.YEARN, p); + //endregion -- Add strategy available init params ----- + //region ----- Reward tokens ----- IPlatform(platform).setAllowedBBTokenVaults(TOKEN_PROFIT, 2); address[] memory allowedBoostRewardToken = new address[](2); @@ -281,6 +302,7 @@ library PolygonLib { DeployStrategyLib.deployStrategy(platform, StrategyIdLib.ICHI_RETRO_MERKL_FARM, true); DeployStrategyLib.deployStrategy(platform, StrategyIdLib.GAMMA_RETRO_MERKL_FARM, true); DeployStrategyLib.deployStrategy(platform, StrategyIdLib.CURVE_CONVEX_FARM, true); + DeployStrategyLib.deployStrategy(platform, StrategyIdLib.YEARN, false); DeployLib.logDeployStrategies(platform, showLog); //endregion -- Deploy strategy logics ----- @@ -736,7 +758,10 @@ library PolygonLib { _farms[i++] = _makeCurveConvexFarm(POOL_CURVE_crvUSD_USDC, CONVEX_REWARD_POOL_crvUSD_USDC); } - function _makeCurveConvexFarm(address curvePool, address convexRewardPool) internal view returns (IFactory.Farm memory) { + function _makeCurveConvexFarm( + address curvePool, + address convexRewardPool + ) internal view returns (IFactory.Farm memory) { IFactory.Farm memory farm; uint rewardTokensLength = IConvexRewardPool(convexRewardPool).rewardLength(); farm.status = 0; diff --git a/script/PrepareUpgrade.24.05.0-alpha.Polygon.s.sol b/script/PrepareUpgrade.24.05.0-alpha.Polygon.s.sol new file mode 100644 index 00000000..724f23d5 --- /dev/null +++ b/script/PrepareUpgrade.24.05.0-alpha.Polygon.s.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "forge-std/Script.sol"; +import "../src/core/Factory.sol"; +import "../src/core/vaults/CVault.sol"; +import "../src/core/vaults/RVault.sol"; +import "../src/core/vaults/RMVault.sol"; +import "../src/strategies/YearnStrategy.sol"; + +contract PrepareUpgrade5Polygon is Script { + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + // Factory 1.1.0: getDeploymentKey fix for not farming strategies, strategyAvailableInitParams + new Factory(); + + // CVault 1.3.0: VaultBase 1.3.0 + new CVault(); + + // RVault 1.3.0: VaultBase 1.3.0 + new RVault(); + + // RMVault 1.3.0: VaultBase 1.3.0 + new RMVault(); + + // new strategy implementation + new YearnStrategy(); + + vm.stopBroadcast(); + } + + function testDeployPolygon() external {} +} diff --git a/script/libs/DeployStrategyLib.sol b/script/libs/DeployStrategyLib.sol index d6e4e3f7..9a6c0951 100644 --- a/script/libs/DeployStrategyLib.sol +++ b/script/libs/DeployStrategyLib.sol @@ -13,6 +13,7 @@ import "../../src/strategies/IchiQuickSwapMerklFarmStrategy.sol"; import "../../src/strategies/IchiRetroMerklFarmStrategy.sol"; import "../../src/strategies/GammaRetroMerklFarmStrategy.sol"; import "../../src/strategies/CurveConvexFarmStrategy.sol"; +import "../../src/strategies/YearnStrategy.sol"; import "../../src/strategies/libs/StrategyDeveloperLib.sol"; library DeployStrategyLib { @@ -59,6 +60,10 @@ library DeployStrategyLib { implementation = address(new CurveConvexFarmStrategy()); } + if (CommonLib.eq(id, StrategyIdLib.YEARN)) { + implementation = address(new YearnStrategy()); + } + // nosemgrep require(implementation != address(0), "DeployStrategyLib: unknown strategy"); diff --git a/src/core/Factory.sol b/src/core/Factory.sol index b57cdc6e..303a6966 100644 --- a/src/core/Factory.sol +++ b/src/core/Factory.sol @@ -21,6 +21,8 @@ import "../interfaces/IStrategyLogic.sol"; /// @notice Platform factory assembling vaults. Stores vault settings, strategy logic, farms. /// Provides the opportunity to upgrade vaults and strategies. +/// Changelog: +/// 1.1.0: getDeploymentKey fix for not farming strategies, strategyAvailableInitParams /// @author Alien Deployer (https://github.com/a17) /// @author Jude (https://github.com/iammrjude) /// @author JodsMigel (https://github.com/JodsMigel) @@ -31,7 +33,7 @@ contract Factory is Controllable, ReentrancyGuardUpgradeable, IFactory { //region ----- Constants ----- /// @inheritdoc IControllable - string public constant VERSION = "1.0.3"; + string public constant VERSION = "1.1.0"; uint internal constant _WEEK = 60 * 60 * 24 * 7; @@ -43,29 +45,6 @@ contract Factory is Controllable, ReentrancyGuardUpgradeable, IFactory { //endregion -- Constants ----- - //region ----- Storage ----- - - /// @custom:storage-location erc7201:stability.Factory - struct FactoryStorage { - /// @inheritdoc IFactory - mapping(bytes32 typeHash => VaultConfig) vaultConfig; - /// @inheritdoc IFactory - mapping(bytes32 idHash => StrategyLogicConfig) strategyLogicConfig; - /// @inheritdoc IFactory - mapping(bytes32 deploymentKey => address vaultProxy) deploymentKey; - /// @inheritdoc IFactory - mapping(address vault => uint status) vaultStatus; - /// @inheritdoc IFactory - mapping(address address_ => bool isStrategy_) isStrategy; - EnumerableSet.Bytes32Set vaultTypeHashes; - EnumerableSet.Bytes32Set strategyLogicIdHashes; - mapping(uint week => mapping(uint builderPermitTokenId => uint vaultsBuilt)) vaultsBuiltByPermitTokenId; - address[] deployedVaults; - Farm[] farms; - } - - //endregion -- Storage ----- - //region ----- Data types ----- struct DeployVaultAndStrategyVars { @@ -100,16 +79,9 @@ contract Factory is Controllable, ReentrancyGuardUpgradeable, IFactory { /// @inheritdoc IFactory function setVaultConfig(VaultConfig memory vaultConfig_) external onlyOperator { FactoryStorage storage $ = _getStorage(); - string memory type_ = vaultConfig_.vaultType; - bytes32 typeHash = keccak256(abi.encodePacked(type_)); - $.vaultConfig[typeHash] = vaultConfig_; - bool newVaultType = $.vaultTypeHashes.add(typeHash); - if (!newVaultType) { + if (FactoryLib.setVaultConfig($, vaultConfig_)) { _requireGovernanceOrMultisig(); } - emit VaultConfigChanged( - type_, vaultConfig_.implementation, vaultConfig_.deployAllowed, vaultConfig_.upgradeAllowed, newVaultType - ); } /// @inheritdoc IFactory @@ -165,12 +137,22 @@ contract Factory is Controllable, ReentrancyGuardUpgradeable, IFactory { emit UpdateFarm(id, farm_); } + /// @inheritdoc IFactory + function setStrategyAvailableInitParams( + string memory id, + StrategyAvailableInitParams memory initParams + ) external onlyOperator { + FactoryStorage storage $ = _getStorage(); + bytes32 idHash = keccak256(abi.encodePacked(id)); + $.strategyAvailableInitParams[idHash] = initParams; + emit SetStrategyAvailableInitParams(id, initParams.initAddresses, initParams.initNums, initParams.initTicks); + } + //endregion -- Restricted actions ---- //region ----- User actions ----- /// @inheritdoc IFactory - //slither-disable-next-line cyclomatic-complexity reentrancy-benign function deployVaultAndStrategy( string memory vaultType, @@ -536,7 +518,7 @@ contract Factory is Controllable, ReentrancyGuardUpgradeable, IFactory { initStrategyAddresses, initStrategyNums, initStrategyTicks, - [1, 0, 0, 1, 0] + [1, 0, 1, 1, 0] ); } @@ -594,6 +576,12 @@ contract Factory is Controllable, ReentrancyGuardUpgradeable, IFactory { return _getStorage().vaultsBuiltByPermitTokenId[week][builderPermitTokenId]; } + /// @inheritdoc IFactory + function strategyAvailableInitParams(bytes32 idHash) external view returns (StrategyAvailableInitParams memory) { + FactoryStorage storage $ = _getStorage(); + return $.strategyAvailableInitParams[idHash]; + } + //endregion -- View functions ----- //region ----- Internal logic ----- diff --git a/src/core/base/VaultBase.sol b/src/core/base/VaultBase.sol index ada6bac4..c08e541c 100644 --- a/src/core/base/VaultBase.sol +++ b/src/core/base/VaultBase.sol @@ -7,6 +7,7 @@ import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol import "./Controllable.sol"; import "../libs/ConstantsLib.sol"; import "../libs/VaultStatusLib.sol"; +import "../libs/VaultBaseLib.sol"; import "../../interfaces/IVault.sol"; import "../../interfaces/IStrategy.sol"; import "../../interfaces/IPriceReader.sol"; @@ -19,6 +20,11 @@ import "../../interfaces/IFactory.sol"; /// User can deposit and withdraw a changing set of assets managed by the strategy. /// Start price of vault share is $1. /// @dev Used by all vault implementations (CVault, RVault, etc) +/// Changelog: +/// 1.3.0: hardWorkMintFeeCallback +/// 1.2.0: isHardWorkOnDepositAllowed +/// 1.1.0: setName, setSymbol, gas optimization +/// 1.0.1: add receiver and owner args to withdrawAssets method /// @author Alien Deployer (https://github.com/a17) /// @author JodsMigel (https://github.com/JodsMigel) abstract contract VaultBase is Controllable, ERC20Upgradeable, ReentrancyGuardUpgradeable, IVault { @@ -29,7 +35,7 @@ abstract contract VaultBase is Controllable, ERC20Upgradeable, ReentrancyGuardUp /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @dev Version of VaultBase implementation - string public constant VERSION_VAULT_BASE = "1.2.0"; + string public constant VERSION_VAULT_BASE = "1.3.0"; /// @dev Delay between deposits/transfers and withdrawals uint internal constant _WITHDRAW_REQUEST_BLOCKS = 5; @@ -41,7 +47,7 @@ abstract contract VaultBase is Controllable, ERC20Upgradeable, ReentrancyGuardUp uint internal constant _MIN_HARDWORK_DELAY = 3600; // keccak256(abi.encode(uint256(keccak256("erc7201:stability.VaultBase")) - 1)) & ~bytes32(uint256(0xff)); - bytes32 private constant VAULTBASE_STORAGE_LOCATION = + bytes32 private constant _VAULTBASE_STORAGE_LOCATION = 0xd602ae9af1fed726d4890dcf3c81a074ed87a6343646550e5de293c5a9330a00; /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ @@ -98,6 +104,19 @@ abstract contract VaultBase is Controllable, ERC20Upgradeable, ReentrancyGuardUp /// @dev Need to receive ETH for HardWork and re-balance gas compensation receive() external payable {} + /// @inheritdoc IVault + function hardWorkMintFeeCallback(address[] memory revenueAssets, uint[] memory revenueAmounts) external virtual { + (address[] memory feeReceivers, uint[] memory feeShares) = VaultBaseLib.hardWorkMintFeeCallback( + IPlatform(platform()), revenueAssets, revenueAmounts, _getVaultBaseStorage() + ); + uint len = feeReceivers.length; + for (uint i; i < len; ++i) { + if (feeShares[i] != 0) { + _mint(feeReceivers[i], feeShares[i]); + } + } + } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* RESTRICTED ACTIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ @@ -415,7 +434,7 @@ abstract contract VaultBase is Controllable, ERC20Upgradeable, ReentrancyGuardUp function _getVaultBaseStorage() internal pure returns (VaultBaseStorage storage $) { //slither-disable-next-line assembly assembly { - $.slot := VAULTBASE_STORAGE_LOCATION + $.slot := _VAULTBASE_STORAGE_LOCATION } } diff --git a/src/core/libs/FactoryLib.sol b/src/core/libs/FactoryLib.sol index ec04de13..7ccb13df 100644 --- a/src/core/libs/FactoryLib.sol +++ b/src/core/libs/FactoryLib.sol @@ -15,6 +15,7 @@ import "../../interfaces/IRVault.sol"; library FactoryLib { using SafeERC20 for IERC20; + using EnumerableSet for EnumerableSet.Bytes32Set; uint public constant BOOST_REWARD_DURATION = 86400 * 30; @@ -182,7 +183,7 @@ library FactoryLib { vars.strategyInitAddresses, vars.strategyInitNums, vars.strategyInitTicks, - [1, 0, 0, 1, 0] + [1, 0, 1, 1, 0] ); if (factory.deploymentKey(_deploymentKey) == address(0)) { @@ -293,7 +294,7 @@ library FactoryLib { vars.strategyInitAddresses, vars.strategyInitNums, vars.strategyInitTicks, - [1, 0, 0, 1, 0] + [1, 0, 1, 1, 0] ); if (factory.deploymentKey(_deploymentKey) == address(0)) { @@ -637,4 +638,20 @@ library FactoryLib { } } } + + function setVaultConfig( + IFactory.FactoryStorage storage $, + IFactory.VaultConfig memory vaultConfig_ + ) external returns (bool needGovOrMultisigAccess) { + string memory type_ = vaultConfig_.vaultType; + bytes32 typeHash = keccak256(abi.encodePacked(type_)); + $.vaultConfig[typeHash] = vaultConfig_; + bool newVaultType = $.vaultTypeHashes.add(typeHash); + if (!newVaultType) { + needGovOrMultisigAccess = true; + } + emit IFactory.VaultConfigChanged( + type_, vaultConfig_.implementation, vaultConfig_.deployAllowed, vaultConfig_.upgradeAllowed, newVaultType + ); + } } diff --git a/src/core/libs/VaultBaseLib.sol b/src/core/libs/VaultBaseLib.sol new file mode 100644 index 00000000..0b434a5b --- /dev/null +++ b/src/core/libs/VaultBaseLib.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "../../core/libs/ConstantsLib.sol"; +import "../../interfaces/IVault.sol"; +import "../../interfaces/IControllable.sol"; +import "../../interfaces/IPlatform.sol"; +import "../../interfaces/IVaultManager.sol"; +import "../../interfaces/IFactory.sol"; +import "../../interfaces/IStrategyLogic.sol"; + +library VaultBaseLib { + using SafeERC20 for IERC20; + + struct MintFeesVars { + uint feePlatform; + uint feeShareVaultManager; + uint feeShareStrategyLogic; + uint feeShareEcosystem; + uint vaultSharesForPlatform; + uint vaultSharesForVaultManager; + uint vaultSharesForStrategyLogic; + uint vaultSharesForEcosystem; + } + + function hardWorkMintFeeCallback( + IPlatform platform, + address[] memory revenueAssets, + uint[] memory revenueAmounts, + IVault.VaultBaseStorage storage $ + ) external returns (address[] memory feeReceivers, uint[] memory feeShares) { + MintFeesVars memory v; + IStrategy s = $.strategy; + if (address(s) != msg.sender) { + revert IControllable.IncorrectMsgSender(); + } + + (, uint revenueSharesOut,) = IVault(address(this)).previewDepositAssets(revenueAssets, revenueAmounts); + + (v.feePlatform, v.feeShareVaultManager, v.feeShareStrategyLogic, v.feeShareEcosystem) = platform.getFees(); + uint strategyLogicTokenId = + IFactory(platform.factory()).strategyLogicConfig(keccak256(bytes(s.strategyLogicId()))).tokenId; + + uint returnArraysLength = 2; + v.vaultSharesForPlatform = revenueSharesOut * v.feePlatform / ConstantsLib.DENOMINATOR; + v.vaultSharesForVaultManager = v.vaultSharesForPlatform * v.feeShareVaultManager / ConstantsLib.DENOMINATOR; + v.vaultSharesForStrategyLogic = v.vaultSharesForPlatform * v.feeShareStrategyLogic / ConstantsLib.DENOMINATOR; + if (v.feeShareEcosystem != 0) { + v.vaultSharesForEcosystem = v.vaultSharesForPlatform * v.feeShareEcosystem / ConstantsLib.DENOMINATOR; + ++returnArraysLength; + } + uint multisigShare = + ConstantsLib.DENOMINATOR - v.feeShareVaultManager - v.feeShareStrategyLogic - v.feeShareEcosystem; + uint vaultSharesForMultisig; + if (multisigShare > 0) { + vaultSharesForMultisig = v.vaultSharesForPlatform - v.vaultSharesForVaultManager + - v.vaultSharesForStrategyLogic - v.vaultSharesForEcosystem; + ++returnArraysLength; + } + feeReceivers = new address[](returnArraysLength); + feeShares = new uint[](returnArraysLength); + + // vaultManagerReceiver + feeReceivers[0] = IVaultManager(platform.vaultManager()).getRevenueReceiver($.tokenId); + feeShares[0] = v.vaultSharesForVaultManager; + // strategyLogicReceiver + feeReceivers[1] = IStrategyLogic(platform.strategyLogic()).getRevenueReceiver(strategyLogicTokenId); + feeShares[1] = v.vaultSharesForStrategyLogic; + // ecosystem + uint k = 2; + if (v.vaultSharesForEcosystem != 0) { + feeReceivers[k] = platform.ecosystemRevenueReceiver(); + feeShares[k] = v.vaultSharesForEcosystem; + ++k; + } + if (vaultSharesForMultisig != 0) { + feeReceivers[k] = platform.multisig(); + feeShares[k] = vaultSharesForMultisig; + } + emit IVault.MintFees( + v.vaultSharesForVaultManager, + v.vaultSharesForStrategyLogic, + v.vaultSharesForEcosystem, + vaultSharesForMultisig + ); + } +} diff --git a/src/core/vaults/CVault.sol b/src/core/vaults/CVault.sol index 354f763e..a54dcc80 100644 --- a/src/core/vaults/CVault.sol +++ b/src/core/vaults/CVault.sol @@ -14,7 +14,7 @@ contract CVault is VaultBase { //region ----- Constants ----- /// @dev Version of CVault implementation - string public constant VERSION = "1.2.0"; + string public constant VERSION = "1.3.0"; uint internal constant _UNIQUE_INIT_ADDRESSES = 1; diff --git a/src/core/vaults/RMVault.sol b/src/core/vaults/RMVault.sol index a817cc5e..61384140 100644 --- a/src/core/vaults/RMVault.sol +++ b/src/core/vaults/RMVault.sol @@ -21,7 +21,7 @@ contract RMVault is RVaultBase, IManagedVault { //region ----- Constants ----- /// @dev Version of RMVault implementation - string public constant VERSION = "1.2.0"; + string public constant VERSION = "1.3.0"; uint internal constant _UNIQUE_INIT_ADDRESSES = 1; @@ -47,6 +47,15 @@ contract RMVault is RVaultBase, IManagedVault { //endregion -- Init ----- + //region ----- Callbacks ----- + + /// @inheritdoc IVault + function hardWorkMintFeeCallback(address[] memory, uint[] memory) external pure override(VaultBase, IVault) { + revert NotSupported(); + } + + //endregion -- Callbacks ----- + //region ----- User actions ----- /// @inheritdoc IManagedVault diff --git a/src/core/vaults/RVault.sol b/src/core/vaults/RVault.sol index f4e19e24..36a84265 100644 --- a/src/core/vaults/RVault.sol +++ b/src/core/vaults/RVault.sol @@ -20,7 +20,7 @@ contract RVault is RVaultBase { //region ----- Constants ----- /// @dev Version of RVault implementation - string public constant VERSION = "1.2.0"; + string public constant VERSION = "1.3.0"; uint public constant BB_TOKEN_DURATION = 86400 * 7; @@ -81,6 +81,15 @@ contract RVault is RVaultBase { //endregion -- Init ----- + //region ----- Callbacks ----- + + /// @inheritdoc IVault + function hardWorkMintFeeCallback(address[] memory, uint[] memory) external pure override(VaultBase, IVault) { + revert NotSupported(); + } + + //endregion -- Callbacks ----- + //region ----- View functions ----- /// @inheritdoc IVault diff --git a/src/integrations/yearn/IYearnVault.sol b/src/integrations/yearn/IYearnVault.sol new file mode 100644 index 00000000..e99f6847 --- /dev/null +++ b/src/integrations/yearn/IYearnVault.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +interface IYearnVault { + /// @notice Get the full default queue currently set. + /// @return The current default withdrawal queue. + function get_default_queue() external view returns(address[] memory); + + /// @notice Get the price per share (pps) of the vault. + /// @dev This value offers limited precision. Integrations that require + /// exact precision should use convertToAssets or convertToShares instead. + /// @return The price per share. + function pricePerShare() external view returns(uint); + +} \ No newline at end of file diff --git a/src/interfaces/IFactory.sol b/src/interfaces/IFactory.sol index 7a686c56..3fa32ec8 100644 --- a/src/interfaces/IFactory.sol +++ b/src/interfaces/IFactory.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + /// @notice Creating vaults, upgrading vaults and strategies, vault list, farms and strategy logics management /// @author Alien Deployer (https://github.com/a17) /// @author Jude (https://github.com/iammrjude) @@ -49,11 +51,33 @@ interface IFactory { event VaultStatus(address indexed vault, uint newStatus); event NewFarm(Farm[] farms); event UpdateFarm(uint id, Farm farm); + event SetStrategyAvailableInitParams(string id, address[] initAddresses, uint[] initNums, int24[] initTicks); //endregion -- Events ----- //region ----- Data types ----- + /// @custom:storage-location erc7201:stability.Factory + struct FactoryStorage { + /// @inheritdoc IFactory + mapping(bytes32 typeHash => VaultConfig) vaultConfig; + /// @inheritdoc IFactory + mapping(bytes32 idHash => StrategyLogicConfig) strategyLogicConfig; + /// @inheritdoc IFactory + mapping(bytes32 deploymentKey => address vaultProxy) deploymentKey; + /// @inheritdoc IFactory + mapping(address vault => uint status) vaultStatus; + /// @inheritdoc IFactory + mapping(address address_ => bool isStrategy_) isStrategy; + EnumerableSet.Bytes32Set vaultTypeHashes; + EnumerableSet.Bytes32Set strategyLogicIdHashes; + mapping(uint week => mapping(uint builderPermitTokenId => uint vaultsBuilt)) vaultsBuiltByPermitTokenId; + address[] deployedVaults; + Farm[] farms; + /// @inheritdoc IFactory + mapping(bytes32 idHash => StrategyAvailableInitParams) strategyAvailableInitParams; + } + struct VaultConfig { string vaultType; address implementation; @@ -81,6 +105,12 @@ interface IFactory { int24[] ticks; } + struct StrategyAvailableInitParams { + address[] initAddresses; + uint[] initNums; + int24[] initTicks; + } + //endregion -- Data types ----- //region ----- View functions ----- @@ -275,6 +305,9 @@ interface IFactory { bytes32[] memory extra ); + /// @notice Initialization strategy params store + function strategyAvailableInitParams(bytes32 idHash) external view returns (StrategyAvailableInitParams memory); + //endregion -- View functions ----- //region ----- Write functions ----- @@ -333,5 +366,10 @@ interface IFactory { /// @param statuses New vault statuses. Constant from VaultStatusLib function setVaultStatus(address[] memory vaults, uint[] memory statuses) external; + /// @notice Initial addition or change of strategy available init params + /// @param id Strategy ID string + /// @param initParams Init params variations that will be parsed by strategy + function setStrategyAvailableInitParams(string memory id, StrategyAvailableInitParams memory initParams) external; + //endregion -- Write functions ----- } diff --git a/src/interfaces/IStrategy.sol b/src/interfaces/IStrategy.sol index fa8ce7a2..f103bf8b 100644 --- a/src/interfaces/IStrategy.sol +++ b/src/interfaces/IStrategy.sol @@ -137,7 +137,7 @@ interface IStrategy is IERC165 { view returns (string[] memory variants, address[] memory addresses, uint[] memory nums, int24[] memory ticks); - /// @notice How strategy earns money + /// @notice How does the strategy make money? /// @return Description in free form function description() external view returns (string memory); @@ -147,6 +147,9 @@ interface IStrategy is IERC165 { /// @notice Is HardWork can be executed function isReadyForHardWork() external view returns (bool); + /// @notice Strategy not need to process revenue on HardWorks + function autoCompoundingByUnderlyingProtocol() external view returns (bool); + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* WRITE FUNCTIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ diff --git a/src/interfaces/IVault.sol b/src/interfaces/IVault.sol index 60434639..14b4a728 100644 --- a/src/interfaces/IVault.sol +++ b/src/interfaces/IVault.sol @@ -22,6 +22,7 @@ interface IVault is IERC165 { error NotEnoughAmountToInitSupply(uint mintAmount, uint initialShares); error WaitAFewBlocks(); error StrategyZeroDeposit(); + error NotSupported(); /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* EVENTS */ @@ -36,6 +37,12 @@ interface IVault is IERC165 { event MaxSupply(uint maxShares); event VaultName(string newName); event VaultSymbol(string newSymbol); + event MintFees( + uint vaultManagerReceiverFee, + uint strategyLogicReceiverFee, + uint ecosystemRevenueReceiverFee, + uint multisigReceiverFee + ); /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* DATA TYPES */ @@ -151,6 +158,12 @@ interface IVault is IERC165 { /* WRITE FUNCTIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + /// @dev Mint fee shares callback + /// @param revenueAssets Assets returned by _claimRevenue function that was earned during HardWork + /// @param revenueAmounts Assets amounts returned from _claimRevenue function that was earned during HardWork + /// Only strategy can call this + function hardWorkMintFeeCallback(address[] memory revenueAssets, uint[] memory revenueAmounts) external; + /// @dev Deposit final assets (pool assets) to the strategy and minting of vault shares. /// If the strategy interacts with a pool or farms through an underlying token, then it will be minted. /// Emits a {DepositAssets} event with consumed amounts. diff --git a/src/strategies/YearnStrategy.sol b/src/strategies/YearnStrategy.sol new file mode 100644 index 00000000..c40ae3f9 --- /dev/null +++ b/src/strategies/YearnStrategy.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "./libs/StrategyIdLib.sol"; +import "./base/ERC4626StrategyBase.sol"; +import "../integrations/yearn/IYearnVault.sol"; + +/// @title Hodl Yearn V3 multi ERC4626 vault, emit revenue, collect fees and show underlying protocols +/// @author Alien Deployer (https://github.com/a17) +contract YearnStrategy is ERC4626StrategyBase { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IControllable + string public constant VERSION = "1.0.0"; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INITIALIZATION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IStrategy + function initialize(address[] memory addresses, uint[] memory nums, int24[] memory ticks) public initializer { + if (addresses.length != 3 || nums.length != 0 || ticks.length != 0) { + revert IControllable.IncorrectInitParams(); + } + + __ERC4626StrategyBase_init(StrategyIdLib.YEARN, addresses[0], addresses[1], addresses[2]); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* VIEW FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IStrategy + function description() external view returns (string memory) { + StrategyBaseStorage storage $base = _getStrategyBaseStorage(); + return _generateDescription($base._underlying); + } + + /// @inheritdoc IStrategy + function extra() external pure returns (bytes32) { + //slither-disable-next-line too-many-digits + return CommonLib.bytesToBytes32(abi.encodePacked(bytes3(0xdc568a), bytes3(0x000000))); + } + + /// @inheritdoc IStrategy + function getSpecificName() external view override returns (string memory, bool) { + return (_getQueueNames(underlying()), false); + } + + /// @inheritdoc IStrategy + function initVariants(address platform_) + public + view + returns (string[] memory variants, address[] memory addresses, uint[] memory nums, int24[] memory ticks) + { + IFactory.StrategyAvailableInitParams memory params = + IFactory(IPlatform(platform_).factory()).strategyAvailableInitParams(keccak256(bytes(strategyLogicId()))); + uint len = params.initAddresses.length; + variants = new string[](len); + addresses = new address[](len); + nums = new uint[](0); + ticks = new int24[](0); + for (uint i; i < len; ++i) { + variants[i] = _generateDescription(params.initAddresses[i]); + addresses[i] = params.initAddresses[i]; + } + } + + /// @inheritdoc IStrategy + function isHardWorkOnDepositAllowed() external pure returns (bool) { + return true; + } + + /// @inheritdoc IStrategy + function strategyLogicId() public pure override returns (string memory) { + return StrategyIdLib.YEARN; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL LOGIC */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + function _generateDescription(address u) internal view returns (string memory) { + //slither-disable-next-line calls-loop + return string.concat( + "Hodl ", + //slither-disable-next-line calls-loop + IERC20Metadata(u).symbol(), + " (", + _getQueueNames(u), + ")" + ); + } + + function _getQueueNames(address u) internal view returns (string memory) { + //slither-disable-next-line calls-loop + address[] memory subVaults = IYearnVault(u).get_default_queue(); + return CommonLib.implode(_getNames(subVaults), ", "); + } + + function _getNames(address[] memory assets_) internal view returns (string[] memory names) { + uint len = assets_.length; + names = new string[](len); + // nosemgrep + for (uint i; i < len; ++i) { + //slither-disable-next-line calls-loop + names[i] = IERC20Metadata(assets_[i]).name(); + } + } +} diff --git a/src/strategies/base/ERC4626StrategyBase.sol b/src/strategies/base/ERC4626StrategyBase.sol new file mode 100644 index 00000000..4861adbb --- /dev/null +++ b/src/strategies/base/ERC4626StrategyBase.sol @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import "./StrategyBase.sol"; + +/// @notice Hold ERC4626 vault shares, emit APR and collect fees +/// @author Alien Deployer (https://github.com/a17) +abstract contract ERC4626StrategyBase is StrategyBase { + using SafeERC20 for IERC20; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Version of ERC4626StrategyBase implementation + string public constant VERSION_ERC4626_STRATEGY_BASE = "1.0.0"; + + // keccak256(abi.encode(uint256(keccak256("erc7201:stability.ERC4626StrategyBase")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant ERC4626_STRATEGY_BASE_STORAGE_LOCATION = + 0x5b77806ff180dee2d0be2cd23be20d60541fe5fbef60bd0f3013af3027492200; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* DATA TYPES */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @custom:storage-location erc7201:stability.ERC4626StrategyBase + struct ERC4626StrategyBaseStorage { + uint lastSharePrice; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INITIALIZATION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + //slither-disable-next-line naming-convention + function __ERC4626StrategyBase_init( + string memory id, + address platform_, + address vault_, + address underlying_ + ) internal onlyInitializing { + address[] memory _assets = new address[](1); + _assets[0] = IERC4626(underlying_).asset(); + //slither-disable-next-line reentrancy-events + __StrategyBase_init(platform_, id, vault_, _assets, underlying_, type(uint).max); + IERC20(_assets[0]).forceApprove(underlying_, type(uint).max); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* VIEW FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IStrategy + function supportedVaultTypes() external view virtual override returns (string[] memory types) { + types = new string[](1); + types[0] = VaultTypeLib.COMPOUNDING; + } + + /// @inheritdoc IStrategy + function total() public view override returns (uint) { + StrategyBaseStorage storage __$__ = _getStrategyBaseStorage(); + return StrategyLib.balance(__$__._underlying); + } + + /// @inheritdoc IStrategy + function getAssetsProportions() public pure returns (uint[] memory proportions) { + proportions = new uint[](1); + proportions[0] = 1e18; + } + + /// @inheritdoc IStrategy + function getRevenue() public view returns (address[] memory __assets, uint[] memory amounts) { + StrategyBaseStorage storage __$__ = _getStrategyBaseStorage(); + address u = __$__._underlying; + uint newSharePrice = _getSharePrice(u); + (__assets, amounts) = _getRevenue(newSharePrice, u); + } + + /// @inheritdoc IStrategy + function autoCompoundingByUnderlyingProtocol() public view virtual override returns (bool) { + return true; + } + + /// @inheritdoc IStrategy + function isReadyForHardWork() external view virtual returns (bool isReady) { + (address[] memory __assets, uint[] memory amounts) = getRevenue(); + isReady = amounts[0] > ISwapper(IPlatform(platform()).swapper()).threshold(__assets[0]); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STRATEGY BASE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc StrategyBase + function _depositAssets(uint[] memory amounts, bool) internal override returns (uint value) { + StrategyBaseStorage storage $base = _getStrategyBaseStorage(); + address u = $base._underlying; + value = IERC4626(u).deposit(amounts[0], address(this)); + ERC4626StrategyBaseStorage storage $ = _getERC4626StrategyBaseStorage(); + if ($.lastSharePrice == 0) { + $.lastSharePrice = _getSharePrice(u); + } + } + + /// @inheritdoc StrategyBase + function _depositUnderlying(uint amount) internal override returns (uint[] memory amountsConsumed) { + amountsConsumed = new uint[](1); + StrategyBaseStorage storage $base = _getStrategyBaseStorage(); + address u = $base._underlying; + amountsConsumed[0] = IERC4626(u).convertToAssets(amount); + ERC4626StrategyBaseStorage storage $ = _getERC4626StrategyBaseStorage(); + if ($.lastSharePrice == 0) { + $.lastSharePrice = _getSharePrice(u); + } + } + + function _liquidateRewards( + address, /*exchangeAsset*/ + address[] memory, /*rewardAssets_*/ + uint[] memory /*rewardAmounts_*/ + ) internal pure override returns (uint earnedExchangeAsset) { + // do nothing + } + + /// @inheritdoc StrategyBase + function _processRevenue( + address[] memory, /*assets_*/ + uint[] memory /*amountsRemaining*/ + ) internal pure override returns (bool needCompound) { + // do nothing + } + + /// @inheritdoc StrategyBase + function _compound() internal override { + // do nothing + } + + /// @inheritdoc StrategyBase + function _previewDepositAssets(uint[] memory amountsMax) + internal + view + override(StrategyBase) + returns (uint[] memory amountsConsumed, uint value) + { + amountsConsumed = new uint[](1); + amountsConsumed[0] = amountsMax[0]; + StrategyBaseStorage storage __$__ = _getStrategyBaseStorage(); + value = IERC4626(__$__._underlying).convertToShares(amountsMax[0]); + } + + /// @inheritdoc StrategyBase + function _previewDepositAssets( + address[] memory, /*assets_*/ + uint[] memory amountsMax + ) internal view override(StrategyBase) returns (uint[] memory amountsConsumed, uint value) { + return _previewDepositAssets(amountsMax); + } + + /// @inheritdoc StrategyBase + function _withdrawAssets(uint value, address receiver) internal override returns (uint[] memory amountsOut) { + StrategyBaseStorage storage _$_ = _getStrategyBaseStorage(); + return _withdrawAssets(_$_._assets, value, receiver); + } + + /// @inheritdoc StrategyBase + function _withdrawAssets( + address[] memory, + uint value, + address receiver + ) internal override returns (uint[] memory amountsOut) { + amountsOut = new uint[](1); + StrategyBaseStorage storage __$__ = _getStrategyBaseStorage(); + amountsOut[0] = IERC4626(__$__._underlying).redeem(value, receiver, address(this)); + } + + /// @inheritdoc StrategyBase + function _withdrawUnderlying(uint amount, address receiver) internal override { + StrategyBaseStorage storage __$__ = _getStrategyBaseStorage(); + IERC20(__$__._underlying).safeTransfer(receiver, amount); + } + + /// @inheritdoc StrategyBase + function _assetsAmounts() internal view override returns (address[] memory assets_, uint[] memory amounts_) { + StrategyBaseStorage storage __$__ = _getStrategyBaseStorage(); + assets_ = __$__._assets; + address u = __$__._underlying; + amounts_ = new uint[](1); + amounts_[0] = IERC4626(u).convertToAssets(IERC20(u).balanceOf(address(this))); + } + + /// @inheritdoc StrategyBase + function _claimRevenue() + internal + override + returns ( + address[] memory __assets, + uint[] memory __amounts, + address[] memory __rewardAssets, + uint[] memory __rewardAmounts + ) + { + ERC4626StrategyBaseStorage storage $ = _getERC4626StrategyBaseStorage(); + StrategyBaseStorage storage __$__ = _getStrategyBaseStorage(); + address u = __$__._underlying; + uint newSharePrice = _getSharePrice(u); + (__assets, __amounts) = _getRevenue(newSharePrice, u); + $.lastSharePrice = newSharePrice; + __rewardAssets = new address[](0); + __rewardAmounts = new uint[](0); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL LOGIC */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + function _getERC4626StrategyBaseStorage() internal pure returns (ERC4626StrategyBaseStorage storage $) { + //slither-disable-next-line assembly + assembly { + $.slot := ERC4626_STRATEGY_BASE_STORAGE_LOCATION + } + } + + function _getSharePrice(address u) internal view returns (uint) { + // totalSupply cant be zero in our integrations + return IERC4626(u).totalAssets() * 1e18 / IERC4626(u).totalSupply(); + } + + function _getRevenue( + uint newSharePrice, + address u + ) internal view returns (address[] memory __assets, uint[] memory amounts) { + ERC4626StrategyBaseStorage storage $ = _getERC4626StrategyBaseStorage(); + StrategyBaseStorage storage __$__ = _getStrategyBaseStorage(); + __assets = __$__._assets; + amounts = new uint[](1); + uint oldSharePrice = $.lastSharePrice; + // nosemgrep + if (newSharePrice > oldSharePrice && oldSharePrice != 0) { + amounts[0] = StrategyLib.balance(u) * newSharePrice * (newSharePrice - oldSharePrice) / oldSharePrice / 1e18; + } + } +} diff --git a/src/strategies/base/StrategyBase.sol b/src/strategies/base/StrategyBase.sol index 487dae4a..ee7ea2ea 100644 --- a/src/strategies/base/StrategyBase.sol +++ b/src/strategies/base/StrategyBase.sol @@ -8,6 +8,8 @@ import "../../interfaces/IStrategy.sol"; import "../../interfaces/IVault.sol"; /// @dev Base universal strategy +/// Changelog: +/// 1.1.0: autoCompoundingByUnderlyingProtocol(), virtual total() /// @author Alien Deployer (https://github.com/a17) /// @author JodsMigel (https://github.com/JodsMigel) abstract contract StrategyBase is Controllable, IStrategy { @@ -18,7 +20,7 @@ abstract contract StrategyBase is Controllable, IStrategy { /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @dev Version of StrategyBase implementation - string public constant VERSION_STRATEGY_BASE = "1.0.0"; + string public constant VERSION_STRATEGY_BASE = "1.1.0"; // keccak256(abi.encode(uint256(keccak256("erc7201:stability.StrategyBase")) - 1)) & ~bytes32(uint256(0xff)); bytes32 private constant STRATEGYBASE_STORAGE_LOCATION = @@ -117,16 +119,29 @@ abstract contract StrategyBase is Controllable, IStrategy { uint[] memory __rewardAmounts ) = _claimRevenue(); - __amounts[exchangeAssetIndex] += - _liquidateRewards(__assets[exchangeAssetIndex], __rewardAssets, __rewardAmounts); - - uint[] memory amountsRemaining = StrategyLib.extractFees(_platform, _vault, $._id, __assets, __amounts); - - bool needCompound = _processRevenue(__assets, amountsRemaining); - - uint totalBefore = $.total; - - if (needCompound) { + //slither-disable-next-line uninitialized-local + uint totalBefore; + if (!autoCompoundingByUnderlyingProtocol()) { + __amounts[exchangeAssetIndex] += + _liquidateRewards(__assets[exchangeAssetIndex], __rewardAssets, __rewardAmounts); + + uint[] memory amountsRemaining = StrategyLib.extractFees(_platform, _vault, $._id, __assets, __amounts); + + bool needCompound = _processRevenue(__assets, amountsRemaining); + + totalBefore = $.total; + + if (needCompound) { + _compound(); + } + } else { + // maybe this is not final logic + // vault shares as fees can be used not only for autoCompoundingByUnderlyingProtocol strategies, + // but for many strategies linked to CVault if this feature will be implemented + IVault(_vault).hardWorkMintFeeCallback(__assets, __amounts); + // call empty method only for coverage or them can be overriden + _liquidateRewards(__assets[0], __rewardAssets, __rewardAmounts); + _processRevenue(__assets, __amounts); _compound(); } @@ -136,9 +151,8 @@ abstract contract StrategyBase is Controllable, IStrategy { /// @inheritdoc IStrategy function emergencyStopInvesting() external onlyGovernanceOrMultisig { - StrategyBaseStorage storage $ = _getStrategyBaseStorage(); // slither-disable-next-line unused-return - _withdrawAssets($.total, address(this)); + _withdrawAssets(total(), address(this)); } /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ @@ -168,7 +182,7 @@ abstract contract StrategyBase is Controllable, IStrategy { } /// @inheritdoc IStrategy - function total() public view override returns (uint) { + function total() public view virtual override returns (uint) { return _getStrategyBaseStorage().total; } @@ -211,6 +225,11 @@ abstract contract StrategyBase is Controllable, IStrategy { } } + /// @inheritdoc IStrategy + function autoCompoundingByUnderlyingProtocol() public view virtual returns (bool) { + return false; + } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* Default implementations */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ diff --git a/src/strategies/libs/StrategyDeveloperLib.sol b/src/strategies/libs/StrategyDeveloperLib.sol index 709573de..55c45d84 100644 --- a/src/strategies/libs/StrategyDeveloperLib.sol +++ b/src/strategies/libs/StrategyDeveloperLib.sol @@ -32,6 +32,9 @@ library StrategyDeveloperLib { if (CommonLib.eq(strategyId, StrategyIdLib.CURVE_CONVEX_FARM)) { return 0x88888887C3ebD4a33E34a15Db4254C74C75E5D4A; } + if (CommonLib.eq(strategyId, StrategyIdLib.YEARN)) { + return 0x88888887C3ebD4a33E34a15Db4254C74C75E5D4A; + } return address(0); } diff --git a/src/strategies/libs/StrategyIdLib.sol b/src/strategies/libs/StrategyIdLib.sol index 5fe0e7bf..14dcecb7 100644 --- a/src/strategies/libs/StrategyIdLib.sol +++ b/src/strategies/libs/StrategyIdLib.sol @@ -12,4 +12,5 @@ library StrategyIdLib { string internal constant QUICKSWAP_STATIC_MERKL_FARM = "QuickSwap Static Merkl Farm"; string internal constant GAMMA_RETRO_MERKL_FARM = "Gamma Retro Merkl Farm"; string internal constant CURVE_CONVEX_FARM = "Curve Convex Farm"; + string internal constant YEARN = "Yearn"; } diff --git a/src/strategies/libs/StrategyLib.sol b/src/strategies/libs/StrategyLib.sol index b224c6f0..ddfd56b3 100644 --- a/src/strategies/libs/StrategyLib.sol +++ b/src/strategies/libs/StrategyLib.sol @@ -185,7 +185,8 @@ library StrategyLib { //slither-disable-next-line unused-return (uint earned,, uint[] memory assetPrices,) = priceReader.getAssetsPrice(assets, amounts); uint apr = computeApr(tvl, earned, duration); - uint aprCompound = computeApr(totalBefore, $.total - totalBefore, duration); + uint aprCompound = totalBefore != 0 ? computeApr(totalBefore, $.total - totalBefore, duration) : apr; + uint sharePrice = tvl * 1e18 / IERC20($.vault).totalSupply(); emit IStrategy.HardWork(apr, aprCompound, earned, tvl, duration, sharePrice, assetPrices); $.lastApr = apr; diff --git a/src/test/MockVaultUpgrade.sol b/src/test/MockVaultUpgrade.sol index 4d20af49..38650b1f 100644 --- a/src/test/MockVaultUpgrade.sol +++ b/src/test/MockVaultUpgrade.sol @@ -27,6 +27,8 @@ contract MockVaultUpgrade is Controllable, ERC20Upgradeable, IVault { function initialize(VaultInitializationData memory vaultInitializationData) public initializer {} + function hardWorkMintFeeCallback(address[] memory revenueAssets, uint[] memory revenueAmounts) external {} + function extra() external view returns (bytes32) {} function vaultType() external view returns (string memory) {} diff --git a/test/base/FullMockSetup.sol b/test/base/FullMockSetup.sol index b37a48b3..498e196f 100644 --- a/test/base/FullMockSetup.sol +++ b/test/base/FullMockSetup.sol @@ -146,4 +146,6 @@ abstract contract FullMockSetup is MockSetup { address(this) ); } + + function testFullMockSetup() public {} } diff --git a/test/base/MockSetup.sol b/test/base/MockSetup.sol index abdf1fbf..f3d0edf1 100644 --- a/test/base/MockSetup.sol +++ b/test/base/MockSetup.sol @@ -66,4 +66,6 @@ abstract contract MockSetup { builderPayPerVaultToken = tokenC; builderPayPerVaultPrice = 10e6; } + + function testMockSetup() public {} } diff --git a/test/base/UniversalTest.sol b/test/base/UniversalTest.sol index c362ca9c..0386ac69 100644 --- a/test/base/UniversalTest.sol +++ b/test/base/UniversalTest.sol @@ -77,6 +77,8 @@ abstract contract UniversalTest is Test, ChainSetup, Utils { function _preHardWork() internal virtual {} + function _preDeposit() internal virtual {} + function testNull() public {} function _testStrategies() internal { @@ -156,46 +158,62 @@ abstract contract UniversalTest is Test, ChainSetup, Utils { vaultInitNums[vaultInitAddressesLength * 2 - 1] = 50_000; } - if (!vars.farming) { - revert("UniversalTest: only farming strategies supported yet"); - } - - address[] memory initStrategyAddresses = new address[](0); - uint[] memory nums = new uint[](1); - nums[0] = strategies[i].farmId; + address[] memory initStrategyAddresses; + uint[] memory nums; int24[] memory ticks = new int24[](0); - // test bad params - initStrategyAddresses = new address[](1); - vm.expectRevert(IControllable.IncorrectInitParams.selector); - factory.deployVaultAndStrategy( - vars.types[k], - strategies[i].id, - vaultInitAddresses, - vaultInitNums, - initStrategyAddresses, - nums, - ticks - ); - initStrategyAddresses = new address[](0); + if (vars.farming) { + nums = new uint[](1); + nums[0] = strategies[i].farmId; + + // test bad params + initStrategyAddresses = new address[](1); + vm.expectRevert(IControllable.IncorrectInitParams.selector); + factory.deployVaultAndStrategy( + vars.types[k], + strategies[i].id, + vaultInitAddresses, + vaultInitNums, + initStrategyAddresses, + nums, + ticks + ); + initStrategyAddresses = new address[](0); + + IFactory.Farm memory f = factory.farm(nums[0]); + int24[] memory goodTicks = f.ticks; + f.ticks = new int24[](1000); + factory.updateFarm(nums[0], f); + vm.expectRevert(IFarmingStrategy.BadFarm.selector); + factory.deployVaultAndStrategy( + vars.types[k], + strategies[i].id, + vaultInitAddresses, + vaultInitNums, + initStrategyAddresses, + nums, + ticks + ); + f.ticks = goodTicks; + factory.updateFarm(nums[0], f); + /// + } else { + initStrategyAddresses = new address[](2); + vm.expectRevert(IControllable.IncorrectInitParams.selector); + factory.deployVaultAndStrategy( + vars.types[k], + strategies[i].id, + vaultInitAddresses, + vaultInitNums, + initStrategyAddresses, + nums, + ticks + ); - IFactory.Farm memory f = factory.farm(nums[0]); - int24[] memory goodTicks = f.ticks; - f.ticks = new int24[](1000); - factory.updateFarm(nums[0], f); - vm.expectRevert(IFarmingStrategy.BadFarm.selector); - factory.deployVaultAndStrategy( - vars.types[k], - strategies[i].id, - vaultInitAddresses, - vaultInitNums, - initStrategyAddresses, - nums, - ticks - ); - f.ticks = goodTicks; - factory.updateFarm(nums[0], f); - /// + initStrategyAddresses = new address[](1); + initStrategyAddresses[0] = strategies[i].underlying; + nums = new uint[](0); + } factory.deployVaultAndStrategy( vars.types[k], @@ -207,7 +225,9 @@ abstract contract UniversalTest is Test, ChainSetup, Utils { ticks ); - assertEq(IERC721(platform.vaultManager()).ownerOf(i), address(this)); + uint vaultTokenId = factory.deployedVaultsLength() - 1; + assertEq(IERC721(platform.vaultManager()).ownerOf(vaultTokenId), address(this)); + IVaultManager(platform.vaultManager()).setRevenueReceiver(vaultTokenId, address(1)); } vars.vault = factory.deployedVault(factory.deployedVaultsLength() - 1); @@ -263,6 +283,8 @@ abstract contract UniversalTest is Test, ChainSetup, Utils { /* DEPOSIT */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + _preDeposit(); + // get amounts for deposit uint[] memory depositAmounts = new uint[](assets.length); for (uint j; j < assets.length; ++j) { @@ -501,6 +523,7 @@ abstract contract UniversalTest is Test, ChainSetup, Utils { address underlying = strategy.underlying(); if (underlying != address(0)) { skip(7200); + bool wasReadyForHardWork = strategy.isReadyForHardWork(); address tempVault = vars.vault; deal(underlying, address(this), totalWas); assertEq(IERC20(underlying).balanceOf(address(this)), totalWas, "U1"); @@ -514,7 +537,7 @@ abstract contract UniversalTest is Test, ChainSetup, Utils { assertEq(valueOut, totalWas, "previewDepositAssets by underlying valueOut"); uint lastHw = strategy.lastHardWork(); IVault(tempVault).depositAssets(underlyingAssets, underlyingAmounts, 0, address(0)); - if (strategy.isHardWorkOnDepositAllowed()) { + if (strategy.isHardWorkOnDepositAllowed() && wasReadyForHardWork) { assertGt(strategy.lastHardWork(), lastHw, "HardWork not happened"); assertGt(strategy.total(), totalWas, "Strategy total not increased after HardWork"); } @@ -531,8 +554,8 @@ abstract contract UniversalTest is Test, ChainSetup, Utils { } else { assertLt( vaultBalance, - sharesOut, - "previewDepositAssets by underlying: sharesOut and real shares after deposit compare error" + sharesOut + sharesOut / 10000, + "previewDepositAssets by underlying: vault balance too big" ); assertGt( vaultBalance, @@ -547,7 +570,7 @@ abstract contract UniversalTest is Test, ChainSetup, Utils { IVault(tempVault).withdrawAssets(underlyingAssets, vaultBalance, minAmounts); vm.roll(block.number + 6); IVault(tempVault).withdrawAssets(underlyingAssets, vaultBalance, minAmounts); - assertGe(IERC20(underlying).balanceOf(address(this)), totalWas - 1); + assertGe(IERC20(underlying).balanceOf(address(this)), totalWas - 1, "U2"); assertLe(IERC20(underlying).balanceOf(address(this)), totalWas + 1); } else { { @@ -645,7 +668,20 @@ abstract contract UniversalTest is Test, ChainSetup, Utils { /* COVERAGE */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ strategy.isHardWorkOnDepositAllowed(); - IFarmingStrategy(address(strategy)).farmMechanics(); + if (vars.farming) { + IFarmingStrategy(address(strategy)).farmMechanics(); + } + strategy.autoCompoundingByUnderlyingProtocol(); + if (CommonLib.eq(strategy.strategyLogicId(), StrategyIdLib.YEARN)) { + vm.expectRevert(abi.encodeWithSelector(IControllable.IncorrectMsgSender.selector)); + IVault(vars.vault).hardWorkMintFeeCallback(new address[](0), new uint[](0)); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INIT VARIANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + (string[] memory variants,,,) = strategy.initVariants(address(platform)); + assertGt(variants.length, 0, "initVariants returns empty arrays"); } } } diff --git a/test/core/RMVault.Polygon.t.sol b/test/core/RMVault.Polygon.t.sol index bd2595c7..9dbf83bf 100644 --- a/test/core/RMVault.Polygon.t.sol +++ b/test/core/RMVault.Polygon.t.sol @@ -246,5 +246,8 @@ contract RMVaultTest is PolygonSetup { IERC20(platform.targetExchangeAsset()).approve(address(vault), a); // vm.expectRevert(IRVault.RewardIsTooBig.selector); vault.notifyTargetRewardAmount(1, a); + + vm.expectRevert(IVault.NotSupported.selector); + vault.hardWorkMintFeeCallback(new address[](0), new uint[](0)); } } diff --git a/test/core/Vault.t.sol b/test/core/Vault.t.sol index d9cadded..cfc62b74 100644 --- a/test/core/Vault.t.sol +++ b/test/core/Vault.t.sol @@ -310,6 +310,9 @@ contract VaultTest is Test, FullMockSetup { vaultInitNums: new uint[](0) }) ); + + vm.expectRevert(IVault.NotSupported.selector); + rVault.hardWorkMintFeeCallback(new address[](0), new uint[](0)); } function testChageNameSymbol() public { diff --git a/test/strategies/Y.Polygon.t.sol b/test/strategies/Y.Polygon.t.sol new file mode 100644 index 00000000..198979b5 --- /dev/null +++ b/test/strategies/Y.Polygon.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "../base/chains/PolygonSetup.sol"; +import "../base/UniversalTest.sol"; + +contract YearnStrategyTest is PolygonSetup, UniversalTest { + function testYearnStrategy() public universalTest { + _addStrategy(PolygonLib.YEARN_USDCe); + _addStrategy(PolygonLib.YEARN_DAI); + _addStrategy(PolygonLib.YEARN_USDT); + _addStrategy(PolygonLib.YEARN_WMATIC); + _addStrategy(PolygonLib.YEARN_WETH); + } + + function _addStrategy(address yaernV3Vault) internal { + strategies.push( + Strategy({id: StrategyIdLib.YEARN, pool: address(0), farmId: type(uint).max, underlying: yaernV3Vault}) + ); + } + + function _preDeposit() internal override { + if (IStrategy(currentStrategy).underlying() == PolygonLib.YEARN_USDT) { + // for some vault we need to cover deposit by underlying as first vault deposit + address vault = IStrategy(currentStrategy).vault(); + address[] memory underlyingAssets = new address[](1); + underlyingAssets[0] = PolygonLib.YEARN_USDT; + uint[] memory underlyingAmounts = new uint[](1); + underlyingAmounts[0] = 100e6; + _deal(underlyingAssets[0], address(this), 100e6); + IERC20(underlyingAssets[0]).approve(vault, type(uint).max); + IVault(vault).depositAssets(underlyingAssets, underlyingAmounts, 0, address(0)); + + // for some vault we setup ecosystem fee + vm.startPrank(address(0)); + platform.setEcosystemRevenueReceiver(address(10)); + platform.setFees(6_000, 30_000, 30_000, 40_000); + vm.stopPrank(); + } + } +}