diff --git a/script/AllocateFunds.s.sol b/script/AllocateFunds.s.sol new file mode 100644 index 00000000..8f62f0f7 --- /dev/null +++ b/script/AllocateFunds.s.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 +pragma solidity ^0.8.15; + +import {Script} from "forge-std/Script.sol"; +import {MultiStrategyVault, IERC4626, IERC20, Allocation} from "../src/vaults/MultiStrategyVault.sol"; + +contract AllocateFunds is Script { + Allocation[] internal allocations; + + function run() public { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + + vm.startBroadcast(deployerPrivateKey); + + // allocations.push(Allocation({index:0,amount:20e18})); + + // MultiStrategyVault(0xcede40B40F7AF69f5Aa6b12D75fd5eA9cE138b93).pullFunds(allocations); + + // allocations.push(Allocation({index:1,amount:10e18})); + // allocations.push(Allocation({index:2,amount:5e18})); + // allocations.push(Allocation({index:3,amount:5e18})); + + // MultiStrategyVault(0xcede40B40F7AF69f5Aa6b12D75fd5eA9cE138b93).pushFunds(allocations); + + vm.stopBroadcast(); + } +} diff --git a/script/deploy/DeployFeeRecipientProxy.s.sol b/script/deploy/DeployFeeRecipientProxy.s.sol new file mode 100644 index 00000000..9dbcf458 --- /dev/null +++ b/script/deploy/DeployFeeRecipientProxy.s.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 +pragma solidity ^0.8.15; + +import {Script} from "forge-std/Script.sol"; +import {FeeRecipientProxy} from "../../src/utils/FeeRecipientProxy.sol"; + +contract Deploy is Script { + address deployer; + + function run() public { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + deployer = vm.addr(deployerPrivateKey); + + vm.startBroadcast(deployerPrivateKey); + + new FeeRecipientProxy{salt: bytes32("FeeRecipientProxy")}(deployer); + + vm.stopBroadcast(); + } +} diff --git a/script/deploy/DeployMultiStrategyVault.s.sol b/script/deploy/DeployMultiStrategyVault.s.sol new file mode 100644 index 00000000..65ccd3a8 --- /dev/null +++ b/script/deploy/DeployMultiStrategyVault.s.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 +pragma solidity ^0.8.25; + +import {Script} from "forge-std/Script.sol"; +import {MultiStrategyVault, IERC4626, IERC20} from "../../src/vaults/MultiStrategyVault.sol"; + +contract DeployMultiStrategyVault is Script { + address deployer; + + IERC20 internal asset; + IERC4626[] internal strategies; + uint256 internal defaultDepositIndex; + uint256[] internal withdrawalQueue; + uint256 internal depositLimit; + address internal owner; + + function run() public { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + deployer = vm.addr(deployerPrivateKey); + + vm.startBroadcast(deployerPrivateKey); + + // @dev edit this values below + asset = IERC20(0x17FC002b466eEc40DaE837Fc4bE5c67993ddBd6F); + + strategies = [ + IERC4626(0x61dCd1Da725c0Cdb2C6e67a0058E317cA819Cf5f), + IERC4626(0x9168AC3a83A31bd85c93F4429a84c05db2CaEF08), + IERC4626(0x2D0483FefAbA4325c7521539a3DFaCf94A19C472), + IERC4626(0x6076ebDFE17555ed3E6869CF9C373Bbd9aD55d38) + ]; + + defaultDepositIndex = uint256(0); + + withdrawalQueue = [0, 1, 2, 3]; + + depositLimit = type(uint256).max; + + owner = deployer; + + // Actual deployment + MultiStrategyVault vault = new MultiStrategyVault(); + + vault.initialize(asset, strategies, defaultDepositIndex, withdrawalQueue, depositLimit, deployer); + + vm.stopBroadcast(); + } +} diff --git a/script/deploy/aave/AaveV3Depositor.s.sol.txt b/script/deploy/aave/AaveV3Depositor.s.sol.txt new file mode 100644 index 00000000..e4668e09 --- /dev/null +++ b/script/deploy/aave/AaveV3Depositor.s.sol.txt @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {Script} from "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; +import {AaveV3Depositor, IERC20} from "../../../src/strategies/aave/aaveV3/AaveV3Depositor.sol"; + +contract DeployStrategy is Script { + using stdJson for string; + + function run() public { + string memory json = vm.readFile( + string.concat( + vm.projectRoot(), + "./srcript/deploy/aave/AaveV3DepositorDeployConfig.json" + ) + ); + + AaveV3Depositor strategy = new AaveV3Depositor(); + + strategy.initialize( + json.readAddress(".baseInit.asset"), + json.readAddress(".baseInit.owner"), + json.readBool(".baseInit.autoHarvest"), + abi.encode(json.readAddress(".strategyInit.aaveDataProvider")) + ); + } +} diff --git a/script/deploy/aave/AaveV3DepositorDeployConfig.json b/script/deploy/aave/AaveV3DepositorDeployConfig.json new file mode 100644 index 00000000..d6d61209 --- /dev/null +++ b/script/deploy/aave/AaveV3DepositorDeployConfig.json @@ -0,0 +1,11 @@ +{ + "baseInit": { + "asset": "", + "owner": "", + "autoHarvest": false + }, + "strategyInit": { + "aaveDataProvider": "0x7B4EB56E7CD4b454BA8ff71E4518426369a138a3" + }, + "harvest": {} +} diff --git a/script/deploy/aura/AuraCompounder.s.sol.txt b/script/deploy/aura/AuraCompounder.s.sol.txt new file mode 100644 index 00000000..c2b8c607 --- /dev/null +++ b/script/deploy/aura/AuraCompounder.s.sol.txt @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {Script} from "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; +import {AuraCompounder, IERC20, BatchSwapStep, IAsset, AuraValues, HarvestValues, HarvestTradePath, TradePath} from "../../../src/strategies/aura/AuraCompounder.sol"; + +contract DeployStrategy is Script { + using stdJson for string; + + function run() public { + string memory json = vm.readFile( + string.concat( + vm.projectRoot(), + "./srcript/deploy/aura/AuraCompounderDeployConfig.json" + ) + ); + + // Read strategy init values + AuraValues memory auraValues_ = abi.decode( + json.parseRaw(".strategyInit"), + (AuraValues) + ); + + // Deploy Strategy + AuraCompounder strategy = new AuraCompounder(); + + strategy.initialize( + json.readAddress(".baseInit.asset"), + json.readAddress(".baseInit.owner"), + json.readBool(".baseInit.autoHarvest"), + abi.encode(auraValues_) + ); + + HarvestValues memory harvestValues_ = abi.decode( + json.parseRaw(".harvest.harvestValues"), + (HarvestValues) + ); + + HarvestTradePath[] memory tradePaths_ = abi.decode( + json.parseRaw(".harvest.tradePaths"), + (HarvestTradePath[]) + ); + + strategy.setHarvestValues(harvestValues_, tradePaths_); + } +} diff --git a/script/deploy/aura/AuraCompounderDeployConfig.json b/script/deploy/aura/AuraCompounderDeployConfig.json new file mode 100644 index 00000000..89123ab5 --- /dev/null +++ b/script/deploy/aura/AuraCompounderDeployConfig.json @@ -0,0 +1,68 @@ +{ + "baseInit": { + "asset": "", + "owner": "", + "autoHarvest": false + }, + "strategyInit": { + "auraBooster": "0xA57b8d98dAE62B26Ec3bcC4a365338157060B234", + "balPoolId": "0x596192bb6e41802428ac943d2f1476c1af25cc0e000000000000000000000659", + "balVault": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", + "pid": 189, + "underlyings": [ + "0x596192bB6e41802428Ac943D2f1476C1Af25CC0E", + "0xbf5495Efe5DB9ce00f80364C8B423567e58d2110", + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + ] + }, + "harvest": { + "harvestValues": { + "amountsInLen": 2, + "baseAsset": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "indexIn": 2, + "indexInUserData": 1 + }, + "tradePaths": [ + { + "assets": [ + "0xba100000625a3754423978a60c9317c58a424e3D", + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + ], + "limits": [ + 57896044618658097711785492504343953926634992332820282019728792003956564819967, + 57896044618658097711785492504343953926634992332820282019728792003956564819967 + ], + "minTradeAmount": 0, + "swaps": [ + { + "a-poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014", + "b-assetInIndex": 0, + "c-assetOutIndex": 1, + "d-amount": 0, + "e-userData": "" + } + ] + }, + { + "assets": [ + "0xC0c293ce456fF0ED870ADd98a0828Dd4d2903DBF", + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + ], + "limits": [ + 57896044618658097711785492504343953926634992332820282019728792003956564819967, + 57896044618658097711785492504343953926634992332820282019728792003956564819967 + ], + "minTradeAmount": 0, + "swaps": [ + { + "a-poolId": "0xcfca23ca9ca720b6e98e3eb9b6aa0ffc4a5c08b9000200000000000000000274", + "b-assetInIndex": 0, + "c-assetOutIndex": 1, + "d-amount": 0, + "e-userData": "" + } + ] + } + ] + } +} diff --git a/script/deploy/balancer/BalancerCompounder.s.sol.txt b/script/deploy/balancer/BalancerCompounder.s.sol.txt new file mode 100644 index 00000000..c97bce43 --- /dev/null +++ b/script/deploy/balancer/BalancerCompounder.s.sol.txt @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {Script} from "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; +import {BalancerCompounder, IERC20, BatchSwapStep, IAsset, BalancerValues, HarvestValues, HarvestTradePath, TradePath} from "../../../src/strategies/balancer/BalancerCompounder.sol"; + +contract DeployStrategy is Script { + using stdJson for string; + + function run() public { + string memory json = vm.readFile( + string.concat( + vm.projectRoot(), + "./srcript/deploy/balancer/BalancerCompounderDeployConfig.json" + ) + ); + + BalancerValues memory balancerValues_ = abi.decode( + json.parseRaw(string.concat(".strategyInit")), + (BalancerValues) + ); + + // Deploy Strategy + BalancerCompounder strategy = new BalancerCompounder(); + + strategy.initialize( + json.readAddress(".baseInit.asset"), + json.readAddress(".baseInit.owner"), + json.readBool(".baseInit.autoHarvest"), + abi.encode(balancerValues_) + ); + + HarvestValues memory harvestValues_ = abi.decode( + json.parseRaw(".harvest.harvestValues"), + (HarvestValues) + ); + + HarvestTradePath[] memory tradePaths_ = abi.decode( + json.parseRaw(".harvest.tradePaths"), + (HarvestTradePath[]) + ); + + // Set harvest values + strategy.setHarvestValues(harvestValues_, tradePaths_); + } +} diff --git a/script/deploy/balancer/BalancerCompounderDeployConfig.json b/script/deploy/balancer/BalancerCompounderDeployConfig.json new file mode 100644 index 00000000..5bcb39bf --- /dev/null +++ b/script/deploy/balancer/BalancerCompounderDeployConfig.json @@ -0,0 +1,48 @@ +{ + "baseInit": { + "asset": "", + "owner": "", + "autoHarvest": false + }, + "strategyInit": { + "balMinter": "0x239e55F427D44C3cc793f49bFB507ebe76638a2b", + "balPoolId": "0x596192bb6e41802428ac943d2f1476c1af25cc0e000000000000000000000659", + "balVault": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", + "gauge": "0xee01c0d9c0439c94D314a6ecAE0490989750746C", + "underlyings": [ + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "0xE7e2c68d3b13d905BBb636709cF4DfD21076b9D2", + "0xf951E335afb289353dc249e82926178EaC7DEd78" + ] + }, + "harvest": { + "harvestValues": { + "amountsInLen": 2, + "baseAsset": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "indexIn": 0, + "indexInUserData": 0 + }, + "tradePaths": [ + { + "assets": [ + "0xba100000625a3754423978a60c9317c58a424e3D", + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + ], + "limits": [ + 57896044618658097711785492504343953926634992332820282019728792003956564819967, + 57896044618658097711785492504343953926634992332820282019728792003956564819967 + ], + "minTradeAmount": 10000000000000000000, + "swaps": [ + { + "a-poolId": "0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014", + "b-assetInIndex": 0, + "c-assetOutIndex": 1, + "d-amount": 0, + "e-userData": "" + } + ] + } + ] + } +} diff --git a/script/deploy/beefy/BeefyDepositor.s.sol.txt b/script/deploy/beefy/BeefyDepositor.s.sol.txt new file mode 100644 index 00000000..49fe1dc6 --- /dev/null +++ b/script/deploy/beefy/BeefyDepositor.s.sol.txt @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {Script} from "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; + +import {BeefyDepositor, IERC20} from "../../../src/strategies/beefy/BeefyDepositor.sol"; + +contract BeefyDepositorTest is Script { + using stdJson for string; + + function run() public { + string memory json = vm.readFile( + string.concat( + vm.projectRoot(), + "./srcript/deploy/beefy/BeefyDepositorDeployConfig.json" + ) + ); + + BeefyDepositor strategy = new BeefyDepositor(); + + strategy.initialize( + json.readAddress(".baseInit.asset"), + json.readAddress(".baseInit.owner"), + json.readBool(".baseInit.autoHarvest"), + abi.encode(json.readAddress(".strategyInit.beefyVault")) + ); + } +} diff --git a/script/deploy/beefy/BeefyDepositorDeployConfig.json b/script/deploy/beefy/BeefyDepositorDeployConfig.json new file mode 100644 index 00000000..95776049 --- /dev/null +++ b/script/deploy/beefy/BeefyDepositorDeployConfig.json @@ -0,0 +1,10 @@ +{ + "baseInit": { + "asset": "", + "owner": "", + "autoHarvest": false + }, + "strategyInit": { + "beefyVault": "0xa7739fd3d12ac7F16D8329AF3Ee407e19De10D8D" + } +} diff --git a/script/deploy/compound/v2/CompoundV2Depositor.s.sol.txt b/script/deploy/compound/v2/CompoundV2Depositor.s.sol.txt new file mode 100644 index 00000000..85c8c883 --- /dev/null +++ b/script/deploy/compound/v2/CompoundV2Depositor.s.sol.txt @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {Script} from "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; + +import {CompoundV2Depositor, IERC20} from "../../../../src/strategies/compound/v2/CompoundV2Depositor.sol"; + +contract DeployStrategy is Script { + using stdJson for string; + + function run() public { + string memory json = vm.readFile( + string.concat( + vm.projectRoot(), + "./srcript/deploy/compound/v2/CompoundV2DepositorDeployConfig.json" + ) + ); + + CompoundV2Depositor strategy = new CompoundV2Depositor(); + + strategy.initialize( + json.readAddress(".baseInit.asset"), + json.readAddress(".baseInit.owner"), + json.readBool(".baseInit.autoHarvest"), + abi.encode( + json.readAddress(".strategyInit.cToken"), + json.readAddress(".strategyInit.comptroller") + ) + ); + } +} diff --git a/script/deploy/compound/v2/CompoundV2DepositorDeployConfig.json b/script/deploy/compound/v2/CompoundV2DepositorDeployConfig.json new file mode 100644 index 00000000..fca7b322 --- /dev/null +++ b/script/deploy/compound/v2/CompoundV2DepositorDeployConfig.json @@ -0,0 +1,11 @@ +{ + "baseInit": { + "asset": "", + "owner": "", + "autoHarvest": false + }, + "strategyInit": { + "cToken": "0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643", + "comptroller": "0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B" + } +} diff --git a/script/deploy/compound/v3/CompoundV3Depositor.s.sol.txt b/script/deploy/compound/v3/CompoundV3Depositor.s.sol.txt new file mode 100644 index 00000000..22826a8d --- /dev/null +++ b/script/deploy/compound/v3/CompoundV3Depositor.s.sol.txt @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {Script} from "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; + +import {CompoundV3Depositor, IERC20} from "../../../../src/strategies/compound/v3/CompoundV3Depositor.sol"; + +contract DeployStrategy is Script { + using stdJson for string; + + function run() public { + string memory json = vm.readFile( + string.concat( + vm.projectRoot(), + "./srcript/deploy/compound/v3/CompoundV3DepositorDeployConfig.json" + ) + ); + + CompoundV3Depositor strategy = new CompoundV3Depositor(); + + strategy.initialize( + json.readAddress(".baseInit.asset"), + json.readAddress(".baseInit.owner"), + json.readBool(".baseInit.autoHarvest"), + abi.encode(json.readAddress(".strategyInit.cToken")) + ); + } +} diff --git a/script/deploy/compound/v3/CompoundV3DepositorDeployConfig.json b/script/deploy/compound/v3/CompoundV3DepositorDeployConfig.json new file mode 100644 index 00000000..da4da3d0 --- /dev/null +++ b/script/deploy/compound/v3/CompoundV3DepositorDeployConfig.json @@ -0,0 +1,10 @@ +{ + "baseInit": { + "asset": "", + "owner": "", + "autoHarvest": false + }, + "strategyInit": { + "cToken": "0xc3d688B66703497DAA19211EEdff47f25384cdc3" + } +} diff --git a/script/deploy/convex/ConvexCompounder.s.sol.txt b/script/deploy/convex/ConvexCompounder.s.sol.txt new file mode 100644 index 00000000..1867b50f --- /dev/null +++ b/script/deploy/convex/ConvexCompounder.s.sol.txt @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {Script} from "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; +import {ConvexCompounder, IERC20, CurveSwap} from "../../../src/strategies/convex/ConvexCompounder.sol"; + +struct ConvexInit { + address convexBooster; + address curvePool; + uint256 pid; +} + +contract DeployStrategy is Script { + using stdJson for string; + + function run() public { + string memory json = vm.readFile( + string.concat( + vm.projectRoot(), + "./srcript/deploy/convex/ConvexCompounderDeployConfig.json" + ) + ); + + ConvexInit memory convexInit = abi.decode( + json.parseRaw(".strategyInit"), + (ConvexInit) + ); + + // Deploy Strategy + ConvexCompounder strategy = new ConvexCompounder(); + + strategy.initialize( + json.readAddress(".baseInit.asset"), + json.readAddress(".baseInit.owner"), + json.readBool(".baseInit.autoHarvest"), + abi.encode( + convexInit.convexBooster, + convexInit.curvePool, + convexInit.pid + ) + ); + + // Set Harvest values + _setHarvestValues(json, address(strategy)); + } + + function _setHarvestValues(string memory json_, address strategy) internal { + // Read harvest values + address curveRouter_ = abi.decode( + json_.parseRaw(".harvest.curveRouter"), + (address) + ); + + int128 indexIn_ = abi.decode( + json_.parseRaw(".harvest.indexIn"), + (int128) + ); + + uint256[] memory minTradeAmounts_ = abi.decode( + json_.parseRaw(".harvest.minTradeAmounts"), + (uint256[]) + ); + + address[] memory rewardTokens_ = abi.decode( + json_.parseRaw(".harvest.rewardTokens"), + (address[]) + ); + + //Construct CurveSwap structs + CurveSwap[] memory swaps_ = _getCurveSwaps(json_); + + // Set harvest values + ConvexCompounder(strategy).setHarvestValues( + curveRouter_, + rewardTokens_, + minTradeAmounts_, + swaps_, + indexIn_ + ); + } + + function _getCurveSwaps( + string memory json_ + ) internal returns (CurveSwap[] memory) { + //Construct CurveSwap structs + uint256 swapLen = json_.readUint(string.concat(".harvest.swaps.length")); + + CurveSwap[] memory swaps_ = new CurveSwap[](swapLen); + for (uint i; i < swapLen; i++) { + // Read route and convert dynamic into fixed size array + address[] memory route_ = json_.readAddressArray( + string.concat( + ".harvest.harvest.swaps.structs[", + vm.toString(i), + "].route" + ) + ); + address[11] memory route; + for (uint n; n < 11; n++) { + route[n] = route_[n]; + } + + // Read swapParams and convert dynamic into fixed size array + uint256[5][5] memory swapParams; + for (uint n = 0; n < 5; n++) { + uint256[] memory swapParams_ = json_.readUintArray( + string.concat( + "harvest.swaps.structs[", + vm.toString(i), + "].swapParams[", + vm.toString(n), + "]" + ) + ); + for (uint y; y < 5; y++) { + swapParams[n][y] = swapParams_[y]; + } + } + + // Read pools and convert dynamic into fixed size array + address[] memory pools_ = json_.readAddressArray( + string.concat( + "harvest.swaps.structs[", + vm.toString(i), + "].pools" + ) + ); + address[5] memory pools; + for (uint n = 0; n < 5; n++) { + pools[n] = pools_[n]; + } + + // Construct the struct + swaps_[i] = CurveSwap({ + route: route, + swapParams: swapParams, + pools: pools + }); + } + return swaps_; + } +} diff --git a/script/deploy/convex/ConvexCompounderDeployConfig.json b/script/deploy/convex/ConvexCompounderDeployConfig.json new file mode 100644 index 00000000..ff09bf92 --- /dev/null +++ b/script/deploy/convex/ConvexCompounderDeployConfig.json @@ -0,0 +1,86 @@ +{ + "baseInit": { + "asset": "", + "owner": "", + "autoHarvest": false + }, + "strategyInit": { + "convexBooster": "0xF403C135812408BFbE8713b5A23a04b3D48AAE31", + "curvePool": "0x625E92624Bc2D88619ACCc1788365A69767f6200", + "pid": 289 + }, + "harvest": { + "curveRouter": "0xF0d4c12A5768D806021F80a262B4d39d26C58b8D", + "indexIn": 1, + "minTradeAmounts": [1000000000000000000, 1000000000000000000], + "rewardTokens": [ + "0xD533a949740bb3306d119CC777fa900bA034cd52", + "0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B" + ], + "swaps": { + "length": 2, + "structs": [ + { + "route": [ + "0xD533a949740bb3306d119CC777fa900bA034cd52", + "0x4eBdF703948ddCEA3B11f675B4D1Fba9d2414A14", + "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000" + ], + "swapParams": [ + [2, 0, 2, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0] + ], + "pools": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000" + ] + }, + { + "route": [ + "0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B", + "0xB576491F1E6e5E62f1d8F26062Ee822B40B0E0d4", + "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + "0x4eBdF703948ddCEA3B11f675B4D1Fba9d2414A14", + "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000" + ], + "swapParams": [ + [1, 0, 1, 2, 2], + [1, 0, 1, 3, 3], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0] + ], + "pools": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000" + ] + } + ] + } + } +} diff --git a/script/deploy/curve/CurveGaugeCompounder.s.sol.txt b/script/deploy/curve/CurveGaugeCompounder.s.sol.txt new file mode 100644 index 00000000..9fc51094 --- /dev/null +++ b/script/deploy/curve/CurveGaugeCompounder.s.sol.txt @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {Script} from "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; +import {CurveGaugeCompounder, IERC20, CurveSwap} from "../../../src/strategies/curve/gauge/mainnet/CurveGaugeCompounder.sol"; + +struct CurveGaugeInit { + address gauge; + address minter; + address pool; +} + +contract DeployStrategy is Script { + using stdJson for string; + + function run() public { + string memory json = vm.readFile( + string.concat( + vm.projectRoot(), + "./srcript/deploy/curve/CurveGaugeCompounderDeployConfig.json" + ) + ); + + CurveGaugeInit memory curveInit = abi.decode( + json.parseRaw(".strategyInit"), + (CurveGaugeInit) + ); + + // Deploy Strategy + CurveGaugeCompounder strategy = new CurveGaugeCompounder(); + + strategy.initialize( + json.readAddress(".baseInit.asset"), + json.readAddress(".baseInit.owner"), + json.readBool(".baseInit.autoHarvest"), + abi.encode(curveInit.gauge, curveInit.pool, curveInit.minter) + ); + + address curveRouter_ = abi.decode( + json.parseRaw(".harvest.curveRouter"), + (address) + ); + + int128 indexIn_ = abi.decode( + json.parseRaw(".harvest.indexIn"), + (int128) + ); + + uint256[] memory minTradeAmounts_ = abi.decode( + json.parseRaw(".harvest.minTradeAmounts"), + (uint256[]) + ); + + address[] memory rewardTokens_ = abi.decode( + json.parseRaw(".harvest.rewardTokens"), + (address[]) + ); + + CurveSwap[] memory swaps_ = abi.decode( + json.parseRaw(".harvest.swaps"), + (CurveSwap[]) + ); + + // Set harvest values + strategy.setHarvestValues( + curveRouter_, + rewardTokens_, + minTradeAmounts_, + swaps_, + indexIn_ + ); + } +} diff --git a/script/deploy/curve/CurveGaugeCompounderDeployConfig.json b/script/deploy/curve/CurveGaugeCompounderDeployConfig.json new file mode 100644 index 00000000..d6d61209 --- /dev/null +++ b/script/deploy/curve/CurveGaugeCompounderDeployConfig.json @@ -0,0 +1,11 @@ +{ + "baseInit": { + "asset": "", + "owner": "", + "autoHarvest": false + }, + "strategyInit": { + "aaveDataProvider": "0x7B4EB56E7CD4b454BA8ff71E4518426369a138a3" + }, + "harvest": {} +} diff --git a/script/deploy/curve/CurveGaugeSingleAssetCompounder.s.sol.txt b/script/deploy/curve/CurveGaugeSingleAssetCompounder.s.sol.txt new file mode 100644 index 00000000..e40bfe14 --- /dev/null +++ b/script/deploy/curve/CurveGaugeSingleAssetCompounder.s.sol.txt @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {Script} from "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; + +import {CurveGaugeSingleAssetCompounder, IERC20, CurveSwap} from "../../../src/strategies/curve/gauge/other/CurveGaugeSingleAssetCompounder.sol"; + +struct CurveGaugeInit { + address gauge; + int128 indexIn; + address lpToken; +} + +contract DeployStrategy is Script { + using stdJson for string; + + function run() public { + string memory json = vm.readFile( + string.concat( + vm.projectRoot(), + "./srcript/deploy/curve/CurveGaugeSingleAssetCompounderDeployConfig.json" + ) + ); + + CurveGaugeInit memory curveInit = abi.decode( + json.parseRaw(".strategyInit"), + (CurveGaugeInit) + ); + + // Deploy Strategy + CurveGaugeSingleAssetCompounder strategy = new CurveGaugeSingleAssetCompounder(); + + strategy.initialize( + json.readAddress(".baseInit.asset"), + json.readAddress(".baseInit.owner"), + json.readBool(".baseInit.autoHarvest"), + abi.encode(curveInit.lpToken, curveInit.gauge, curveInit.indexIn) + ); + + address curveRouter_ = abi.decode( + json.parseRaw(".harvest.curveRouter"), + (address) + ); + + uint256 discountBps_ = abi.decode( + json.parseRaw(".harvest.discountBps"), + (uint256) + ); + + uint256[] memory minTradeAmounts_ = abi.decode( + json.parseRaw(".harvest.minTradeAmounts"), + (uint256[]) + ); + + address[] memory rewardTokens_ = abi.decode( + json.parseRaw(".harvest.rewardTokens"), + (address[]) + ); + + CurveSwap[] memory swaps_ = abi.decode( + json.parseRaw(".harvest.swaps"), + (CurveSwap[]) + ); + + // Set harvest values + strategy.setHarvestValues( + curveRouter_, + rewardTokens_, + minTradeAmounts_, + swaps_, + discountBps_ + ); + } +} diff --git a/script/deploy/curve/CurveGaugeSingleAssetCompounderDeployConfig.json b/script/deploy/curve/CurveGaugeSingleAssetCompounderDeployConfig.json new file mode 100644 index 00000000..d6d61209 --- /dev/null +++ b/script/deploy/curve/CurveGaugeSingleAssetCompounderDeployConfig.json @@ -0,0 +1,11 @@ +{ + "baseInit": { + "asset": "", + "owner": "", + "autoHarvest": false + }, + "strategyInit": { + "aaveDataProvider": "0x7B4EB56E7CD4b454BA8ff71E4518426369a138a3" + }, + "harvest": {} +} diff --git a/script/deploy/ion/IonDepositor.s.sol b/script/deploy/ion/IonDepositor.s.sol new file mode 100644 index 00000000..69ae358c --- /dev/null +++ b/script/deploy/ion/IonDepositor.s.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 + +pragma solidity ^0.8.15; + +import {Script} from "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; + +import {IonDepositor, SafeERC20, IERC20} from "../../../src/strategies/ion/IonDepositor.sol"; + +contract DeployStrategy is Script { + using stdJson for string; + + function run() public { + string memory json = + vm.readFile(string.concat(vm.projectRoot(), "./srcript/deploy/ion/IonDepositorDeployConfig.json")); + + // Deploy strategy + IonDepositor strategy = new IonDepositor(); + + strategy.initialize( + json.readAddress(".baseInit.asset"), + json.readAddress(".baseInit.owner"), + json.readBool(".baseInit.autoHarvest"), + abi.encode(json.readAddress(".strategyInit.ionPool")) + ); + } +} diff --git a/script/deploy/ion/IonDepositorDeployConfig.json b/script/deploy/ion/IonDepositorDeployConfig.json new file mode 100644 index 00000000..1a706d04 --- /dev/null +++ b/script/deploy/ion/IonDepositorDeployConfig.json @@ -0,0 +1,10 @@ +{ + "baseInit": { + "asset": "", + "owner": "", + "autoHarvest": false + }, + "strategyInit": { + "ionPool": "0x0000000000eaEbd95dAfcA37A39fd09745739b78" + } +} diff --git a/script/deploy/lido/WstETHLooper.s.sol b/script/deploy/lido/WstETHLooper.s.sol new file mode 100644 index 00000000..5fc17849 --- /dev/null +++ b/script/deploy/lido/WstETHLooper.s.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {Script} from "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; + +import {WstETHLooper, LooperInitValues, IERC20} from "../../../src/strategies/lido/WstETHLooper.sol"; + +contract DeployStrategy is Script { + using stdJson for string; + + IERC20 wstETH; + IERC20 awstETH; + IERC20 vdWETH; + + function run() public { + string memory json = + vm.readFile(string.concat(vm.projectRoot(), "./srcript/deploy/lido/WstETHLooperDeployConfig.json")); + + LooperInitValues memory looperValues = abi.decode(json.parseRaw(".strategyInit"), (LooperInitValues)); + + // Deploy Strategy + WstETHLooper strategy = new WstETHLooper(); + + address asset = json.readAddress(".baseInit.asset"); + + strategy.initialize( + asset, + json.readAddress(".baseInit.owner"), + json.readBool(".baseInit.autoHarvest"), + abi.encode( + looperValues.aaveDataProvider, + looperValues.curvePool, + looperValues.maxLTV, + looperValues.poolAddressesProvider, + looperValues.slippage, + looperValues.targetLTV + ) + ); + + IERC20(asset).approve(address(strategy), 1); + strategy.setUserUseReserveAsCollateral(1); + } +} diff --git a/script/deploy/lido/WstETHLooperDeployConfig.json b/script/deploy/lido/WstETHLooperDeployConfig.json new file mode 100644 index 00000000..93e1b88b --- /dev/null +++ b/script/deploy/lido/WstETHLooperDeployConfig.json @@ -0,0 +1,15 @@ +{ + "baseInit": { + "asset": "", + "owner": "", + "autoHarvest": false + }, + "strategyInit": { + "aaveDataProvider": "0x7B4EB56E7CD4b454BA8ff71E4518426369a138a3", + "curvePool": "0xDC24316b9AE028F1497c275EB9192a3Ea0f67022", + "maxLTV": 850000000000000000, + "poolAddressesProvider": "0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e", + "slippage": 10000000000000000, + "targetLTV": 800000000000000000 + } +} diff --git a/src/interfaces/external/balancer/IBalancerVault.sol b/src/interfaces/external/balancer/IBalancer.sol similarity index 53% rename from src/interfaces/external/balancer/IBalancerVault.sol rename to src/interfaces/external/balancer/IBalancer.sol index c20bfdae..2c1620d4 100644 --- a/src/interfaces/external/balancer/IBalancerVault.sol +++ b/src/interfaces/external/balancer/IBalancer.sol @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + pragma solidity ^0.8.25; import {IERC20} from "openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; @@ -9,6 +12,15 @@ enum SwapKind { interface IAsset {} +struct SingleSwap { + bytes32 poolId; + SwapKind kind; + address assetIn; + address assetOut; + uint256 amount; + bytes userData; +} + struct BatchSwapStep { bytes32 poolId; uint256 assetInIndex; @@ -52,3 +64,37 @@ interface IBalancerVault { external payable; } + +interface IBalancerRouter { + function swap(SingleSwap memory singleSwap, FundManagement memory funds, uint256 limit, uint256 deadline) + external + returns (uint256 amountCalculated); +} + +interface IGauge { + function lp_token() external view returns (address); + + function bal_token() external view returns (address); + + function is_killed() external view returns (bool); + + function totalSupply() external view returns (uint256); + + function balanceOf(address user) external view returns (uint256); + + function withdraw(uint256 amount, bool _claim_rewards) external; + + function deposit(uint256 amount) external; +} + +interface IMinter { + function mint(address gauge) external; + + function getBalancerToken() external view returns (address); + + function getGaugeController() external view returns (address); +} + +interface IController { + function gauge_exists(address _gauge) external view returns (bool); +} diff --git a/src/peripheral/BalancerTradeLibrary.sol b/src/peripheral/BalancerTradeLibrary.sol index 6a0e99b2..18fef0bc 100644 --- a/src/peripheral/BalancerTradeLibrary.sol +++ b/src/peripheral/BalancerTradeLibrary.sol @@ -10,7 +10,7 @@ import { BatchSwapStep, FundManagement, JoinPoolRequest -} from "../interfaces/external/balancer/IBalancerVault.sol"; +} from "../interfaces/external/balancer/IBalancer.sol"; library BalancerTradeLibrary { function trade( diff --git a/src/peripheral/BaseBalancerCompounder.sol b/src/peripheral/BaseBalancerCompounder.sol index ca8d6bd8..f1a22a4d 100644 --- a/src/peripheral/BaseBalancerCompounder.sol +++ b/src/peripheral/BaseBalancerCompounder.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.25; import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; import {BalancerTradeLibrary, IBalancerVault, IAsset, BatchSwapStep} from "./BalancerTradeLibrary.sol"; struct TradePath { @@ -13,10 +14,11 @@ struct TradePath { } abstract contract BaseBalancerCompounder { + using SafeERC20 for IERC20; IBalancerVault public balancerVault; + address[] public _balancerSellTokens; TradePath[] public tradePaths; - address[] public _rewardTokens; function sellRewardsViaBalancer() internal { // Caching @@ -43,15 +45,15 @@ abstract contract BaseBalancerCompounder { function setBalancerTradeValues(address newBalancerVault, TradePath[] memory newTradePaths) internal { // Remove old rewardToken allowance - uint256 rewardTokenLen = _rewardTokens.length; + uint256 rewardTokenLen = _balancerSellTokens.length; if (rewardTokenLen > 0) { // caching address oldBalancerVault = address(balancerVault); - address[] memory oldRewardTokens = _rewardTokens; + address[] memory oldRewardTokens = _balancerSellTokens; // void approvals for (uint256 i = 0; i < rewardTokenLen;) { - IERC20(oldRewardTokens[i]).approve(oldBalancerVault, 0); + IERC20(oldRewardTokens[i]).forceApprove(oldBalancerVault, 0); unchecked { ++i; @@ -60,7 +62,7 @@ abstract contract BaseBalancerCompounder { } // delete old state - delete _rewardTokens; + delete _balancerSellTokens; delete tradePaths; // Add new allowance + state @@ -69,9 +71,9 @@ abstract contract BaseBalancerCompounder { for (uint256 i; i < rewardTokenLen;) { newRewardToken = address(newTradePaths[i].assets[0]); - IERC20(newRewardToken).approve(newBalancerVault, type(uint256).max); + IERC20(newRewardToken).forceApprove(newBalancerVault, type(uint256).max); - _rewardTokens.push(newRewardToken); + _balancerSellTokens.push(newRewardToken); tradePaths.push(newTradePaths[i]); unchecked { diff --git a/src/peripheral/BaseBalancerLpCompounder.sol b/src/peripheral/BaseBalancerLpCompounder.sol index 2e2eeddd..981fba24 100644 --- a/src/peripheral/BaseBalancerLpCompounder.sol +++ b/src/peripheral/BaseBalancerLpCompounder.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.25; import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; import {BaseBalancerCompounder, BalancerTradeLibrary, TradePath} from "./BaseBalancerCompounder.sol"; struct HarvestValues { @@ -16,6 +17,8 @@ struct HarvestValues { } abstract contract BaseBalancerLpCompounder is BaseBalancerCompounder { + using SafeERC20 for IERC20; + HarvestValues public harvestValues; error CompoundFailed(); @@ -52,10 +55,10 @@ abstract contract BaseBalancerLpCompounder is BaseBalancerCompounder { // Reset old base asset if (harvestValues.depositAsset != address(0)) { - IERC20(harvestValues.depositAsset).approve(address(balancerVault), 0); + IERC20(harvestValues.depositAsset).forceApprove(address(balancerVault), 0); } // approve and set new base asset - IERC20(harvestValues_.depositAsset).approve(newBalancerVault, type(uint256).max); + IERC20(harvestValues_.depositAsset).forceApprove(newBalancerVault, type(uint256).max); harvestValues = harvestValues_; } diff --git a/src/peripheral/BaseCurveCompounder.sol b/src/peripheral/BaseCurveCompounder.sol index 5b4c2d8c..001bed70 100644 --- a/src/peripheral/BaseCurveCompounder.sol +++ b/src/peripheral/BaseCurveCompounder.sol @@ -4,19 +4,22 @@ pragma solidity ^0.8.25; import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; import {ICurveRouter, CurveSwap, ICurveLp} from "../strategies/curve/ICurve.sol"; import {CurveTradeLibrary} from "./CurveTradeLibrary.sol"; abstract contract BaseCurveCompounder { + using SafeERC20 for IERC20; + ICurveRouter public curveRouter; - address[] public _rewardTokens; - CurveSwap[] internal swaps; // Must be ordered like `_rewardTokens` + address[] public _curveSellTokens; + CurveSwap[] internal curveSwaps; // Must be ordered like `_sellTokens` function sellRewardsViaCurve() internal { // caching ICurveRouter router = curveRouter; - CurveSwap[] memory sellSwaps = swaps; + CurveSwap[] memory sellSwaps = curveSwaps; uint256 amount; uint256 rewLen = sellSwaps.length; @@ -35,15 +38,15 @@ abstract contract BaseCurveCompounder { function setCurveTradeValues(address newRouter, CurveSwap[] memory newSwaps) internal { // Remove old rewardToken allowance - uint256 rewardTokenLen = _rewardTokens.length; - if (rewardTokenLen > 0) { + uint256 sellTokensLen = _curveSellTokens.length; + if (sellTokensLen > 0) { // caching address oldRouter = address(curveRouter); - address[] memory oldRewardTokens = _rewardTokens; + address[] memory oldSellTokens = _curveSellTokens; // void approvals - for (uint256 i = 0; i < rewardTokenLen;) { - IERC20(oldRewardTokens[i]).approve(oldRouter, 0); + for (uint256 i = 0; i < sellTokensLen;) { + IERC20(oldSellTokens[i]).forceApprove(oldRouter, 0); unchecked { ++i; @@ -52,19 +55,19 @@ abstract contract BaseCurveCompounder { } // delete old state - delete _rewardTokens; - delete swaps; + delete _curveSellTokens; + delete curveSwaps; // Add new allowance + state address newRewardToken; - rewardTokenLen = newSwaps.length; - for (uint256 i = 0; i < rewardTokenLen;) { + sellTokensLen = newSwaps.length; + for (uint256 i = 0; i < sellTokensLen;) { newRewardToken = newSwaps[i].route[0]; - IERC20(newRewardToken).approve(newRouter, type(uint256).max); + IERC20(newRewardToken).forceApprove(newRouter, type(uint256).max); - _rewardTokens.push(newRewardToken); - swaps.push(newSwaps[i]); + _curveSellTokens.push(newRewardToken); + curveSwaps.push(newSwaps[i]); unchecked { ++i; diff --git a/src/peripheral/BaseCurveLpCompounder.sol b/src/peripheral/BaseCurveLpCompounder.sol index 3c3a77e3..0b2b857a 100644 --- a/src/peripheral/BaseCurveLpCompounder.sol +++ b/src/peripheral/BaseCurveLpCompounder.sol @@ -4,9 +4,12 @@ pragma solidity ^0.8.25; import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; import {BaseCurveCompounder, CurveTradeLibrary, CurveSwap, ICurveLp} from "./BaseCurveCompounder.sol"; abstract contract BaseCurveLpCompounder is BaseCurveCompounder { + using SafeERC20 for IERC20; + address public depositAsset; int128 public indexIn; @@ -36,9 +39,9 @@ abstract contract BaseCurveLpCompounder is BaseCurveCompounder { address depositAsset_ = ICurveLp(poolAddress).coins(uint256(uint128(indexIn_))); if (depositAsset != address(0)) { - IERC20(depositAsset).approve(poolAddress, 0); + IERC20(depositAsset).forceApprove(poolAddress, 0); } - IERC20(depositAsset_).approve(poolAddress, type(uint256).max); + IERC20(depositAsset_).forceApprove(poolAddress, type(uint256).max); depositAsset = depositAsset_; indexIn = indexIn_; diff --git a/src/peripheral/BaseUniV2Compounder.sol b/src/peripheral/BaseUniV2Compounder.sol new file mode 100644 index 00000000..3dd3379a --- /dev/null +++ b/src/peripheral/BaseUniV2Compounder.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {UniswapV2TradeLibrary, IUniswapRouterV2} from "./UniswapV2TradeLibrary.sol"; +import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "openzeppelin-contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; +import {Math} from "openzeppelin-contracts/utils/math/Math.sol"; + +struct SwapStep { + address[] path; +} + +abstract contract BaseUniV2Compounder { + using SafeERC20 for IERC20; + using Math for uint256; + + IUniswapRouterV2 public uniswapRouter; + + address[] public sellTokens; + SwapStep[] internal sellSwaps; // for each sellToken there are two paths. + + // sell half the rewards for a pool tokenA and half for the tokenB + function sellRewardsForBaseTokensViaUniswapV2() internal { + // caching + IUniswapRouterV2 router = uniswapRouter; + SwapStep memory swap; + + uint256 rewLen = sellTokens.length; + for (uint256 i = 0; i < rewLen;) { + uint256 totAmount = IERC20(sellTokens[i]).balanceOf(address(this)); + + // sell half for tokenA + uint256 decimals = IERC20Metadata(sellTokens[i]).decimals(); + uint256 amount = totAmount.mulDiv( + 10**decimals, + 20**decimals, + Math.Rounding.Floor + ); + + swap = sellSwaps[2 * i]; + + if (amount > 0) { + UniswapV2TradeLibrary.trade(router, swap.path, address(this), block.timestamp, amount, 0); + } + + // sell the rest for tokenB + swap = sellSwaps[2 * i + 1]; + amount = totAmount - amount; + if (amount > 0) { + UniswapV2TradeLibrary.trade(router, swap.path, address(this), block.timestamp, amount, 0); + } + + unchecked { + ++i; + } + } + } + + // sell all rewards for a single token + function sellRewardsViaUniswapV2() internal { + IUniswapRouterV2 router = uniswapRouter; + SwapStep memory swap; + + uint256 rewLen = sellTokens.length; + for (uint256 i = 0; i < rewLen;) { + uint256 totAmount = IERC20(sellTokens[i]).balanceOf(address(this)); + swap = sellSwaps[i]; + + if (totAmount > 0) { + UniswapV2TradeLibrary.trade(router, swap.path, address(this), block.timestamp, totAmount, 0); + } + + unchecked { + ++i; + } + } + } + + function setUniswapTradeValues( + address newRouter, + address[] memory rewTokens, + SwapStep[] memory newSwaps + ) internal { + // Remove old rewardToken allowance + uint256 sellTokensLen = sellTokens.length; + if (sellTokensLen > 0) { + // caching + address oldRouter = address(uniswapRouter); + address[] memory oldSellTokens = sellTokens; + + // void approvals + for (uint256 i = 0; i < sellTokensLen;) { + IERC20(oldSellTokens[i]).forceApprove(oldRouter, 0); + + unchecked { + ++i; + } + } + } + + // delete old state + delete sellTokens; + delete sellSwaps; + + // Add new allowance + state + address newRewardToken; + sellTokensLen = rewTokens.length; + for (uint256 i = 0; i < sellTokensLen;) { + newRewardToken = rewTokens[i]; + + IERC20(newRewardToken).forceApprove(newRouter, type(uint256).max); + + sellTokens.push(newRewardToken); + + unchecked { + ++i; + } + } + + for (uint256 i=0; i 0 && amountB > 0) { + UniswapV2TradeLibrary.addLiquidity( + uniswapRouter, + depositAssets[0], + depositAssets[1], + amountA, + amountB, + 0, + 0, + to, + deadline + ); + + uint256 amountLP = IERC20(vaultAsset).balanceOf(address(this)); + uint256 minOut = abi.decode(data, (uint256)); + if (amountLP < minOut) revert CompoundFailed(); + } + } + + function setUniswapLpCompounderValues( + address newRouter, + address[2] memory newDepositAssets, + address[] memory rewardTokens, + SwapStep[] memory newSwaps + ) internal { + setUniswapTradeValues(newRouter, rewardTokens, newSwaps); + + address tokenA = newDepositAssets[0]; + address tokenB = newDepositAssets[1]; + + address oldTokenA = depositAssets[0]; + address oldTokenB = depositAssets[1]; + + if (oldTokenA != address(0)) { + IERC20(oldTokenA).forceApprove(address(uniswapRouter), 0); + } + if (oldTokenB != address(0)) { + IERC20(oldTokenB).forceApprove(address(uniswapRouter), 0); + } + + IERC20(tokenA).forceApprove(address(uniswapRouter), type(uint256).max); + IERC20(tokenB).forceApprove(address(uniswapRouter), type(uint256).max); + + depositAssets = newDepositAssets; + } +} \ No newline at end of file diff --git a/src/peripheral/UniswapV2TradeLibrary.sol b/src/peripheral/UniswapV2TradeLibrary.sol new file mode 100644 index 00000000..827ad118 --- /dev/null +++ b/src/peripheral/UniswapV2TradeLibrary.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {IUniswapRouterV2} from "../interfaces/external/uni/IUniswapRouterV2.sol"; + +library UniswapV2TradeLibrary { + function trade( + IUniswapRouterV2 router, + address[] memory path, + address receiver, + uint256 deadline, + uint256 amount, + uint256 minOut + ) internal returns (uint256[] memory amounts) { + amounts = router.swapExactTokensForTokens(amount, minOut, path, receiver, deadline); + } + + function addLiquidity( + IUniswapRouterV2 router, + address tokenA, + address tokenB, + uint256 amountA, + uint256 amountB, + uint256 amountAMin, + uint256 amountBMin, + address receiver, + uint256 deadline + ) internal returns (uint256 amountOutA, uint256 amountOutB, uint256 lpAmountOut) { + (amountOutA, amountOutB, lpAmountOut) = router.addLiquidity( + tokenA, + tokenB, + amountA, + amountB, + amountAMin, + amountBMin, + receiver, + deadline + ); + } +} \ No newline at end of file diff --git a/src/strategies/aave/aaveV3/AaveV3Depositor.sol b/src/strategies/aave/aaveV3/AaveV3Depositor.sol new file mode 100644 index 00000000..94e096f3 --- /dev/null +++ b/src/strategies/aave/aaveV3/AaveV3Depositor.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {BaseStrategy, IERC20, IERC20Metadata, SafeERC20, ERC20, Math} from "../../BaseStrategy.sol"; +import {ILendingPool, IAaveIncentives, IAToken, IProtocolDataProvider} from "./IAaveV3.sol"; +import {DataTypes} from "./lib.sol"; + +/** + * @title AaveV3 Adapter + * @author RedVeil + * @notice ERC4626 wrapper for AaveV3 Vaults. + */ +contract AaveV3Depositor is BaseStrategy { + using SafeERC20 for IERC20; + using Math for uint256; + + string internal _name; + string internal _symbol; + + /// @notice The Aave aToken contract + IAToken public aToken; + + /// @notice The Aave liquidity mining contract + IAaveIncentives public aaveIncentives; + + /// @notice Check to see if Aave liquidity mining is active + bool public isActiveIncentives; + + /// @notice The Aave LendingPool contract + ILendingPool public lendingPool; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + error DifferentAssets(address asset, address underlying); + + /** + * @notice Initialize a new Strategy. + * @param asset_ The underlying asset used for deposit/withdraw and accounting + * @param owner_ Owner of the contract. Controls management functions. + * @param autoDeposit_ Controls if `protocolDeposit` gets called on deposit + * @param strategyInitData_ Encoded data for this specific strategy + */ + function initialize(address asset_, address owner_, bool autoDeposit_, bytes memory strategyInitData_) + external + initializer + { + address _aaveDataProvider = abi.decode(strategyInitData_, (address)); + + (address _aToken,,) = IProtocolDataProvider(_aaveDataProvider).getReserveTokensAddresses(asset_); + + aToken = IAToken(_aToken); + if (aToken.UNDERLYING_ASSET_ADDRESS() != asset_) { + revert DifferentAssets(aToken.UNDERLYING_ASSET_ADDRESS(), asset_); + } + + lendingPool = ILendingPool(aToken.POOL()); + aaveIncentives = IAaveIncentives(aToken.getIncentivesController()); + + __BaseStrategy_init(asset_, owner_, autoDeposit_); + + IERC20(asset_).approve(address(lendingPool), type(uint256).max); + + _name = string.concat("VaultCraft AaveV3 ", IERC20Metadata(asset()).name(), " Adapter"); + _symbol = string.concat("vcAv3-", IERC20Metadata(asset()).symbol()); + } + + function name() public view override(IERC20Metadata, ERC20) returns (string memory) { + return _name; + } + + function symbol() public view override(IERC20Metadata, ERC20) returns (string memory) { + return _symbol; + } + + /*////////////////////////////////////////////////////////////// + ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ + + function _totalAssets() internal view override returns (uint256) { + return aToken.balanceOf(address(this)); + } + + /// @notice The token rewarded if the aave liquidity mining is active + function rewardTokens() external view override returns (address[] memory) { + return aaveIncentives.getRewardsByAsset(asset()); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL HOOKS LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Deposit into aave lending pool + function _protocolDeposit(uint256 assets, uint256, bytes memory) internal override { + lendingPool.supply(asset(), assets, address(this), 0); + } + + /// @notice Withdraw from lending pool + function _protocolWithdraw(uint256 assets, uint256, bytes memory) internal override { + lendingPool.withdraw(asset(), assets, address(this)); + } + + /*////////////////////////////////////////////////////////////// + STRATEGY LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Claim additional rewards given that it's active. + function claim() internal override returns (bool success) { + if (address(aaveIncentives) == address(0)) return false; + + address[] memory _assets = new address[](1); + _assets[0] = address(aToken); + + try aaveIncentives.claimAllRewardsOnBehalf(_assets, address(this), address(this)) { + success = true; + } catch {} + } +} diff --git a/src/strategies/aave/aaveV3/IAaveV3.sol b/src/strategies/aave/aaveV3/IAaveV3.sol new file mode 100644 index 00000000..e4051caa --- /dev/null +++ b/src/strategies/aave/aaveV3/IAaveV3.sol @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.20; + +import {IERC20} from "openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; +import {DataTypes} from "./lib.sol"; + +interface IScaledBalanceToken { + /** + * @dev Returns the scaled balance of the user. The scaled balance is the sum of all the + * updated stored balance divided by the reserve's liquidity index at the moment of the update + * @param user The user whose balance is calculated + * @return The scaled balance of the user + * + */ + function scaledBalanceOf(address user) external view returns (uint256); + + /** + * @dev Returns the scaled total supply of the variable debt token. Represents sum(debt/index) + * @return The scaled total supply + * + */ + function scaledTotalSupply() external view returns (uint256); +} + +// Aave aToken (wrapped underlying) +interface IAToken is IERC20, IScaledBalanceToken { + /** + * @dev Returns the address of the underlying asset of this aToken (E.g. WETH for aWETH) + * + */ + function UNDERLYING_ASSET_ADDRESS() external view returns (address); + + /** + * @dev Returns the address of the incentives controller contract + * + */ + function getIncentivesController() external view returns (IAaveIncentives); + + function POOL() external view returns (address); +} + +// Aave Incentives controller +interface IAaveIncentives { + /** + * @dev Returns list of reward token addresses for particular aToken. + * + */ + function getRewardsByAsset(address asset) external view returns (address[] memory); + + /** + * @dev Returns list of reward tokens for all markets. + * + */ + function getRewardsList() external view returns (address[] memory); + + /** + * @dev Claim all rewards for specified assets for user. + * + */ + function claimAllRewardsOnBehalf(address[] memory assets, address user, address to) + external + returns (address[] memory rewardsList, uint256[] memory claimedAmount); +} + +// Aave lending pool interface +interface ILendingPool { + function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external; + + function withdraw(address asset, uint256 amount, address to) external returns (uint256); + + function repay(address asset, uint256 amount, uint256 rateMode, address onBehalfOf) external returns (uint256); + + function borrow(address asset, uint256 amount, uint256 interestRateMode, uint16 referralCode, address onBehalfOf) + external; + + function flashLoan( + address receiverAddress, + address[] memory assets, + uint256[] memory amounts, + uint256[] memory interestRateModes, + address onBehalfOf, + bytes calldata params, + uint16 referralCode + ) external; + + function setUserUseReserveAsCollateral(address asset, bool useAsCollateral) external; + + function setUserEMode(uint8 category) external; + + function getEModeCategoryData(uint8 id) external returns (DataTypes.EModeData memory emodeData); + + function getUserEMode(address user) external returns (uint256); + + /** + * @dev Returns the state and configuration of the reserve + * @param asset The address of the underlying asset of the reserve + * @return The state of the reserve + * + */ + function getReserveData(address asset) external view returns (DataTypes.ReserveData2 memory); + + function getReserveNormalizedIncome(address asset) external view returns (uint256); +} + +// Aave protocol data provider +interface IProtocolDataProvider { + function getReserveTokensAddresses(address asset) + external + view + returns (address aTokenAddress, address stableDebtTokenAddress, address variableDebtTokenAddress); +} + +interface IFlashLoanReceiver { + /** + * @notice Executes an operation after receiving the flash-borrowed assets + * @dev Ensure that the contract can return the debt + premium, e.g., has + * enough funds to repay and has approved the Pool to pull the total amount + * @param assets The addresses of the flash-borrowed assets + * @param amounts The amounts of the flash-borrowed assets + * @param premiums The fee of each flash-borrowed asset + * @param initiator The address of the flashloan initiator + * @param params The byte-encoded params passed when initiating the flashloan + * @return True if the execution of the operation succeeds, false otherwise + */ + function executeOperation( + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata premiums, + address initiator, + bytes calldata params + ) external returns (bool); + + function ADDRESSES_PROVIDER() external view returns (IPoolAddressesProvider); + + function POOL() external view returns (ILendingPool); +} + +/** + * @title IPoolAddressesProvider + * @author Aave + * @notice Defines the basic interface for a Pool Addresses Provider. + */ +interface IPoolAddressesProvider { + /** + * @notice Returns the id of the Aave market to which this contract points to. + * @return The market id + */ + function getMarketId() external view returns (string memory); + + /** + * @notice Associates an id with a specific PoolAddressesProvider. + * @dev This can be used to create an onchain registry of PoolAddressesProviders to + * identify and validate multiple Aave markets. + * @param newMarketId The market id + */ + function setMarketId(string calldata newMarketId) external; + + /** + * @notice Returns an address by its identifier. + * @dev The returned address might be an EOA or a contract, potentially proxied + * @dev It returns ZERO if there is no registered address with the given id + * @param id The id + * @return The address of the registered for the specified id + */ + function getAddress(bytes32 id) external view returns (address); + + /** + * @notice General function to update the implementation of a proxy registered with + * certain `id`. If there is no proxy registered, it will instantiate one and + * set as implementation the `newImplementationAddress`. + * @dev IMPORTANT Use this function carefully, only for ids that don't have an explicit + * setter function, in order to avoid unexpected consequences + * @param id The id + * @param newImplementationAddress The address of the new implementation + */ + function setAddressAsProxy(bytes32 id, address newImplementationAddress) external; + + /** + * @notice Sets an address for an id replacing the address saved in the addresses map. + * @dev IMPORTANT Use this function carefully, as it will do a hard replacement + * @param id The id + * @param newAddress The address to set + */ + function setAddress(bytes32 id, address newAddress) external; + + /** + * @notice Returns the address of the Pool proxy. + * @return The Pool proxy address + */ + function getPool() external view returns (address); + + /** + * @notice Updates the implementation of the Pool, or creates a proxy + * setting the new `pool` implementation when the function is called for the first time. + * @param newPoolImpl The new Pool implementation + */ + function setPoolImpl(address newPoolImpl) external; +} diff --git a/src/strategies/aave/aaveV3/lib.sol b/src/strategies/aave/aaveV3/lib.sol new file mode 100644 index 00000000..a0dd29bd --- /dev/null +++ b/src/strategies/aave/aaveV3/lib.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity >=0.8.20; + +library DataTypes { + // refer to the whitepaper, section 1.1 basic concepts for a formal description of these properties. + struct ReserveData { + //stores the reserve configuration + ReserveConfigurationMap configuration; + //the liquidity index. Expressed in ray + uint128 liquidityIndex; + //variable borrow index. Expressed in ray + uint128 variableBorrowIndex; + //the current supply rate. Expressed in ray + uint128 currentLiquidityRate; + //the current variable borrow rate. Expressed in ray + uint128 currentVariableBorrowRate; + //the current stable borrow rate. Expressed in ray + uint128 currentStableBorrowRate; + uint40 lastUpdateTimestamp; + //tokens addresses + address aTokenAddress; + address stableDebtTokenAddress; + address variableDebtTokenAddress; + //address of the interest rate strategy + address interestRateStrategyAddress; + //the id of the reserve. Represents the position in the list of the active reserves + uint8 id; + } + + struct ReserveData2 { + //stores the reserve configuration + ReserveConfigurationMap configuration; + //the liquidity index. Expressed in ray + uint128 liquidityIndex; + //the current supply rate. Expressed in ray + uint128 currentLiquidityRate; + //variable borrow index. Expressed in ray + uint128 variableBorrowIndex; + //the current variable borrow rate. Expressed in ray + uint128 currentVariableBorrowRate; + //the current stable borrow rate. Expressed in ray + uint128 currentStableBorrowRate; + //timestamp of last update + uint40 lastUpdateTimestamp; + //the id of the reserve. Represents the position in the list of the active reserves + uint16 id; + //aToken address + address aTokenAddress; + //stableDebtToken address + address stableDebtTokenAddress; + //variableDebtToken address + address variableDebtTokenAddress; + //address of the interest rate strategy + address interestRateStrategyAddress; + //the current treasury balance, scaled + uint128 accruedToTreasury; + //the outstanding unbacked aTokens minted through the bridging feature + uint128 unbacked; + //the outstanding debt borrowed against this asset in isolation mode + uint128 isolationModeTotalDebt; + } + + struct ReserveConfigurationMap { + //bit 0-15: LTV + //bit 16-31: Liq. threshold + //bit 32-47: Liq. bonus + //bit 48-55: Decimals + //bit 56: Reserve is active + //bit 57: reserve is frozen + //bit 58: borrowing is enabled + //bit 59: stable rate borrowing enabled + //bit 60-63: reserved + //bit 64-79: reserve factor + uint256 data; + } + + struct UserConfigurationMap { + uint256 data; + } + + struct EModeData { + uint16 maxLTV; + uint16 liqThreshold; + uint16 liqBonus; + address priceSource; + string label; + } + + enum InterestRateMode { + NONE, + STABLE, + VARIABLE + } +} diff --git a/src/strategies/aura/AuraCompounder.sol b/src/strategies/aura/AuraCompounder.sol index fe5a6dfb..f9b67f3b 100644 --- a/src/strategies/aura/AuraCompounder.sol +++ b/src/strategies/aura/AuraCompounder.sol @@ -81,7 +81,7 @@ contract AuraCompounder is BaseStrategy, BaseBalancerLpCompounder { /// @notice The token rewarded function rewardTokens() external view override returns (address[] memory) { - return _rewardTokens; + return _balancerSellTokens; } /*////////////////////////////////////////////////////////////// diff --git a/src/strategies/balancer/BalancerCompounder.sol b/src/strategies/balancer/BalancerCompounder.sol index 668f0716..87b7ee5e 100644 --- a/src/strategies/balancer/BalancerCompounder.sol +++ b/src/strategies/balancer/BalancerCompounder.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.25; import {BaseStrategy, IERC20, IERC20Metadata, SafeERC20, ERC20, Math} from "../BaseStrategy.sol"; -import {IMinter, IGauge} from "./IBalancer.sol"; +import {IMinter, IGauge} from "../../interfaces/external/balancer/IBalancer.sol"; import {BaseBalancerLpCompounder, HarvestValues, TradePath} from "../../peripheral/BaseBalancerLpCompounder.sol"; /** @@ -76,7 +76,7 @@ contract BalancerCompounder is BaseStrategy, BaseBalancerLpCompounder { /// @notice The token rewarded function rewardTokens() external view override returns (address[] memory) { - return _rewardTokens; + return _balancerSellTokens; } /*////////////////////////////////////////////////////////////// diff --git a/src/strategies/beefy/BeefyDepositor.sol b/src/strategies/beefy/BeefyDepositor.sol new file mode 100644 index 00000000..be4bda4e --- /dev/null +++ b/src/strategies/beefy/BeefyDepositor.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {BaseStrategy, IERC20, IERC20Metadata, SafeERC20, ERC20, Math} from "../BaseStrategy.sol"; +import {IBeefyVault, IBeefyStrat} from "./IBeefy.sol"; + +/** + * @title Beefy Adapter + * @author RedVeil + * @notice ERC4626 wrapper for Beefy Vaults. + */ +contract BeefyDepositor is BaseStrategy { + using SafeERC20 for IERC20; + using Math for uint256; + + string internal _name; + string internal _symbol; + + IBeefyVault public beefyVault; + + uint256 public constant BPS_DENOMINATOR = 10_000; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Initialize a new Strategy. + * @param asset_ The underlying asset used for deposit/withdraw and accounting + * @param owner_ Owner of the contract. Controls management functions. + * @param autoDeposit_ Controls if `protocolDeposit` gets called on deposit + * @param strategyInitData_ Encoded data for this specific strategy + */ + function initialize(address asset_, address owner_, bool autoDeposit_, bytes memory strategyInitData_) + external + initializer + { + address _beefyVault = abi.decode(strategyInitData_, (address)); + + beefyVault = IBeefyVault(_beefyVault); + + __BaseStrategy_init(asset_, owner_, autoDeposit_); + + IERC20(asset_).approve(_beefyVault, type(uint256).max); + + _name = string.concat("VaultCraft Beefy ", IERC20Metadata(asset_).name(), " Adapter"); + _symbol = string.concat("vcB-", IERC20Metadata(asset_).symbol()); + } + + function name() public view override(IERC20Metadata, ERC20) returns (string memory) { + return _name; + } + + function symbol() public view override(IERC20Metadata, ERC20) returns (string memory) { + return _symbol; + } + + /*////////////////////////////////////////////////////////////// + ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ + + function _totalAssets() internal view override returns (uint256) { + return beefyVault.balanceOf(address(this)).mulDiv( + beefyVault.balance(), beefyVault.totalSupply(), Math.Rounding.Floor + ); + } + + /// @notice The amount of beefy shares to withdraw given an amount of adapter shares + function convertToUnderlyingShares(uint256, uint256 shares) public view override returns (uint256) { + uint256 supply = totalSupply(); + return supply == 0 ? shares : shares.mulDiv(beefyVault.balanceOf(address(this)), supply, Math.Rounding.Ceil); + } + + /// @notice `previewWithdraw` that takes beefy withdrawal fees into account + function previewWithdraw(uint256 assets) public view override returns (uint256) { + IBeefyStrat strat = IBeefyStrat(beefyVault.strategy()); + + uint256 beefyFee; + try strat.withdrawalFee() returns (uint256 _beefyFee) { + beefyFee = _beefyFee; + } catch { + beefyFee = strat.withdrawFee(); + } + + if (beefyFee > 0) { + assets = assets.mulDiv(BPS_DENOMINATOR, BPS_DENOMINATOR - beefyFee, Math.Rounding.Floor); + } + + return _convertToShares(assets, Math.Rounding.Ceil); + } + + /// @notice `previewRedeem` that takes beefy withdrawal fees into account + function previewRedeem(uint256 shares) public view override returns (uint256) { + uint256 assets = _convertToAssets(shares, Math.Rounding.Floor); + + IBeefyStrat strat = IBeefyStrat(beefyVault.strategy()); + + uint256 beefyFee; + try strat.withdrawalFee() returns (uint256 _beefyFee) { + beefyFee = _beefyFee; + } catch { + beefyFee = strat.withdrawFee(); + } + + if (beefyFee > 0) { + assets = assets.mulDiv(BPS_DENOMINATOR - beefyFee, BPS_DENOMINATOR, Math.Rounding.Floor); + } + + return assets; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL HOOKS LOGIC + //////////////////////////////////////////////////////////////*/ + + function _protocolDeposit(uint256 assets, uint256, bytes memory) internal override { + beefyVault.deposit(assets); + } + + function _protocolWithdraw(uint256, uint256 shares, bytes memory) internal override { + uint256 beefyShares = convertToUnderlyingShares(0, shares); + + beefyVault.withdraw(beefyShares); + } +} diff --git a/src/strategies/beefy/IBeefy.sol b/src/strategies/beefy/IBeefy.sol new file mode 100644 index 00000000..bbd5d7a1 --- /dev/null +++ b/src/strategies/beefy/IBeefy.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 + +pragma solidity ^0.8.15; + +interface IBeefyVault { + function want() external view returns (address); + + function deposit(uint256 _amount) external; + + function withdraw(uint256 _shares) external; + + function withdrawAll() external; + + function balanceOf(address _account) external view returns (uint256); + + //Returns total balance of underlying token in the vault and its strategies + function balance() external view returns (uint256); + + function totalSupply() external view returns (uint256); + + function earn() external; + + function getPricePerFullShare() external view returns (uint256); + + function strategy() external view returns (address); +} + +interface IBeefyStrat { + function withdrawFee() external view returns (uint256); + + function withdrawalFee() external view returns (uint256); +} diff --git a/src/strategies/compound/v2/CompoundV2Depositor.sol b/src/strategies/compound/v2/CompoundV2Depositor.sol new file mode 100644 index 00000000..59cc94b6 --- /dev/null +++ b/src/strategies/compound/v2/CompoundV2Depositor.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {BaseStrategy, IERC20, IERC20Metadata, SafeERC20, ERC20, Math} from "../../BaseStrategy.sol"; +import {ICToken, IComptroller} from "./ICompoundV2.sol"; +import {LibCompound} from "./LibCompound.sol"; + +/** + * @title CompoundV2 Adapter + * @author RedVeil + * @notice ERC4626 wrapper for CompoundV2 Vaults. + */ +contract CompoundV2Depositor is BaseStrategy { + using SafeERC20 for IERC20; + using Math for uint256; + + string internal _name; + string internal _symbol; + + /// @notice The Compound cToken contract + ICToken public cToken; + + /// @notice The Compound Comptroller contract + IComptroller public comptroller; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Initialize a new Strategy. + * @param asset_ The underlying asset used for deposit/withdraw and accounting + * @param owner_ Owner of the contract. Controls management functions. + * @param autoDeposit_ Controls if `protocolDeposit` gets called on deposit + * @param strategyInitData_ Encoded data for this specific strategy + */ + function initialize(address asset_, address owner_, bool autoDeposit_, bytes memory strategyInitData_) + external + initializer + { + (address cToken_, address comptroller_) = abi.decode(strategyInitData_, (address, address)); + + cToken = ICToken(cToken_); + comptroller = IComptroller(comptroller_); + + __BaseStrategy_init(asset_, owner_, autoDeposit_); + + IERC20(asset_).approve(cToken_, type(uint256).max); + + _name = string.concat("VaultCraft CompoundV2 ", IERC20Metadata(asset_).name(), " Adapter"); + _symbol = string.concat("vcCv2-", IERC20Metadata(asset_).symbol()); + } + + function name() public view override(IERC20Metadata, ERC20) returns (string memory) { + return _name; + } + + function symbol() public view override(IERC20Metadata, ERC20) returns (string memory) { + return _symbol; + } + + /*////////////////////////////////////////////////////////////// + ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @dev CompoundV2 has some rounding issues on deposit / withdraw which "steals" small amount of funds from the user on instant deposit/withdrawals + /// As one can see in the tests we need to adjust the expected delta here slightly. + /// These issues should vanish over time with a bit of interest and arent security relevant + function _totalAssets() internal view override returns (uint256) { + return LibCompound.viewUnderlyingBalanceOf(cToken, address(this)); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL HOOKS LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Deposit into aave lending pool + function _protocolDeposit(uint256 assets, uint256, bytes memory) internal override { + cToken.mint(assets); + } + + /// @notice Withdraw from lending pool + function _protocolWithdraw(uint256 assets, uint256, bytes memory) internal override { + cToken.redeemUnderlying(assets); + } +} diff --git a/src/strategies/compound/v2/ICompoundV2.sol b/src/strategies/compound/v2/ICompoundV2.sol new file mode 100644 index 00000000..bdc3ef6f --- /dev/null +++ b/src/strategies/compound/v2/ICompoundV2.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +interface ICToken { + /** + * @dev Returns the address of the underlying asset of this cToken + * + */ + function underlying() external view returns (address); + + /** + * @dev Returns the symbol of this cToken + * + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the address of the comptroller + * + */ + function comptroller() external view returns (address); + + function balanceOf(address) external view returns (uint256); + + /** + * @dev Send underlying to mint cToken. + * + */ + function mint(uint256) external; + + function redeem(uint256) external; + + function redeemUnderlying(uint256) external returns (uint256); + + /** + * @dev Returns exchange rate from the underlying to the cToken. + * + */ + function exchangeRateStored() external view returns (uint256); + + function getCash() external view returns (uint256); + + function totalBorrows() external view returns (uint256); + + function totalReserves() external view returns (uint256); + + function borrowRatePerBlock() external view returns (uint256); + + function reserveFactorMantissa() external view returns (uint256); + + function totalSupply() external view returns (uint256); + + function accrualBlockNumber() external view returns (uint256); + + function balanceOfUnderlying(address owner) external view returns (uint256); + + function exchangeRateCurrent() external; +} + +interface IComptroller { + /** + * @dev Returns the address of the underlying asset of this cToken + * + */ + function getCompAddress() external view returns (address); + + /** + * @dev Returns the address of the underlying asset of this cToken + * + */ + function compSpeeds(address) external view returns (uint256); + + function compSupplySpeeds(address) external view returns (uint256); + + /** + * @dev Returns the isListed, collateralFactorMantissa, and isCompred of the cToken market + * + */ + function markets(address) external view returns (bool, uint256, bool); + + function claimComp(address holder) external; +} diff --git a/src/strategies/compound/v2/LibCompound.sol b/src/strategies/compound/v2/LibCompound.sol new file mode 100644 index 00000000..8f7e114f --- /dev/null +++ b/src/strategies/compound/v2/LibCompound.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {Math} from "openzeppelin-contracts/utils/math/Math.sol"; + +import {ICToken} from "./ICompoundV2.sol"; + +/// @notice Get up to date cToken data without mutating state. +/// @author Transmissions11 (https://github.com/transmissions11/libcompound) +library LibCompound { + using FixedPointMathLib for uint256; + using Math for uint256; + + function viewUnderlyingBalanceOf(ICToken cToken, address user) internal view returns (uint256) { + return cToken.balanceOf(user).mulWadDown(viewExchangeRate(cToken)); + } + + function viewExchangeRate(ICToken cToken) internal view returns (uint256) { + uint256 accrualBlockNumberPrior = cToken.accrualBlockNumber(); + + if (accrualBlockNumberPrior == block.number) { + return cToken.exchangeRateStored(); + } + + uint256 totalCash = cToken.getCash(); + uint256 borrowsPrior = cToken.totalBorrows(); + uint256 reservesPrior = cToken.totalReserves(); + + uint256 borrowRateMantissa = cToken.borrowRatePerBlock(); + + require(borrowRateMantissa <= 0.0005e16, "RATE_TOO_HIGH"); // Same as borrowRateMaxMantissa in ICTokenInterfaces.sol + + uint256 interestAccumulated = + (borrowRateMantissa * (block.number - accrualBlockNumberPrior)).mulWadDown(borrowsPrior); + + uint256 totalReserves = cToken.reserveFactorMantissa().mulWadDown(interestAccumulated) + reservesPrior; + uint256 totalBorrows = interestAccumulated + borrowsPrior; + uint256 totalSupply = cToken.totalSupply(); + + // Reverts if totalSupply == 0 + return (totalCash + totalBorrows - totalReserves).divWadDown(totalSupply); + } + + /// @notice The amount of compound shares to withdraw given an mount of adapter shares + function convertToUnderlyingShares(uint256 shares, uint256 totalSupply, uint256 adapterCTokenBalance) + public + pure + returns (uint256) + { + return totalSupply == 0 ? shares : shares.mulDivUp(adapterCTokenBalance, totalSupply); + } +} diff --git a/src/strategies/compound/v3/CompoundV3Depositor.sol b/src/strategies/compound/v3/CompoundV3Depositor.sol new file mode 100644 index 00000000..f9dfb454 --- /dev/null +++ b/src/strategies/compound/v3/CompoundV3Depositor.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {BaseStrategy, IERC20, IERC20Metadata, SafeERC20, ERC20, Math} from "../../BaseStrategy.sol"; +import {ICToken} from "./ICompoundV3.sol"; + +/** + * @title CompoundV3 Adapter + * @author RedVeil + * @notice ERC4626 wrapper for CompoundV3 Vaults. + */ +contract CompoundV3Depositor is BaseStrategy { + using SafeERC20 for IERC20; + using Math for uint256; + + string internal _name; + string internal _symbol; + + /// @notice The Compound cToken contract + ICToken public cToken; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Initialize a new Strategy. + * @param asset_ The underlying asset used for deposit/withdraw and accounting + * @param owner_ Owner of the contract. Controls management functions. + * @param autoDeposit_ Controls if `protocolDeposit` gets called on deposit + * @param strategyInitData_ Encoded data for this specific strategy + */ + function initialize(address asset_, address owner_, bool autoDeposit_, bytes memory strategyInitData_) + external + initializer + { + address cToken_ = abi.decode(strategyInitData_, (address)); + + cToken = ICToken(cToken_); + + __BaseStrategy_init(asset_, owner_, autoDeposit_); + + IERC20(asset_).approve(cToken_, type(uint256).max); + + _name = string.concat("VaultCraft CompoundV3 ", IERC20Metadata(asset_).name(), " Adapter"); + _symbol = string.concat("vcCv3-", IERC20Metadata(asset_).symbol()); + } + + function name() public view override(IERC20Metadata, ERC20) returns (string memory) { + return _name; + } + + function symbol() public view override(IERC20Metadata, ERC20) returns (string memory) { + return _symbol; + } + + /*////////////////////////////////////////////////////////////// + ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ + + function _totalAssets() internal view override returns (uint256) { + return cToken.balanceOf(address(this)); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL HOOKS LOGIC + //////////////////////////////////////////////////////////////*/ + + function _protocolDeposit(uint256 assets, uint256, bytes memory) internal override { + cToken.supply(asset(), assets); + } + + function _protocolWithdraw(uint256 assets, uint256, bytes memory) internal override { + cToken.withdraw(asset(), assets); + } +} diff --git a/src/strategies/compound/v3/ICompoundV3.sol b/src/strategies/compound/v3/ICompoundV3.sol new file mode 100644 index 00000000..5433b74b --- /dev/null +++ b/src/strategies/compound/v3/ICompoundV3.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +interface ICToken { + function baseTrackingBorrowSpeed() external view returns (uint256); + + function baseTrackingSupplySpeed() external view returns (uint256); + + function balanceOf(address _user) external view returns (uint256); + + function governor() external view returns (address); + + function isSupplyPaused() external view returns (bool); + + function supply(address _asset, uint256 _amount) external; + + function isWithdrawPaused() external view returns (bool); + + function withdraw(address _asset, uint256 _amount) external; + + function baseToken() external view returns (address); +} + +interface ICometRewarder { + function claim(address _cToken, address _owner, bool _accrue) external; +} + +interface IGovernor { + function admin() external view returns (address); +} + +interface IAdmin { + function comp() external view returns (address); +} + +interface ICometConfigurator { + struct Configuration { + address governor; + address pauseGuardian; + address baseToken; + address baseTokenPriceFeed; + address extensionDelegate; + uint64 supplyKink; + uint64 supplyPerYearInterestRateSlopeLow; + uint64 supplyPerYearInterestRateSlopeHigh; + uint64 supplyPerYearInterestRateBase; + uint64 borrowKink; + uint64 borrowPerYearInterestRateSlopeLow; + uint64 borrowPerYearInterestRateSlopeHigh; + uint64 borrowPerYearInterestRateBase; + uint64 storeFrontPriceFactor; + uint64 trackingIndexScale; + uint64 baseTrackingSupplySpeed; + uint64 baseTrackingBorrowSpeed; + uint104 baseMinForRewards; + uint104 baseBorrowMin; + uint104 targetReserves; + AssetConfig[] assetConfigs; + } + + struct AssetConfig { + address asset; + address priceFeed; + uint8 decimals; + uint64 borrowCollateralFactor; + uint64 liquidateCollateralFactor; + uint64 liquidationFactor; + uint128 supplyCap; + } + + function getConfiguration(address cometProxy) external view returns (Configuration memory); +} diff --git a/src/strategies/convex/ConvexCompounder.sol b/src/strategies/convex/ConvexCompounder.sol index a0e85a8f..5c3d99cc 100644 --- a/src/strategies/convex/ConvexCompounder.sol +++ b/src/strategies/convex/ConvexCompounder.sol @@ -92,7 +92,7 @@ contract ConvexCompounder is BaseStrategy, BaseCurveLpCompounder { /// @notice The token rewarded from the convex reward contract function rewardTokens() external view override returns (address[] memory) { - return _rewardTokens; + return _curveSellTokens; } /*////////////////////////////////////////////////////////////// diff --git a/src/strategies/curve/CurveGaugeCompounder.sol b/src/strategies/curve/CurveGaugeCompounder.sol index 5e6feeb6..42702dc4 100644 --- a/src/strategies/curve/CurveGaugeCompounder.sol +++ b/src/strategies/curve/CurveGaugeCompounder.sol @@ -78,7 +78,7 @@ contract CurveGaugeCompounder is BaseStrategy, BaseCurveLpCompounder { /// @notice The token rewarded from the convex reward contract function rewardTokens() external view override returns (address[] memory) { - return _rewardTokens; + return _curveSellTokens; } /*////////////////////////////////////////////////////////////// diff --git a/src/strategies/curve/CurveGaugeSingleAssetCompounder.sol b/src/strategies/curve/CurveGaugeSingleAssetCompounder.sol index 205d78f4..168fcfdf 100644 --- a/src/strategies/curve/CurveGaugeSingleAssetCompounder.sol +++ b/src/strategies/curve/CurveGaugeSingleAssetCompounder.sol @@ -88,7 +88,7 @@ contract CurveGaugeSingleAssetCompounder is BaseStrategy, BaseCurveCompounder { /// @notice The token rewarded from the convex reward contract function rewardTokens() external view override returns (address[] memory) { - return _rewardTokens; + return _curveSellTokens; } function previewDeposit(uint256 assets) public view override returns (uint256) { diff --git a/src/strategies/lido/WstETHLooper.sol b/src/strategies/lido/WstETHLooper.sol index 3388b6f0..78875168 100644 --- a/src/strategies/lido/WstETHLooper.sol +++ b/src/strategies/lido/WstETHLooper.sol @@ -16,7 +16,7 @@ import { IProtocolDataProvider, IPoolAddressesProvider, DataTypes -} from "../../interfaces/external/aave/IAaveV3.sol"; +} from "../aave/aaveV3/IAaveV3.sol"; struct LooperInitValues { address aaveDataProvider; diff --git a/src/strategies/peapods/IPeapods.sol b/src/strategies/peapods/IPeapods.sol new file mode 100644 index 00000000..df664bee --- /dev/null +++ b/src/strategies/peapods/IPeapods.sol @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2020 Lido + +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.25; + +import {IERC20} from "openzeppelin-contracts/interfaces/IERC20.sol"; + +// @dev router contract +interface IIndexUtils { + function addLPAndStake( + address _indexFund, + uint256 _amountIdxTokens, + address _pairedLpTokenProvided, + uint256 _amtPairedLpTokenProvided, + uint256 _amountPairedLpTokenMin, + uint256 _slippage, + uint256 _deadline + ) external; + + function unstakeAndRemoveLP( + address _indexFund, + uint256 _amountStakedTokens, + uint256 _minLPTokens, + uint256 _minPairedLpToken, + uint256 _deadline + ) external; +} + +interface IIndexToken { + function indexTokens(uint256 index) external view returns( + address token, + uint256 weighting, + uint256 basePriceUSDX96, + uint256 c1, + uint256 q1 + ); +} + +interface ICamelotLPToken { + function addLiquidityV2(uint256 _idxLPTokens,uint256 _pairedLPTokens,uint256 _slippage,uint256 _deadline) external; +} + +interface ITokenPod { + // unwraps pod-token into its underlying + // _token: underlying address + // _amount: amount of pod-token to unwrap + // _percentage: 100 + function debond(uint256 _amount,address[] memory _token,uint8[] memory _percentage) external; +} + +interface IStakedToken { + // returns the address of the camelotLP token staked + function stakingToken() external view returns (address lpToken); + + // returns the address of the pool to claim rewards + function poolRewards() external view returns (address poolReward); + + // stakes LP token for rewards. output amount is 1:1 + function stake(address user, uint256 amount) external; + + // unstakes and receives LP token, 1:1 + function unstake(uint256 amount) external; + +} + +interface IPoolRewards { + // returns token address + function rewardsToken() external view returns (address); + + function claimReward(address wallet) external; + + function shares(address who) external view returns (uint256); +} + +interface IPeapods { + function totalSupply() external view returns (uint256); + + function getTotalShares() external view returns (uint256); + + function asset() external view returns (address); + + function initialize(bytes memory adapterInitData, address _wethAddress, bytes memory lidoInitData) external; +} \ No newline at end of file diff --git a/src/strategies/peapods/PeapodsBalancerUniV2Compounder.sol b/src/strategies/peapods/PeapodsBalancerUniV2Compounder.sol new file mode 100644 index 00000000..931ddc80 --- /dev/null +++ b/src/strategies/peapods/PeapodsBalancerUniV2Compounder.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 + +pragma solidity ^0.8.15; + +import {PeapodsDepositor, IERC20, SafeERC20} from "./PeapodsStrategy.sol"; +import {BaseBalancerLpCompounder, HarvestValues, TradePath} from "../../peripheral/BaseBalancerLpCompounder.sol"; +import {BaseUniV2Compounder, SwapStep} from "../../peripheral/BaseUniV2Compounder.sol"; + +/** + * @title ERC4626 Peapods Protocol Vault Adapter + * @author ADN + * @notice ERC4626 wrapper for Peapods protocol + * + * An ERC4626 compliant Wrapper for Peapods. + * Implements harvest func that swaps via Balancer + */ +contract PeapodsDepositorBalancerUniV2Compounder is PeapodsDepositor, BaseBalancerLpCompounder, BaseUniV2Compounder{ + using SafeERC20 for IERC20; + + /*////////////////////////////////////////////////////////////// + INITIALIZATION + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Initialize a new Strategy. + * @param asset_ The underlying asset used for deposit/withdraw and accounting + * @param owner_ Owner of the contract. Controls management functions. + * @param autoDeposit_ Controls if `protocolDeposit` gets called on deposit + * @param strategyInitData_ Encoded data for this specific strategy + */ + function initialize(address asset_, address owner_, bool autoDeposit_, bytes memory strategyInitData_) + external + override + initializer + { + __PeapodsBase_init(asset_, owner_, autoDeposit_, strategyInitData_); + } + + /*////////////////////////////////////////////////////////////// + REWARDS LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice The token rewarded from the pendle market + function rewardTokens() external view override returns (address[] memory) { + return sellTokens; + } + + /*////////////////////////////////////////////////////////////// + HARVESGT LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Claim rewards, swaps to asset and add liquidity + */ + function harvest(bytes memory data) external override onlyKeeperOrOwner { + claim(); + + // caching + address asset_ = asset(); + + // sell for a balancer asset via univ2 + sellRewardsViaUniswapV2(); + + // sell the balancer asset for deposit asset and add liquidity + sellRewardsForLpTokenViaBalancer(asset_, data); + + // compound the lp token + _protocolDeposit(IERC20(asset_).balanceOf(address(this)), 0, data); + + emit Harvested(); + } + + function setHarvestValues( + address newBalancerVault, + TradePath[] memory newTradePaths, + HarvestValues memory harvestValues_, + address newUniswapRouter, + address[] memory rewTokens, + SwapStep[] memory newSwapSteps + ) external onlyOwner { + setUniswapTradeValues(newUniswapRouter, rewTokens, newSwapSteps); + setBalancerLpCompounderValues(newBalancerVault, newTradePaths, harvestValues_); + } +} diff --git a/src/strategies/peapods/PeapodsStrategy.sol b/src/strategies/peapods/PeapodsStrategy.sol new file mode 100644 index 00000000..fec7ed9c --- /dev/null +++ b/src/strategies/peapods/PeapodsStrategy.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 + +pragma solidity ^0.8.15; + +import {BaseStrategy, IERC20, IERC20Metadata, SafeERC20, ERC20, Math} from "../BaseStrategy.sol"; +import { + IStakedToken, + IPoolRewards +} from "./IPeapods.sol"; + +/** + * @title ERC4626 Peapods Finance Vault Adapter + * @author ADN + * @notice ERC4626 wrapper for Peapods protocol + * + * Receives Peapods Camelot-LP tokens and stakes them for extra rewards. + * Claim and compound the rewards into more LP tokens + */ +contract PeapodsDepositor is BaseStrategy { + using SafeERC20 for IERC20; + using Math for uint256; + + string internal _name; + string internal _symbol; + + IStakedToken public stakedToken; // vault holding after deposit + IPoolRewards public poolRewards; // pool to get rewards from + + /*////////////////////////////////////////////////////////////// + INITIALIZATION + //////////////////////////////////////////////////////////////*/ + + error InvalidAsset(); + + /** + * @notice Initialize a new Strategy. + * @param asset_ The underlying asset used for deposit/withdraw and accounting + * @param owner_ Owner of the contract. Controls management functions. + * @param autoDeposit_ Controls if `protocolDeposit` gets called on deposit + * @param strategyInitData_ Encoded data for this specific strategy + */ + function initialize(address asset_, address owner_, bool autoDeposit_, bytes memory strategyInitData_) + external + virtual + initializer + { + __PeapodsBase_init(asset_, owner_, autoDeposit_, strategyInitData_); + } + + function __PeapodsBase_init(address asset_, address owner_, bool autoDeposit_, bytes memory strategyInitData_) + internal + onlyInitializing + { + // asset is LP token + __BaseStrategy_init(asset_, owner_, autoDeposit_); + + _name = string.concat("VaultCraft Peapods ", IERC20Metadata(asset_).name(), " Adapter"); + _symbol = string.concat("vcp-", IERC20Metadata(asset_).symbol()); + + // validate staking contract + (address staking_) = abi.decode(strategyInitData_, (address)); + stakedToken = IStakedToken(staking_); + + if(stakedToken.stakingToken() != asset_) + revert InvalidAsset(); + + poolRewards = IPoolRewards(stakedToken.poolRewards()); + + // approve peapods staking contract + IERC20(asset_).approve(staking_, type(uint256).max); + } + + + function name() public view override(IERC20Metadata, ERC20) returns (string memory) { + return _name; + } + + function symbol() public view override(IERC20Metadata, ERC20) returns (string memory) { + return _symbol; + } + + /*////////////////////////////////////////////////////////////// + ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ + + function _totalAssets() internal view override returns (uint256 t) { + // return balance of staked tokens -> 1:1 with LP token + return IERC20(address(stakedToken)).balanceOf(address(this)); + } + + /*////////////////////////////////////////////////////////////// + REWARDS LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Claim liquidity mining rewards given that it's active + function claim() internal override returns (bool success) { + try poolRewards.claimReward(address(this)){ + success = true; + } catch {} + } + + /*////////////////////////////////////////////////////////////// + INTERNAL HOOKS LOGIC + //////////////////////////////////////////////////////////////*/ + + function _protocolDeposit(uint256 amount, uint256, bytes memory) internal override { + // stake lp tokens + stakedToken.stake(address(this), amount); + } + + function _protocolWithdraw(uint256 amount, uint256, bytes memory) internal override { + // unstake lp tokens + stakedToken.unstake(amount); + } +} diff --git a/src/strategies/peapods/PeapodsUniswapV2Compounder.sol b/src/strategies/peapods/PeapodsUniswapV2Compounder.sol new file mode 100644 index 00000000..2f19e9bd --- /dev/null +++ b/src/strategies/peapods/PeapodsUniswapV2Compounder.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 + +pragma solidity ^0.8.15; + +import {PeapodsDepositor, IERC20, SafeERC20} from "./PeapodsStrategy.sol"; +import {BaseUniV2LpCompounder, SwapStep} from "../../peripheral/BaseUniV2LpCompounder.sol"; + +/** + * @title ERC4626 Peapods Protocol Vault Adapter + * @author ADN + * @notice ERC4626 wrapper for Peapods protocol + * + * An ERC4626 compliant Wrapper for Peapods. + * Implements harvest func that swaps via Uniswap V2 + */ +contract PeapodsDepositorUniswapV2Compounder is PeapodsDepositor, BaseUniV2LpCompounder{ + using SafeERC20 for IERC20; + + /*////////////////////////////////////////////////////////////// + INITIALIZATION + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Initialize a new Strategy. + * @param asset_ The underlying asset used for deposit/withdraw and accounting + * @param owner_ Owner of the contract. Controls management functions. + * @param autoDeposit_ Controls if `protocolDeposit` gets called on deposit + * @param strategyInitData_ Encoded data for this specific strategy + */ + function initialize(address asset_, address owner_, bool autoDeposit_, bytes memory strategyInitData_) + external + override + initializer + { + __PeapodsBase_init(asset_, owner_, autoDeposit_, strategyInitData_); + } + + /*////////////////////////////////////////////////////////////// + REWARDS LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice The token rewarded from the pendle market + function rewardTokens() external view override returns (address[] memory) { + return sellTokens; + } + + /*////////////////////////////////////////////////////////////// + HARVESGT LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Claim rewards, swaps to asset and add liquidity + */ + function harvest(bytes memory data) external override onlyKeeperOrOwner { + claim(); + + // caching + address asset_ = asset(); + + sellRewardsForLpTokenViaUniswap(asset_, address(this), block.timestamp, data); + + _protocolDeposit(IERC20(asset_).balanceOf(address(this)), 0, data); + + emit Harvested(); + } + + function setHarvestValues( + address[] memory rewTokens, + address newRouter, + address[2] memory newDepositAssets, + SwapStep[] memory newSwaps + ) external onlyOwner { + setUniswapLpCompounderValues(newRouter, newDepositAssets, rewTokens, newSwaps); + } + + // allow owner to withdraw eventual dust amount of tokens + // from the compounding operation + function withdrawDust(address token) external onlyOwner { + if(token != depositAssets[0] && token != depositAssets[1]) + revert("Invalid Token"); + + IERC20(token).safeTransfer(owner, IERC20(token).balanceOf(address(this))); + } +} diff --git a/src/strategies/pendle/IPendle.sol b/src/strategies/pendle/IPendle.sol new file mode 100644 index 00000000..36358b35 --- /dev/null +++ b/src/strategies/pendle/IPendle.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.25; + +import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "openzeppelin-contracts/token/ERC20/extensions/IERC20Metadata.sol"; +/* + ******************************************************************************************************************* + ******************************************************************************************************************* + * NOTICE * + * Refer to https://docs.pendle.finance/Developers/Contracts/PendleRouter for more information on + * TokenInput, TokenOutput, ApproxParams, LimitOrderData + * It's recommended to use Pendle's Hosted SDK to generate the params + ******************************************************************************************************************* + ******************************************************************************************************************* + */ + +enum OrderType { + SY_FOR_PT, + PT_FOR_SY, + SY_FOR_YT, + YT_FOR_SY +} + +struct Order { + uint256 salt; + uint256 expiry; + uint256 nonce; + OrderType orderType; + address token; + address YT; + address maker; + address receiver; + uint256 makingAmount; + uint256 lnImpliedRate; + uint256 failSafeRate; + bytes permit; +} + +struct FillOrderParams { + Order order; + bytes signature; + uint256 makingAmount; +} + +// if not using LimitOrder, leave alla fields empty +struct LimitOrderData { + address limitRouter; + uint256 epsSkipMarket; + FillOrderParams[] normalFills; + FillOrderParams[] flashFills; + bytes optData; +} + +struct ApproxParams { + uint256 guessMin; + uint256 guessMax; + uint256 guessOffchain; + uint256 maxIteration; + uint256 eps; +} + +enum SwapType { + NONE, + KYBERSWAP, + ONE_INCH, + // ETH_WETH not used in Aggregator + ETH_WETH +} + +struct SwapData { + SwapType swapType; + address extRouter; + bytes extCalldata; + bool needScale; +} + +struct TokenInput { + // TOKEN DATA + address tokenIn; + uint256 netTokenIn; + address tokenMintSy; + // AGGREGATOR DATA + address pendleSwap; + SwapData swapData; +} + +struct TokenOutput { + // TOKEN DATA + address tokenOut; + uint256 minTokenOut; + address tokenRedeemSy; + // AGGREGATOR DATA + address pendleSwap; + SwapData swapData; +} + +interface IPendleRouter { + function addLiquiditySingleToken( + address receiver, + address market, + uint256 minLpOut, + ApproxParams calldata guessPtReceivedFromSy, + TokenInput calldata input, + LimitOrderData calldata limit + ) external payable returns (uint256 netLpOut, uint256 netSyFee, uint256 netSyInterm); + + function removeLiquiditySingleToken( + address receiver, + address market, + uint256 netLpToRemove, + TokenOutput calldata output, + LimitOrderData calldata limit + ) external returns (uint256 netTokenOut, uint256 netSyFee, uint256 netSyInterm); +} + +interface IPendleRouterStatic { + function removeLiquiditySingleTokenStatic(address market, uint256 netLpToRemove, address tokenOut) + external + view + returns ( + uint256 netTokenOut, + uint256 netSyFee, + uint256 priceImpact, + uint256 exchangeRateAfter, + uint256 netSyOut, + uint256 netSyFromBurn, + uint256 netPtFromBurn, + uint256 netSyFromSwap + ); +} + +interface IPendleMarket is IERC20 { + // return pendle tokens of a market + function readTokens() external view returns (address _SY, address _PT, address _YT); + + // return reward tokens + function getRewardTokens() external view returns (address[] memory); + + // claim rewards in the same order as reward tokens + function redeemRewards(address user) external returns (uint256[] memory); + + function increaseObservationsCardinalityNext(uint16 cardinalityNext) external; +} + +interface IPendleSYToken is IERC20Metadata { + // returns all tokens that can mint this SY token + function getTokensIn() external view returns (address[] memory); + + // returns all tokens that can be redeemed from this SY token + function getTokensOut() external view returns (address[] memory); + + function totalSupply() external view returns (uint256); + + // returns exchange rate with underlying + function exchangeRate() external view returns(uint256); +} + +interface ISYTokenV3 is IPendleSYToken { + // returns all tokens that can mint this SY token + function supplyCap() external view returns (uint256); +} + +interface IPendleGauge { + function totalActiveSupply() external view returns (uint256); + + function activeBalance(address user) external view returns (uint256); + + /// @notice Redeem all accrued rewards, returning amountOuts in the same order as getRewardTokens. + function redeemRewards(address user) external returns (uint256[] memory); + + /// @notice Returns the list of reward tokens being distributed + function getRewardTokens() external view returns (address[] memory); +} + +interface IPendleOracle { + // returns exchange rate between lp token and underlying + // duration is timestamp remaining till market expiry + function getLpToAssetRate(address market, uint32 duration) external view returns (uint256 ptToAssetRate); + + function getLpToSyRate(address market, uint32 duration) external view returns (uint256 ptToSyRate); + + function getOracleState(address market, uint32 duration) + external + view + returns (bool increaseCardinalityRequired, uint16 cardinalityRequired, bool oldestObservationSatisfied); +} diff --git a/src/strategies/pendle/PendleBalancerCompounder.sol b/src/strategies/pendle/PendleBalancerCompounder.sol new file mode 100644 index 00000000..35918e58 --- /dev/null +++ b/src/strategies/pendle/PendleBalancerCompounder.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 + +pragma solidity ^0.8.15; + +import {PendleDepositor, IERC20} from "./PendleDepositor.sol"; +import {BaseBalancerCompounder, TradePath} from "../../peripheral/BaseBalancerCompounder.sol"; + +/** + * @title ERC4626 Pendle Protocol Vault Adapter + * @author ADN + * @notice ERC4626 wrapper for Pendle protocol + * + * An ERC4626 compliant Wrapper for Pendle Protocol. + * Implements harvest func that swaps via balancer + */ +contract PendleBalancerCompounder is PendleDepositor, BaseBalancerCompounder { + /*////////////////////////////////////////////////////////////// + INITIALIZATION + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Initialize a new Strategy. + * @param asset_ The underlying asset used for deposit/withdraw and accounting + * @param owner_ Owner of the contract. Controls management functions. + * @param autoDeposit_ Controls if `protocolDeposit` gets called on deposit + * @param strategyInitData_ Encoded data for this specific strategy + */ + function initialize(address asset_, address owner_, bool autoDeposit_, bytes memory strategyInitData_) + external + virtual + override + initializer + { + __PendleBase_init(asset_, owner_, autoDeposit_, strategyInitData_); + } + + /*////////////////////////////////////////////////////////////// + REWARDS LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice The token rewarded from the pendle market + function rewardTokens() external view override returns (address[] memory) { + return _balancerSellTokens; + } + + /*////////////////////////////////////////////////////////////// + HARVEST LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Claim rewards, swaps to asset and add liquidity + */ + function harvest(bytes memory data) external override onlyKeeperOrOwner { + claim(); + + sellRewardsViaBalancer(); + + _protocolDeposit(IERC20(asset()).balanceOf(address(this)), 0, data); + + emit Harvested(); + } + + function setHarvestValues(address newBalancerVault, TradePath[] memory newTradePaths) external onlyOwner { + setBalancerTradeValues(newBalancerVault, newTradePaths); + } +} diff --git a/src/strategies/pendle/PendleBalancerCurveCompounder.sol b/src/strategies/pendle/PendleBalancerCurveCompounder.sol new file mode 100644 index 00000000..0fa7d12a --- /dev/null +++ b/src/strategies/pendle/PendleBalancerCurveCompounder.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 + +pragma solidity ^0.8.15; + +import {IPendleMarket} from "./IPendle.sol"; +import {PendleDepositor, IERC20} from "./PendleDepositor.sol"; +import {BaseBalancerCompounder, TradePath} from "../../peripheral/BaseBalancerCompounder.sol"; +import {BaseCurveCompounder, CurveSwap} from "../../peripheral/BaseCurveCompounder.sol"; + +/** + * @title ERC4626 Pendle Protocol Vault Adapter + * @author ADN + * @notice ERC4626 wrapper for Pendle protocol + * + * An ERC4626 compliant Wrapper for Pendle Protocol. + * Implements harvest func that swaps via balancer and curve + */ +contract PendleBalancerCurveCompounder is PendleDepositor, BaseBalancerCompounder, BaseCurveCompounder { + /*////////////////////////////////////////////////////////////// + INITIALIZATION + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Initialize a new Strategy. + * @param asset_ The underlying asset used for deposit/withdraw and accounting + * @param owner_ Owner of the contract. Controls management functions. + * @param autoDeposit_ Controls if `protocolDeposit` gets called on deposit + * @param strategyInitData_ Encoded data for this specific strategy + */ + function initialize(address asset_, address owner_, bool autoDeposit_, bytes memory strategyInitData_) + external + virtual + override + initializer + { + __PendleBase_init(asset_, owner_, autoDeposit_, strategyInitData_); + } + + /*////////////////////////////////////////////////////////////// + REWARDS LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice The token rewarded from the pendle market + function rewardTokens() external view override returns (address[] memory) { + return _balancerSellTokens; + } + + /*////////////////////////////////////////////////////////////// + HARVESGT LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Claim rewards, swaps to asset and add liquidity + */ + function harvest(bytes memory data) external override onlyKeeperOrOwner { + claim(); + + sellRewardsViaBalancer(); + sellRewardsViaCurve(); + + _protocolDeposit(IERC20(asset()).balanceOf(address(this)), 0, data); + + emit Harvested(); + } + + function setHarvestValues( + address newBalancerVault, + TradePath[] memory newTradePaths, + address newCurveRouter, + CurveSwap[] memory newCurveSwaps + ) external onlyOwner { + setBalancerTradeValues(newBalancerVault, newTradePaths); + setCurveTradeValues(newCurveRouter, newCurveSwaps); + } +} diff --git a/src/strategies/pendle/PendleDepositor.sol b/src/strategies/pendle/PendleDepositor.sol new file mode 100644 index 00000000..89ce57f9 --- /dev/null +++ b/src/strategies/pendle/PendleDepositor.sol @@ -0,0 +1,281 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 + +pragma solidity ^0.8.15; + +import {BaseStrategy, IERC20, IERC20Metadata, SafeERC20, ERC20, Math} from "../BaseStrategy.sol"; +import {IPendleRouter, IPendleRouterStatic, IPendleMarket, IPendleSYToken, ISYTokenV3, ApproxParams, LimitOrderData, TokenInput, TokenOutput, SwapData} from "./IPendle.sol"; + +/** + * @title ERC4626 Pendle Protocol Vault Adapter + * @author ADN + * @notice ERC4626 wrapper for Pendle protocol + * + * An ERC4626 compliant Wrapper for Pendle Protocol. + */ +contract PendleDepositor is BaseStrategy { + using SafeERC20 for IERC20; + using Math for uint256; + + string internal _name; + string internal _symbol; + + IPendleRouter public pendleRouter; + IPendleRouterStatic public pendleRouterStatic; + IPendleMarket public pendleMarket; + address public pendleSYToken; + + /*////////////////////////////////////////////////////////////// + INITIALIZATION + //////////////////////////////////////////////////////////////*/ + + error InvalidAsset(); + + receive() external payable {} + + /** + * @notice Initialize a new Strategy. + * @param asset_ The underlying asset used for deposit/withdraw and accounting + * @param owner_ Owner of the contract. Controls management functions. + * @param autoDeposit_ Controls if `protocolDeposit` gets called on deposit + * @param strategyInitData_ Encoded data for this specific strategy + */ + function initialize( + address asset_, + address owner_, + bool autoDeposit_, + bytes memory strategyInitData_ + ) external virtual initializer { + __PendleBase_init(asset_, owner_, autoDeposit_, strategyInitData_); + } + + function __PendleBase_init( + address asset_, + address owner_, + bool autoDeposit_, + bytes memory strategyInitData_ + ) internal onlyInitializing { + __BaseStrategy_init(asset_, owner_, autoDeposit_); + + _name = string.concat( + "VaultCraft Pendle ", + IERC20Metadata(asset_).name(), + " Adapter" + ); + _symbol = string.concat("vcp-", IERC20Metadata(asset_).symbol()); + + ( + address pendleMarket_, + address pendleRouter_, + address pendleRouterStat_ + ) = abi.decode(strategyInitData_, (address, address, address)); + + pendleRouter = IPendleRouter(pendleRouter_); + pendleMarket = IPendleMarket(pendleMarket_); + pendleRouterStatic = IPendleRouterStatic(pendleRouterStat_); + + (address pendleSYToken_, , ) = IPendleMarket(pendleMarket_) + .readTokens(); + pendleSYToken = pendleSYToken_; + + // make sure base asset and market are compatible + _validateAsset(pendleSYToken_, asset_); + + // approve pendle router + IERC20(asset_).approve(pendleRouter_, type(uint256).max); + + // approve LP token for withdrawal + IERC20(pendleMarket_).approve(pendleRouter_, type(uint256).max); + } + + function name() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _name; + } + + function symbol() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _symbol; + } + + /*////////////////////////////////////////////////////////////// + ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Some pendle markets may have a supply cap, some not + function maxDeposit(address who) public view override returns (uint256) { + if (paused()) return 0; + + ISYTokenV3 syToken = ISYTokenV3(pendleSYToken); + + try syToken.supplyCap() returns (uint256 supplyCap) { + uint256 syCap = supplyCap - syToken.totalSupply(); + + return + syCap.mulDiv( + syToken.exchangeRate(), + (10 ** syToken.decimals()), + Math.Rounding.Floor + ); + } catch { + return super.maxDeposit(who); + } + } + + function _totalAssets() internal view override returns (uint256 t) { + uint256 lpBalance = pendleMarket.balanceOf(address(this)); + + if (lpBalance == 0) { + t = 0; + } else { + (t, , , , , , , ) = pendleRouterStatic + .removeLiquiditySingleTokenStatic( + address(pendleMarket), + lpBalance, + asset() + ); + } + } + + /*////////////////////////////////////////////////////////////// + REWARDS LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice The token rewarded from the pendle market + function rewardTokens() + external + view + virtual + override + returns (address[] memory) + { + return _getRewardTokens(); + } + + /// @notice Claim liquidity mining rewards given that it's active + function claim() internal override returns (bool success) { + try IPendleMarket(pendleMarket).redeemRewards(address(this)) { + success = true; + } catch {} + } + + /*////////////////////////////////////////////////////////////// + INTERNAL HOOKS LOGIC + //////////////////////////////////////////////////////////////*/ + + function _protocolDeposit( + uint256 amount, + uint256, + bytes memory data + ) internal virtual override { + // params suggested by docs + ApproxParams memory approxParams = ApproxParams( + 0, + type(uint256).max, + 0, + 256, + 1e14 + ); + + // Empty structs + LimitOrderData memory limitOrderData; + SwapData memory swapData; + + // caching + address asset = asset(); + + TokenInput memory tokenInput = TokenInput( + asset, + amount, + asset, + address(0), + swapData + ); + pendleRouter.addLiquiditySingleToken( + address(this), + address(pendleMarket), + data.length > 0 ? abi.decode(data, (uint256)) : 0, + approxParams, + tokenInput, + limitOrderData + ); + } + + function _protocolWithdraw( + uint256 amount, + uint256, + bytes memory + ) internal virtual override { + // caching + address asset = asset(); + + // Empty structs + LimitOrderData memory limitOrderData; + SwapData memory swapData; + + TokenOutput memory tokenOutput = TokenOutput( + asset, + amount, + asset, + address(0), + swapData + ); + + pendleRouter.removeLiquiditySingleToken( + address(this), + address(pendleMarket), + amountToLp(amount, _totalAssets()), + tokenOutput, + limitOrderData + ); + } + + function amountToLp( + uint256 amount, + uint256 totAssets + ) internal view returns (uint256 lpAmount) { + uint256 lpBalance = pendleMarket.balanceOf(address(this)); + + amount == totAssets ? lpAmount = lpBalance : lpAmount = lpBalance + .mulDiv(amount, totAssets, Math.Rounding.Ceil); + } + + function _validateAsset(address syToken, address baseAsset) internal view { + // check that vault asset is among the tokens available to mint the SY token + address[] memory validTokens = IPendleSYToken(syToken).getTokensIn(); + bool isValidMarket; + + for (uint256 i = 0; i < validTokens.length; i++) { + if (validTokens[i] == baseAsset) { + isValidMarket = true; + break; + } + } + + if (!isValidMarket) revert InvalidAsset(); + + // and among the tokens to be redeemable from the SY token + validTokens = IPendleSYToken(syToken).getTokensOut(); + isValidMarket = false; + for (uint256 i = 0; i < validTokens.length; i++) { + if (validTokens[i] == baseAsset) { + isValidMarket = true; + break; + } + } + + if (!isValidMarket) revert InvalidAsset(); + } + + function _getRewardTokens() internal view returns (address[] memory) { + return pendleMarket.getRewardTokens(); + } +} diff --git a/src/vaults/MultiStrategyVault.sol b/src/vaults/MultiStrategyVault.sol index 4811f31e..97b8648a 100644 --- a/src/vaults/MultiStrategyVault.sol +++ b/src/vaults/MultiStrategyVault.sol @@ -3,13 +3,7 @@ pragma solidity ^0.8.25; -import { - ERC4626Upgradeable, - IERC20Metadata, - ERC20Upgradeable as ERC20, - IERC4626, - IERC20 -} from "openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; +import {ERC4626Upgradeable, IERC20Metadata, ERC20Upgradeable as ERC20, IERC4626, IERC20} from "openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; import {ReentrancyGuardUpgradeable} from "openzeppelin-contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import {PausableUpgradeable} from "openzeppelin-contracts-upgradeable/utils/PausableUpgradeable.sol"; @@ -21,6 +15,8 @@ struct Allocation { uint256 amount; } +// TODO strategies and withdrawalQueue can be duplicates + /** * @title MultiStrategyVault * @author RedVeil @@ -31,7 +27,12 @@ struct Allocation { * It allows for multiple type of fees which are taken by issuing new vault shares. * Strategies and fees can be changed by the owner after a ragequit time. */ -contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, PausableUpgradeable, OwnedUpgradeable { +contract MultiStrategyVault is + ERC4626Upgradeable, + ReentrancyGuardUpgradeable, + PausableUpgradeable, + OwnedUpgradeable +{ using SafeERC20 for IERC20; using Math for uint256; @@ -70,7 +71,7 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P uint256 depositLimit_, address owner_ ) external initializer { - __Pausable_init(); + __Pausable_init(); __ReentrancyGuard_init(); __ERC4626_init(IERC20Metadata(address(asset_))); __Owned_init(owner_); @@ -143,11 +144,21 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P emit VaultInitialized(contractName, address(asset_)); } - function name() public view override(IERC20Metadata, ERC20) returns (string memory) { + function name() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { return _name; } - function symbol() public view override(IERC20Metadata, ERC20) returns (string memory) { + function symbol() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { return _symbol; } @@ -213,12 +224,12 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P /** * @dev Deposit/mint common workflow. */ - function _deposit(address caller, address receiver, uint256 assets, uint256 shares) - internal - override - nonReentrant - takeFees - { + function _deposit( + address caller, + address receiver, + uint256 assets, + uint256 shares + ) internal override nonReentrant takeFees { if (shares == 0 || assets == 0) revert ZeroAmount(); // If _asset is ERC-777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the @@ -228,7 +239,12 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P // Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the // assets are transferred and before the shares are minted, which is a valid state. // slither-disable-next-line reentrancy-no-eth - SafeERC20.safeTransferFrom(IERC20(asset()), caller, address(this), assets); + SafeERC20.safeTransferFrom( + IERC20(asset()), + caller, + address(this), + assets + ); // deposit into default index strategy or leave funds idle if (depositIndex != type(uint256).max) { @@ -243,12 +259,13 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P /** * @dev Withdraw/redeem common workflow. */ - function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares) - internal - override - nonReentrant - takeFees - { + function _withdraw( + address caller, + address receiver, + address owner, + uint256 assets, + uint256 shares + ) internal override nonReentrant takeFees { if (shares == 0 || assets == 0) revert ZeroAmount(); if (caller != owner) { _spendAllowance(owner, caller, shares); @@ -296,10 +313,8 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P ); if (withdrawableAssets >= missing) { - try - strategy.withdraw(missing, address(this), address(this)) - { - break; + try strategy.withdraw(missing, address(this), address(this)) { + break; } catch { emit StrategyWithdrawalFailed(address(strategy), missing); } @@ -313,7 +328,10 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P { float += withdrawableAssets; } catch { - emit StrategyWithdrawalFailed(address(strategy), withdrawableAssets); + emit StrategyWithdrawalFailed( + address(strategy), + withdrawableAssets + ); } } } @@ -328,7 +346,9 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P uint256 assets = IERC20(asset()).balanceOf(address(this)); for (uint8 i; i < strategies.length; i++) { - assets += strategies[i].convertToAssets(strategies[i].balanceOf(address(this))); + assets += strategies[i].convertToAssets( + strategies[i].balanceOf(address(this)) + ); } return assets; } @@ -341,14 +361,18 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P function maxDeposit(address) public view override returns (uint256) { uint256 assets = totalAssets(); uint256 depositLimit_ = depositLimit; - return (paused() || assets >= depositLimit_) ? 0 : depositLimit_ - assets; + return + (paused() || assets >= depositLimit_) ? 0 : depositLimit_ - assets; } /// @return Maximum amount of vault shares that may be minted to given address. Delegates to adapter. function maxMint(address) public view override returns (uint256) { uint256 assets = totalAssets(); uint256 depositLimit_ = depositLimit; - return (paused() || assets >= depositLimit_) ? 0 : convertToShares(depositLimit_ - assets); + return + (paused() || assets >= depositLimit_) + ? 0 + : convertToShares(depositLimit_ - assets); } /*////////////////////////////////////////////////////////////// @@ -396,7 +420,7 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P /** * @notice Sets a new depositIndex. Caller must be Owner. - * @param index The index controls which strategy will be used on user deposits. + * @param index The index controls which strategy will be used on user deposits. * @dev To simply transfer user assets into the vault without using a strategy set the index to `type(uint256).max` */ function setDepositIndex(uint256 index) external onlyOwner { @@ -546,7 +570,10 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P function pushFunds(Allocation[] calldata allocations) external onlyOwner { uint256 len = allocations.length; for (uint256 i; i < len; i++) { - strategies[allocations[i].index].deposit(allocations[i].amount, address(this)); + strategies[allocations[i].index].deposit( + allocations[i].amount, + address(this) + ); } } @@ -558,7 +585,11 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P uint256 len = allocations.length; for (uint256 i; i < len; i++) { if (allocations[i].amount > 0) { - strategies[allocations[i].index].withdraw(allocations[i].amount, address(this), address(this)); + strategies[allocations[i].index].withdraw( + allocations[i].amount, + address(this), + address(this) + ); } } } @@ -570,7 +601,8 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P uint256 public performanceFee; uint256 public highWaterMark; - address public constant FEE_RECIPIENT = address(0x47fd36ABcEeb9954ae9eA1581295Ce9A8308655E); + address public constant FEE_RECIPIENT = + address(0x47fd36ABcEeb9954ae9eA1581295Ce9A8308655E); event PerformanceFeeChanged(uint256 oldFee, uint256 newFee); @@ -587,9 +619,14 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P uint256 shareValue = convertToAssets(1e18); uint256 performanceFee_ = performanceFee; - return performanceFee_ > 0 && shareValue > highWaterMark_ - ? performanceFee_.mulDiv((shareValue - highWaterMark_) * totalSupply(), 1e36, Math.Rounding.Ceil) - : 0; + return + performanceFee_ > 0 && shareValue > highWaterMark_ + ? performanceFee_.mulDiv( + (shareValue - highWaterMark_) * totalSupply(), + 1e36, + Math.Rounding.Ceil + ) + : 0; } /** @@ -667,10 +704,15 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P error PermitDeadlineExpired(uint256 deadline); error InvalidSigner(address signer); - function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) - public - virtual - { + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { if (deadline < block.timestamp) revert PermitDeadlineExpired(deadline); // Unchecked because the only math done is incrementing @@ -709,18 +751,24 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P } function DOMAIN_SEPARATOR() public view returns (bytes32) { - return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator(); + return + block.chainid == INITIAL_CHAIN_ID + ? INITIAL_DOMAIN_SEPARATOR + : computeDomainSeparator(); } function computeDomainSeparator() internal view virtual returns (bytes32) { - return keccak256( - abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - keccak256(bytes(name())), - keccak256("1"), - block.chainid, - address(this) - ) - ); + return + keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes(name())), + keccak256("1"), + block.chainid, + address(this) + ) + ); } } diff --git a/test/Tester.t.sol b/test/Tester.t.sol new file mode 100644 index 00000000..bb074ab7 --- /dev/null +++ b/test/Tester.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2} from "forge-std/Test.sol"; +import {stdJson} from "forge-std/StdJson.sol"; + +import { + ERC4626Upgradeable, + IERC20, + IERC20Metadata, + ERC20Upgradeable as ERC20 +} from "openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; + +interface VaultRouter_I { + function depositAndStake(address vault, address gauge, uint256 assetAmount, address receiver) external; + + function unstakeAndWithdraw(address vault, address gauge, uint256 burnAmount, address receiver) external; +} + +struct BatchSwapStep { + bytes32 poolId; + uint256 assetInIndex; + uint256 assetOutIndex; + uint256 amount; + bytes userData; +} + +struct FixedAddressArray { + address[11] fixed_; +} + +struct FixedUintArray { + uint256[5] swapParams; +} + +interface IAsset {} + +contract Tester is Test { + using stdJson for string; + + VaultRouter_I router = VaultRouter_I(0x4995F3bb85E1381D02699e2164bC1C6c6Fa243cd); + address Vault = address(0x7CEbA0cAeC8CbE74DB35b26D7705BA68Cb38D725); + address adapter = address(0xF6Fe643cb8DCc3E379Cdc6DB88818B09fdF2200d); + IERC20 asset = IERC20(0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7); + + function setUp() public { + vm.selectFork(vm.createFork("mainnet")); + } + + function testA() public { + emit log_uint(bytes("").length); + emit log_address(abi.decode(abi.encode(""), (address))); + } +} diff --git a/test/sample.json b/test/sample.json new file mode 100644 index 00000000..ec967823 --- /dev/null +++ b/test/sample.json @@ -0,0 +1,66 @@ +[ + { + "b": [ + "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512", + "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" + ], + "a": 4, + "batchSwapSteps": [ + { + "amount": 10000, + "assetInIndex": 1, + "assetOutIndex": 2, + "poolId": "0xe7e2c68d3b13d905bbb636709cf4dfd21076b9d20000000000000000000005ca", + "userData": "0x12345" + }, + { + "amount": 20000, + "assetInIndex": 3, + "assetOutIndex": 4, + "poolId": "0xe7e2c68d3b13d905bbb636709cf4dfd21076b9d20000000000000000000004be", + "userData": "0x54321" + } + ], + "assets": [ + "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512", + "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" + ], + "step": { + "a": "0xe7e2c68d3b13d905bbb636709cf4dfd21076b9d20000000000000000000004be", + "b": 3, + "c": 4, + "d": 20000, + "e": "0x54321" + }, + "curveSwap": { + "route": [ + "0xD533a949740bb3306d119CC777fa900bA034cd52", + "0x4eBdF703948ddCEA3B11f675B4D1Fba9d2414A14", + "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000" + ], + "swapParams": [ + [2, 0, 2, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 4, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0] + ], + "c-pools": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000" + ] + } + } +] diff --git a/test/strategies/aave/AaveV3Depositor.t.sol b/test/strategies/aave/AaveV3Depositor.t.sol new file mode 100644 index 00000000..e42ffc16 --- /dev/null +++ b/test/strategies/aave/AaveV3Depositor.t.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {AaveV3Depositor, IERC20} from "../../../src/strategies/aave/aaveV3/AaveV3Depositor.sol"; +import {BaseStrategyTest, IBaseStrategy, TestConfig, stdJson} from "../BaseStrategyTest.sol"; + +contract AaveV3DepositorTest is BaseStrategyTest { + using stdJson for string; + + function setUp() public { + _setUpBaseTest(0, "./test/strategies/aave/AaveV3DepositorTestConfig.json"); + } + + function _setUpStrategy(string memory json_, string memory index_, TestConfig memory testConfig_) + internal + override + returns (IBaseStrategy) + { + AaveV3Depositor strategy = new AaveV3Depositor(); + + strategy.initialize( + testConfig_.asset, + address(this), + true, + abi.encode(json_.readAddress(string.concat(".configs[", index_, "].specific.aaveDataProvider"))) + ); + + vm.label(json_.readAddress(string.concat(".configs[", index_, "].specific.aToken")), "aToken"); + + return IBaseStrategy(address(strategy)); + } + + function _increasePricePerShare(uint256 amount) internal override { + address aToken = address(AaveV3Depositor(address(strategy)).aToken()); + deal(testConfig.asset, aToken, IERC20(testConfig.asset).balanceOf(aToken) + amount); + } +} diff --git a/test/strategies/aave/AaveV3DepositorTestConfig.json b/test/strategies/aave/AaveV3DepositorTestConfig.json new file mode 100644 index 00000000..0d4d8d93 --- /dev/null +++ b/test/strategies/aave/AaveV3DepositorTestConfig.json @@ -0,0 +1,23 @@ +{ + "length": 1, + "configs": [ + { + "base": { + "asset": "0x5f98805A4E8be255a32880FDeC7F6728C6568bA0", + "blockNumber": 0, + "defaultAmount": 1000000000000000000, + "delta": 10, + "maxDeposit": 1000000000000000000000, + "maxWithdraw": 1000000000000000000000, + "minDeposit": 1000000000000000, + "minWithdraw": 1000000000000000, + "network": "mainnet", + "testId": "AaveV3 Depositor" + }, + "specific": { + "aaveDataProvider": "0x7B4EB56E7CD4b454BA8ff71E4518426369a138a3", + "aToken": "0x3Fe6a295459FAe07DF8A0ceCC36F37160FE86AA9" + } + } + ] +} diff --git a/test/strategies/aura/AuraCompounder.t.sol b/test/strategies/aura/AuraCompounder.t.sol index 39b4179c..cbaac25e 100644 --- a/test/strategies/aura/AuraCompounder.t.sol +++ b/test/strategies/aura/AuraCompounder.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.25; import {AuraCompounder, HarvestValues, TradePath} from "../../../src/strategies/aura/AuraCompounder.sol"; -import {IAsset, BatchSwapStep} from "../../../src/interfaces/external/balancer/IBalancerVault.sol"; +import {IAsset, BatchSwapStep} from "../../../src/interfaces/external/balancer/IBalancer.sol"; import {BaseStrategyTest, IBaseStrategy, TestConfig, stdJson} from "../BaseStrategyTest.sol"; contract AuraCompounderTest is BaseStrategyTest { diff --git a/test/strategies/balancer/BalancerCompounder.t.sol b/test/strategies/balancer/BalancerCompounder.t.sol index a97ed16f..df025a5f 100644 --- a/test/strategies/balancer/BalancerCompounder.t.sol +++ b/test/strategies/balancer/BalancerCompounder.t.sol @@ -9,7 +9,7 @@ import { HarvestValues, TradePath } from "../../../src/strategies/balancer/BalancerCompounder.sol"; -import {IAsset, BatchSwapStep} from "../../../src/interfaces/external/balancer/IBalancerVault.sol"; +import {IAsset, BatchSwapStep} from "../../../src/interfaces/external/balancer/IBalancer.sol"; import {BaseStrategyTest, IBaseStrategy, TestConfig, stdJson} from "../BaseStrategyTest.sol"; contract BalancerCompounderTest is BaseStrategyTest { diff --git a/test/strategies/beefy/BeefyDepositor.t.sol.txt b/test/strategies/beefy/BeefyDepositor.t.sol.txt new file mode 100644 index 00000000..fd4fc8f0 --- /dev/null +++ b/test/strategies/beefy/BeefyDepositor.t.sol.txt @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {BeefyDepositor, IBeefyVault, IERC20} from "../../../src/strategies/beefy/BeefyDepositor.sol"; +import {BaseStrategyTest, IBaseStrategy, TestConfig, stdJson} from "../BaseStrategyTest.sol"; + +contract BeefyDepositorTest is BaseStrategyTest { + using stdJson for string; + + function setUp() public { + _setUpBaseTest( + 0, + "./test/strategies/beefy/BeefyDepositorTestConfig.json" + ); + } + + function _setUpStrategy( + string memory json_, + string memory index_, + TestConfig memory testConfig_ + ) internal override returns (IBaseStrategy) { + BeefyDepositor strategy = new BeefyDepositor(); + + strategy.initialize( + testConfig_.asset, + address(this), + true, + abi.encode( + json_.readAddress( + string.concat(".configs[", index_, "].specific.beefyVault") + ) + ) + ); + + return IBaseStrategy(address(strategy)); + } + + function _increasePricePerShare(uint256 amount) internal override { + IBeefyVault beefyVault = BeefyDepositor(address(strategy)).beefyVault(); + + deal( + testConfig.asset, + address(beefyVault), + IERC20(testConfig.asset).balanceOf(address(beefyVault)) + amount + ); + beefyVault.earn(); + } +} diff --git a/test/strategies/beefy/BeefyDepositorTestConfig.json b/test/strategies/beefy/BeefyDepositorTestConfig.json new file mode 100644 index 00000000..60c0ffab --- /dev/null +++ b/test/strategies/beefy/BeefyDepositorTestConfig.json @@ -0,0 +1,22 @@ +{ + "length": 1, + "configs": [ + { + "base": { + "asset": "0x06325440D014e39736583c165C2963BA99fAf14E", + "blockNumber": 17941201, + "defaultAmount": 1000000000000000000, + "delta": 10, + "maxDeposit": 1000000000000000000000, + "maxWithdraw": 1000000000000000000000, + "minDeposit": 1000000000000000, + "minWithdraw": 1000000000000000, + "network": "mainnet", + "testId": "Beefy Depositor" + }, + "specific": { + "beefyVault": "0xa7739fd3d12ac7F16D8329AF3Ee407e19De10D8D" + } + } + ] +} diff --git a/test/strategies/compound/v2/CompoundV2Depositor.t.sol b/test/strategies/compound/v2/CompoundV2Depositor.t.sol new file mode 100644 index 00000000..d1684990 --- /dev/null +++ b/test/strategies/compound/v2/CompoundV2Depositor.t.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {CompoundV2Depositor, IERC20} from "../../../../src/strategies/compound/v2/CompoundV2Depositor.sol"; +import {BaseStrategyTest, IBaseStrategy, TestConfig, stdJson} from "../../BaseStrategyTest.sol"; + +contract CompoundV2DepositorTest is BaseStrategyTest { + using stdJson for string; + + function setUp() public { + _setUpBaseTest(0, "./test/strategies/compound/v2/CompoundV2DepositorTestConfig.json"); + } + + function _setUpStrategy(string memory json_, string memory index_, TestConfig memory testConfig_) + internal + override + returns (IBaseStrategy) + { + CompoundV2Depositor strategy = new CompoundV2Depositor(); + + strategy.initialize( + testConfig_.asset, + address(this), + true, + abi.encode( + json_.readAddress(string.concat(".configs[", index_, "].specific.cToken")), + json_.readAddress(string.concat(".configs[", index_, "].specific.comptroller")) + ) + ); + + return IBaseStrategy(address(strategy)); + } + + function _increasePricePerShare(uint256 amount) internal override { + address cToken = address(CompoundV2Depositor(address(strategy)).cToken()); + deal(testConfig.asset, cToken, IERC20(testConfig.asset).balanceOf(cToken) + amount); + } + + /*////////////////////////////////////////////////////////////// + OVERRIDEN TESTS + //////////////////////////////////////////////////////////////*/ + + function test__previewWithdraw(uint8 fuzzAmount) public override { + uint256 amount = bound(fuzzAmount, testConfig.minDeposit, testConfig.maxDeposit); + + /// Some strategies have slippage or rounding errors which makes `maWithdraw` lower than the deposit amount + uint256 reqAssets = ((strategy.previewMint(strategy.previewWithdraw(amount))) * 11) / 10; + + _mintAssetAndApproveForStrategy(reqAssets, bob); + + vm.prank(bob); + strategy.deposit(reqAssets, bob); + + prop_previewWithdraw(bob, bob, bob, amount, testConfig.testId); + } + + function test__withdraw_autoDeposit_partial() public override { + strategy.toggleAutoDeposit(); + _mintAssetAndApproveForStrategy(testConfig.defaultAmount, bob); + + vm.prank(bob); + strategy.deposit(testConfig.defaultAmount, bob); + + // Push 40% the funds into the underlying protocol + strategy.pushFunds((testConfig.defaultAmount / 5) * 2, bytes("")); + + // Withdraw 80% of deposit + vm.prank(bob); + strategy.withdraw((testConfig.defaultAmount / 5) * 4, bob, bob); + + assertApproxEqAbs(strategy.totalAssets(), testConfig.defaultAmount / 5, 95491862, "ta"); + assertApproxEqAbs(strategy.totalSupply(), testConfig.defaultAmount / 5, 29141911, "ts"); + assertApproxEqAbs(strategy.balanceOf(bob), testConfig.defaultAmount / 5, 29141911, "share bal"); + assertApproxEqAbs(IERC20(_asset_).balanceOf(bob), (testConfig.defaultAmount / 5) * 4, _delta_, "asset bal"); + assertApproxEqAbs(IERC20(_asset_).balanceOf(address(strategy)), 0, _delta_, "strategy asset bal"); + } + + /// @dev Partially redeem assets directly from strategy and the underlying protocol + function test__redeem_autoDeposit_partial() public override { + strategy.toggleAutoDeposit(); + _mintAssetAndApproveForStrategy(testConfig.defaultAmount, bob); + + vm.prank(bob); + strategy.deposit(testConfig.defaultAmount, bob); + + // Push 40% the funds into the underlying protocol + strategy.pushFunds((testConfig.defaultAmount / 5) * 2, bytes("")); + + // Redeem 80% of deposit + vm.prank(bob); + strategy.redeem((testConfig.defaultAmount / 5) * 4, bob, bob); + + assertApproxEqAbs(strategy.totalAssets(), testConfig.defaultAmount / 5, 192304855, "ta"); + assertApproxEqAbs(strategy.totalSupply(), testConfig.defaultAmount / 5, _delta_, "ts"); + assertApproxEqAbs(strategy.balanceOf(bob), testConfig.defaultAmount / 5, _delta_, "share bal"); + assertApproxEqAbs(IERC20(_asset_).balanceOf(bob), (testConfig.defaultAmount / 5) * 4, 29141911, "asset bal"); + assertApproxEqAbs(IERC20(_asset_).balanceOf(address(strategy)), 0, _delta_, "strategy asset bal"); + } + + function test__pushFunds() public override { + strategy.toggleAutoDeposit(); + _mintAssetAndApproveForStrategy(testConfig.defaultAmount, bob); + + vm.prank(bob); + strategy.deposit(testConfig.defaultAmount, bob); + + uint256 oldTa = strategy.totalAssets(); + uint256 oldTs = strategy.totalSupply(); + + strategy.pushFunds(testConfig.defaultAmount, bytes("")); + + assertApproxEqAbs(strategy.totalAssets(), oldTa, 204774025, "ta"); + assertApproxEqAbs(strategy.totalSupply(), oldTs, _delta_, "ts"); + assertApproxEqAbs(IERC20(_asset_).balanceOf(address(strategy)), 0, _delta_, "strategy asset bal"); + } + + function test__pullFunds() public override { + _mintAssetAndApproveForStrategy(testConfig.defaultAmount, bob); + + vm.prank(bob); + strategy.deposit(testConfig.defaultAmount, bob); + + uint256 oldTa = strategy.totalAssets(); + uint256 oldTs = strategy.totalSupply(); + + strategy.pullFunds(testConfig.defaultAmount, bytes("")); + + assertApproxEqAbs(strategy.totalAssets(), oldTa, 204774025, "ta"); + assertApproxEqAbs(strategy.totalSupply(), oldTs, _delta_, "ts"); + assertApproxEqAbs( + IERC20(_asset_).balanceOf(address(strategy)), testConfig.defaultAmount, _delta_, "strategy asset bal" + ); + } + + // @dev Slippage on unpausing is higher than the delta for all other interactions + function test__unpause() public override { + _mintAssetAndApproveForStrategy(testConfig.defaultAmount * 3, bob); + + vm.prank(bob); + strategy.deposit(testConfig.defaultAmount * 3, bob); + + uint256 oldTotalAssets = strategy.totalAssets(); + + vm.prank(address(this)); + strategy.pause(); + + vm.prank(address(this)); + strategy.unpause(); + + // We simply deposit back into the external protocol + // TotalAssets shouldnt change significantly besides some slippage or rounding errors + assertApproxEqAbs(oldTotalAssets, strategy.totalAssets(), 1e8 * 3, "totalAssets"); + assertApproxEqAbs(IERC20(testConfig.asset).balanceOf(address(strategy)), 0, testConfig.delta, "asset balance"); + } +} diff --git a/test/strategies/compound/v2/CompoundV2DepositorTestConfig.json b/test/strategies/compound/v2/CompoundV2DepositorTestConfig.json new file mode 100644 index 00000000..14b7709a --- /dev/null +++ b/test/strategies/compound/v2/CompoundV2DepositorTestConfig.json @@ -0,0 +1,23 @@ +{ + "length": 1, + "configs": [ + { + "base": { + "asset": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "blockNumber": 19138838, + "defaultAmount": 1000000000000000000, + "delta": 10, + "maxDeposit": 1000000000000000000000, + "maxWithdraw": 1000000000000000000000, + "minDeposit": 1000000000000000, + "minWithdraw": 1000000000000000, + "network": "mainnet", + "testId": "CompoundV2 Depositor" + }, + "specific": { + "cToken": "0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643", + "comptroller": "0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B" + } + } + ] +} diff --git a/test/strategies/compound/v3/CompoundV3Depositor.t.sol b/test/strategies/compound/v3/CompoundV3Depositor.t.sol new file mode 100644 index 00000000..5d6c82d3 --- /dev/null +++ b/test/strategies/compound/v3/CompoundV3Depositor.t.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {CompoundV3Depositor, IERC20} from "../../../../src/strategies/compound/v3/CompoundV3Depositor.sol"; +import {BaseStrategyTest, IBaseStrategy, TestConfig, stdJson} from "../../BaseStrategyTest.sol"; + +contract CompoundV3DepositorTest is BaseStrategyTest { + using stdJson for string; + + function setUp() public { + _setUpBaseTest(0, "./test/strategies/compound/v3/CompoundV3DepositorTestConfig.json"); + } + + function _setUpStrategy(string memory json_, string memory index_, TestConfig memory testConfig_) + internal + override + returns (IBaseStrategy) + { + CompoundV3Depositor strategy = new CompoundV3Depositor(); + + strategy.initialize( + testConfig_.asset, + address(this), + true, + abi.encode(json_.readAddress(string.concat(".configs[", index_, "].specific.cToken"))) + ); + + return IBaseStrategy(address(strategy)); + } + + function _increasePricePerShare(uint256 amount) internal override { + address cToken = address(CompoundV3Depositor(address(strategy)).cToken()); + _mintAsset(IERC20(testConfig.asset).balanceOf(cToken) + amount, cToken); + } +} diff --git a/test/strategies/compound/v3/CompoundV3DepositorTestConfig.json b/test/strategies/compound/v3/CompoundV3DepositorTestConfig.json new file mode 100644 index 00000000..8702fa1d --- /dev/null +++ b/test/strategies/compound/v3/CompoundV3DepositorTestConfig.json @@ -0,0 +1,22 @@ +{ + "length": 1, + "configs": [ + { + "base": { + "asset": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "blockNumber": 0, + "defaultAmount": 1000000, + "delta": 10, + "maxDeposit": 100000000, + "maxWithdraw": 100000000, + "minDeposit": 10000, + "minWithdraw": 10000, + "network": "mainnet", + "testId": "CompoundV3 Depositor" + }, + "specific": { + "cToken": "0xc3d688B66703497DAA19211EEdff47f25384cdc3" + } + } + ] +} diff --git a/test/strategies/peapods/PeapodsBalancerUniCompounder.json b/test/strategies/peapods/PeapodsBalancerUniCompounder.json new file mode 100644 index 00000000..002fc4d2 --- /dev/null +++ b/test/strategies/peapods/PeapodsBalancerUniCompounder.json @@ -0,0 +1,83 @@ +{ + "length": 1, + "configs": [ + { + "base": { + "asset": "0x473aA22927ACcAD56303019f75C1b3C735f79fd5", + "blockNumber": 19092311, + "defaultAmount": 1000000000000000000, + "delta": 10000000000000000, + "maxDeposit": 1000000000000000000000, + "maxWithdraw": 1000000000000000000000, + "minDeposit": 1000000000000000, + "minWithdraw": 1000000000000000, + "network": "mainnet", + "testId": "Peapods Uniswap Compounder - pDAI" + }, + "specific": { + "init": { + "stakingContract": "0x4D57ad8FB14311e1Fc4b3fcaC62129506FF373b1" + }, + "harvest": { + "uniswap":{ + "uniswapRouter": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + "rewTokens": [ + "0x02f92800F57BCD74066F5709F1Daa1A4302Df875" + ], + "tradePaths": [ + { + "length": "5", + "path": [ + "0x02f92800F57BCD74066F5709F1Daa1A4302Df875", + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + ] + } + ] + }, + "balancer": { + "balancerVault": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", + "harvestValues": { + "amountsInLen": 2, + "depositAsset": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "indexIn": 1, + "indexInUserData": 1, + "poolId": "0xa6f548df93de924d73be7d25dc02554c6bd66db500020000000000000000000e", + "underlyings": [ + "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + ] + }, + "tradePaths": { + "length": 1, + "structs": [ + { + "assets": [ + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599" + ], + "limits": [ + 57896044618658097711785492504343953926634992332820282019728792003956564819967, + 57896044618658097711785492504343953926634992332820282019728792003956564819967 + ], + "swaps": [ + { + "a-poolId": "0xa6f548df93de924d73be7d25dc02554c6bd66db500020000000000000000000e", + "b-assetInIndex": 0, + "c-assetOutIndex": 1, + "d-amount": 0, + "e-userData": "" + } + ] + } + ] + } + } + } + } + } + ] + } + \ No newline at end of file diff --git a/test/strategies/peapods/PeapodsBalancerUniCompounder.t.sol b/test/strategies/peapods/PeapodsBalancerUniCompounder.t.sol new file mode 100644 index 00000000..465ab08c --- /dev/null +++ b/test/strategies/peapods/PeapodsBalancerUniCompounder.t.sol @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 + +pragma solidity ^0.8.15; + +import {Test} from "forge-std/Test.sol"; + +import {PeapodsDepositorBalancerUniV2Compounder, SwapStep} from "../../../src/strategies/peapods/PeapodsBalancerUniV2Compounder.sol"; +import { + BalancerCompounder, + IERC20, + HarvestValues, + TradePath +} from "../../../src/strategies/balancer/BalancerCompounder.sol"; +import {IAsset, BatchSwapStep} from "../../../src/peripheral/BalancerTradeLibrary.sol"; +import {IStakedToken} from "../../../src/strategies/peapods/PeapodsStrategy.sol"; +import {BaseStrategyTest, IBaseStrategy, TestConfig, stdJson, Math} from "../BaseStrategyTest.sol"; +import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "openzeppelin-contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +contract PeapodsUniswapV2CompounderTest is BaseStrategyTest { + using stdJson for string; + using Math for uint256; + + address asset; + address stakingContract; + + function setUp() public { + _setUpBaseTest(0, "./test/strategies/peapods/PeapodsBalancerUniCompounder.json"); + } + + function _setUpStrategy(string memory json_, string memory index_, TestConfig memory testConfig_) + internal + override + returns (IBaseStrategy) + { + // Read strategy init values + stakingContract = json_.readAddress(string.concat(".configs[", index_, "].specific.init.stakingContract")); + + // Deploy Strategy + PeapodsDepositorBalancerUniV2Compounder strategy = new PeapodsDepositorBalancerUniV2Compounder(); + + strategy.initialize( + testConfig_.asset, + address(this), + true, + abi.encode(stakingContract) + ); + + // Set Harvest values - uniswap + address uniswapRouter = json_.readAddress(string.concat(".configs[", index_, "].specific.harvest.uniswap.uniswapRouter")); + + // assets to buy with rewards and to add to liquidity + address[] memory rewToken = new address[](1); + rewToken[0] = json_.readAddress( + string.concat(".configs[", index_, "].specific.harvest.uniswap.rewTokens[0]") + ); + + // set Uniswap trade paths + SwapStep[] memory swaps = new SwapStep[](1); + + uint256 lenSwap0 = json_.readUint( + string.concat(".configs[", index_, "].specific.harvest.uniswap.tradePaths[0].length") + ); + address[] memory swap0 = new address[](lenSwap0); // PEAS - WETH + for(uint256 i=0; i