diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6c33e4bc..a04ee8bc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,5 +35,5 @@ jobs: - name: Run foundry fuzzing run: FOUNDRY_PROFILE=ci_fuzz forge test -vv - # - name: Run foundry coverage - # run: FOUNDRY_PROFILE=coverage forge coverage --report summary \ No newline at end of file + - name: Run foundry coverage + run: FOUNDRY_PROFILE=coverage forge coverage --report summary \ No newline at end of file diff --git a/.gitignore b/.gitignore index 85198aaa..565f4937 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ docs/ # Dotenv file .env + +# coverage +lcov.info +coverage \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 0e9f83a5..1d3068ab 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,4 @@ [submodule "lib/euler-vault-kit"] path = lib/euler-vault-kit url = https://github.com/euler-xyz/euler-vault-kit + diff --git a/coverage.sh b/coverage.sh new file mode 100755 index 00000000..5eca127e --- /dev/null +++ b/coverage.sh @@ -0,0 +1,36 @@ +# Make sure this script is executable. On Unix based systems, run "chmod +x coverage.sh". +# To run this script, type "./coverage.sh". + +#!/bin/bash + +set -e # exit on error + +# check if lcov is installed, if not, execute installation +if ! command -v lcov &>/dev/null; then + echo "lcov is not installed. Installing..." + # check if its macos or linux. + if [ "$(uname)" == "Darwin" ]; then + brew install lcov + + else + sudo apt-get install lcov + fi +fi +lcov --version + +# generates lcov.info +FOUNDRY_PROFILE=coverage forge coverage --report lcov + +# Generate summary +lcov \ + --rc branch_coverage=1 \ + --ignore-errors inconsistent \ + --list lcov.info + +# Open more granular breakdown in browser +genhtml \ + --ignore-errors category \ + --rc branch_coverage=1 \ + --output-directory coverage \ + lcov.info +open coverage/index.html \ No newline at end of file diff --git a/lib/ethereum-vault-connector b/lib/ethereum-vault-connector index 9bc0c17a..f791f94e 160000 --- a/lib/ethereum-vault-connector +++ b/lib/ethereum-vault-connector @@ -1 +1 @@ -Subproject commit 9bc0c17afd6bed51cd9126d9f3b8fc31614d29a9 +Subproject commit f791f94e6e790dd82041908983b57412dc04fb84 diff --git a/lib/euler-vault-kit b/lib/euler-vault-kit index e7577002..8d8aec00 160000 --- a/lib/euler-vault-kit +++ b/lib/euler-vault-kit @@ -1 +1 @@ -Subproject commit e75770023e1b432a660828120cc166b7dc64a222 +Subproject commit 8d8aec00723daa1cbb723d6f213944199d4bc26b diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index df9ee8b0..00000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console} from "forge-std/Script.sol"; - -contract CounterScript is Script { - function setUp() public {} - - function run() public { - vm.broadcast(); - } -} diff --git a/src/FourSixTwoSixAgg.sol b/src/FourSixTwoSixAgg.sol index 4074a31a..6ebeac5a 100644 --- a/src/FourSixTwoSixAgg.sol +++ b/src/FourSixTwoSixAgg.sol @@ -139,6 +139,134 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { _setRoleAdmin(STRATEGY_REMOVER_ROLE, STRATEGY_REMOVER_ROLE_ADMIN_ROLE); } + /// @notice Rebalance strategy allocation. + /// @dev This function will first harvest yield, gulps and update interest. + /// @dev If current allocation is greater than target allocation, the aggregator will withdraw the excess assets. + /// If current allocation is less than target allocation, the aggregator will: + /// - Try to deposit the delta, if the cash is not sufficient, deposit all the available cash + /// - If all the available cash is greater than the max deposit, deposit the max deposit + function rebalance(address strategy) external nonReentrant { + _rebalance(strategy); + } + + function rebalanceMultipleStrategies(address[] calldata _strategies) external nonReentrant { + for (uint256 i; i < _strategies.length; ++i) { + _rebalance(_strategies[i]); + } + } + + /// @notice Harvest positive yield. + /// @param strategy address of strategy + function harvest(address strategy) external nonReentrant { + _harvest(strategy); + + _gulp(); + } + + function harvestMultipleStrategies(address[] calldata _strategies) external nonReentrant { + for (uint256 i; i < _strategies.length; ++i) { + _harvest(_strategies[i]); + + _gulp(); + } + } + + /// @notice Adjust a certain strategy's allocation points. + /// @dev Can only be called by an address that have the ALLOCATION_ADJUSTER_ROLE + /// @param strategy address of strategy + /// @param newPoints new strategy's points + function adjustAllocationPoints(address strategy, uint256 newPoints) + external + nonReentrant + onlyRole(ALLOCATION_ADJUSTER_ROLE) + { + Strategy memory strategyDataCache = strategies[strategy]; + uint256 totalAllocationPointsCache = totalAllocationPoints; + + if (!strategyDataCache.active) { + revert InactiveStrategy(); + } + + strategies[strategy].allocationPoints = uint120(newPoints); + + totalAllocationPoints = (newPoints > strategyDataCache.allocationPoints) + ? totalAllocationPointsCache + (newPoints - strategyDataCache.allocationPoints) + : totalAllocationPointsCache - (strategyDataCache.allocationPoints - newPoints); + } + + /// @notice Swap two strategies indexes in the withdrawal queue. + /// @dev Can only be called by an address that have the WITHDRAW_QUEUE_REORDERER_ROLE. + /// @param index1 index of first strategy + /// @param index2 index of second strategy + function reorderWithdrawalQueue(uint8 index1, uint8 index2) + external + nonReentrant + onlyRole(WITHDRAW_QUEUE_REORDERER_ROLE) + { + if (index1 >= withdrawalQueue.length || index2 >= withdrawalQueue.length) { + revert OutOfBounds(); + } + + if (index1 == index2) { + revert SameIndexes(); + } + + (withdrawalQueue[index1], withdrawalQueue[index2]) = (withdrawalQueue[index2], withdrawalQueue[index1]); + } + + /// @notice Add new strategy with it's allocation points. + /// @dev Can only be called by an address that have STRATEGY_ADDER_ROLE. + /// @param strategy Address of the strategy + /// @param allocationPoints Strategy's allocation points + function addStrategy(address strategy, uint256 allocationPoints) + external + nonReentrant + onlyRole(STRATEGY_ADDER_ROLE) + { + if (IERC4626(strategy).asset() != asset()) { + revert InvalidStrategyAsset(); + } + + if (strategies[strategy].active) { + revert StrategyAlreadyExist(); + } + + strategies[strategy] = Strategy({allocated: 0, allocationPoints: uint120(allocationPoints), active: true}); + + totalAllocationPoints += allocationPoints; + withdrawalQueue.push(strategy); + } + + /// @notice Remove strategy and set its allocation points to zero. + /// @dev This function does not pull funds, `harvest()` needs to be called to withdraw + /// @dev Can only be called by an address that have the STRATEGY_REMOVER_ROLE + /// @param strategy Address of the strategy + function removeStrategy(address strategy) external nonReentrant onlyRole(STRATEGY_REMOVER_ROLE) { + if (!strategies[strategy].active) { + revert AlreadyRemoved(); + } + + strategies[strategy].active = false; + totalAllocationPoints -= strategies[strategy].allocationPoints; + strategies[strategy].allocationPoints = 0; + + // remove from withdrawalQueue + uint256 lastStrategyIndex = withdrawalQueue.length - 1; + + for (uint256 i = 0; i <= lastStrategyIndex; i++) { + if ((withdrawalQueue[i] == strategy) && (i != lastStrategyIndex)) { + (withdrawalQueue[i], withdrawalQueue[lastStrategyIndex]) = + (withdrawalQueue[lastStrategyIndex], withdrawalQueue[i]); + } + } + + withdrawalQueue.pop(); + } + + function gulp() external nonReentrant { + _gulp(); + } + /// @notice Get strategy params. /// @param _strategy strategy's address /// @return Strategy struct @@ -152,17 +280,16 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { return withdrawalQueue.length; } - /// @notice Return the total amount of assets deposited, plus the accrued interest. - /// @return uint256 total amount - function totalAssets() public view override returns (uint256) { - return totalAssetsDeposited + interestAccrued(); + /// @notice Return the ESRSlot struct + /// @return ESRSlot struct + function getESRSlot() external view returns (ESRSlot memory) { + return esrSlot; } - /// @notice get the total assets allocatable - /// @dev the total assets allocatable is the amount of assets deposited into the aggregator + assets already deposited into strategies - /// @return uint256 total assets - function totalAssetsAllocatable() public view returns (uint256) { - return IERC20(asset()).balanceOf(address(this)) + totalAllocated; + /// @notice Return the accrued interest + /// @return uint256 accrued interest + function interestAccrued() external view returns (uint256) { + return interestAccruedFromCache(esrSlot); } /// @notice Transfers a certain amount of tokens to a recipient. @@ -232,6 +359,34 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { return super.redeem(shares, receiver, owner); } + function updateInterestAndReturnESRSlotCache() public returns (ESRSlot memory) { + ESRSlot memory esrSlotCache = esrSlot; + uint256 accruedInterest = interestAccruedFromCache(esrSlotCache); + + // it's safe to down-cast because the accrued interest is a fraction of interest left + esrSlotCache.interestLeft -= uint168(accruedInterest); + esrSlotCache.lastInterestUpdate = uint40(block.timestamp); + // write esrSlotCache back to storage in a single SSTORE + esrSlot = esrSlotCache; + // Move interest accrued to totalAssetsDeposited + totalAssetsDeposited += accruedInterest; + + return esrSlotCache; + } + + /// @notice Return the total amount of assets deposited, plus the accrued interest. + /// @return uint256 total amount + function totalAssets() public view override returns (uint256) { + return totalAssetsDeposited + interestAccruedFromCache(esrSlot); + } + + /// @notice get the total assets allocatable + /// @dev the total assets allocatable is the amount of assets deposited into the aggregator + assets already deposited into strategies + /// @return uint256 total assets + function totalAssetsAllocatable() public view returns (uint256) { + return IERC20(asset()).balanceOf(address(this)) + totalAllocated; + } + /// @dev Increate the total assets deposited, and call IERC4626._deposit() /// @dev See {IERC4626-_deposit}. function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal override { @@ -286,10 +441,6 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { super._withdraw(caller, receiver, owner, assets, shares); } - function gulp() public nonReentrant { - _gulp(); - } - function _gulp() internal { ESRSlot memory esrSlotCache = updateInterestAndReturnESRSlotCache(); uint256 toGulp = totalAssetsAllocatable() - totalAssetsDeposited - esrSlotCache.interestLeft; @@ -304,37 +455,6 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { esrSlot = esrSlotCache; } - function updateInterestAndReturnESRSlotCache() public returns (ESRSlot memory) { - ESRSlot memory esrSlotCache = esrSlot; - uint256 accruedInterest = interestAccruedFromCache(esrSlotCache); - - // it's safe to down-cast because the accrued interest is a fraction of interest left - esrSlotCache.interestLeft -= uint168(accruedInterest); - esrSlotCache.lastInterestUpdate = uint40(block.timestamp); - // write esrSlotCache back to storage in a single SSTORE - esrSlot = esrSlotCache; - // Move interest accrued to totalAssetsDeposited - totalAssetsDeposited += accruedInterest; - - return esrSlotCache; - } - - function rebalanceMultipleStrategies(address[] calldata _strategies) external nonReentrant { - for (uint256 i; i < _strategies.length; ++i) { - _rebalance(_strategies[i]); - } - } - - /// @notice Rebalance strategy allocation. - /// @dev This function will first harvest yield, gulps and update interest. - /// @dev If current allocation is greater than target allocation, the aggregator will withdraw the excess assets. - /// If current allocation is less than target allocation, the aggregator will: - /// - Try to deposit the delta, if the cash is not sufficient, deposit all the available cash - /// - If all the available cash is greater than the max deposit, deposit the max deposit - function rebalance(address strategy) public nonReentrant { - _rebalance(strategy); - } - function _rebalance(address _strategy) internal { if (_strategy == address(0)) { return; //nothing to rebalance as this is the cash reserve @@ -395,22 +515,6 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { } } - /// @notice Harvest positive yield. - /// @param strategy address of strategy - function harvest(address strategy) public nonReentrant { - _harvest(strategy); - - _gulp(); - } - - function harvestMultipleStrategies(address[] calldata _strategies) external nonReentrant { - for (uint256 i; i < _strategies.length; ++i) { - _harvest(_strategies[i]); - - _gulp(); - } - } - function _harvest(address strategy) internal { Strategy memory strategyData = strategies[strategy]; @@ -433,105 +537,6 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { } } - /// @notice Adjust a certain strategy's allocation points. - /// @dev Can only be called by an address that have the ALLOCATION_ADJUSTER_ROLE - /// @param strategy address of strategy - /// @param newPoints new strategy's points - function adjustAllocationPoints(address strategy, uint256 newPoints) - public - nonReentrant - onlyRole(ALLOCATION_ADJUSTER_ROLE) - { - Strategy memory strategyDataCache = strategies[strategy]; - uint256 totalAllocationPointsCache = totalAllocationPoints; - - if (!strategyDataCache.active) { - revert InactiveStrategy(); - } - - strategies[strategy].allocationPoints = uint120(newPoints); - - totalAllocationPoints = (newPoints > strategyDataCache.allocationPoints) - ? totalAllocationPointsCache + (newPoints - strategyDataCache.allocationPoints) - : totalAllocationPointsCache - (strategyDataCache.allocationPoints - newPoints); - } - - /// @notice Swap two strategies indexes in the withdrawal queue. - /// @dev Can only be called by an address that have the WITHDRAW_QUEUE_REORDERER_ROLE. - /// @param index1 index of first strategy - /// @param index2 index of second strategy - function reorderWithdrawalQueue(uint8 index1, uint8 index2) - public - nonReentrant - onlyRole(WITHDRAW_QUEUE_REORDERER_ROLE) - { - if (index1 >= withdrawalQueue.length || index2 >= withdrawalQueue.length) { - revert OutOfBounds(); - } - - if (index1 == index2) { - revert SameIndexes(); - } - - address temp = withdrawalQueue[index1]; - withdrawalQueue[index1] = withdrawalQueue[index2]; - withdrawalQueue[index2] = temp; - } - - /// @notice Add new strategy with it's allocation points. - /// @dev Can only be called by an address that have STRATEGY_ADDER_ROLE. - /// @param strategy Address of the strategy - /// @param allocationPoints Strategy's allocation points - function addStrategy(address strategy, uint256 allocationPoints) - public - nonReentrant - onlyRole(STRATEGY_ADDER_ROLE) - { - if (IERC4626(strategy).asset() != asset()) { - revert InvalidStrategyAsset(); - } - - if (strategies[strategy].active) { - revert StrategyAlreadyExist(); - } - - strategies[strategy] = Strategy({allocated: 0, allocationPoints: uint120(allocationPoints), active: true}); - - totalAllocationPoints += allocationPoints; - withdrawalQueue.push(strategy); - } - - /// @notice Remove strategy and set its allocation points to zero. - /// @dev This function does not pull funds, `harvest()` needs to be called to withdraw - /// @dev Can only be called by an address that have the STRATEGY_REMOVER_ROLE - /// @param strategy Address of the strategy - function removeStrategy(address strategy) public nonReentrant onlyRole(STRATEGY_REMOVER_ROLE) { - if (!strategies[strategy].active) { - revert AlreadyRemoved(); - } - - strategies[strategy].active = false; - totalAllocationPoints -= strategies[strategy].allocationPoints; - strategies[strategy].allocationPoints = 0; - - // remove from withdrawalQueue - uint256 lastStrategyIndex = withdrawalQueue.length - 1; - for (uint256 i; i <= lastStrategyIndex; ++i) { - if ((withdrawalQueue[i] == strategy) && (i != lastStrategyIndex)) { - (withdrawalQueue[i], withdrawalQueue[lastStrategyIndex]) = - (withdrawalQueue[lastStrategyIndex], withdrawalQueue[i]); - } - - withdrawalQueue.pop(); - } - } - - /// @notice Return the accrued interest - /// @return uint256 accrued interest - function interestAccrued() public view returns (uint256) { - return interestAccruedFromCache(esrSlot); - } - /// @dev Get accrued interest without updating it. /// @param esrSlotCache Cached esrSlot /// @return uint256 accrued interest @@ -553,12 +558,6 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { return esrSlotCache.interestLeft * timePassed / totalDuration; } - /// @notice Return the ESRSlot struct - /// @return ESRSlot struct - function getESRSlot() public view returns (ESRSlot memory) { - return esrSlot; - } - /// @notice Retrieves the message sender in the context of the EVC. /// @dev This function returns the account on behalf of which the current operation is being performed, which is /// either msg.sender or the account authenticated by the EVC. diff --git a/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol b/test/e2e/DepositRebalanceHarvestWithdrawE2ETest.t.sol similarity index 78% rename from test/e2e/DepositRebalanceWithdrawE2ETest.t.sol rename to test/e2e/DepositRebalanceHarvestWithdrawE2ETest.t.sol index 437b73b6..2da74da6 100644 --- a/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol +++ b/test/e2e/DepositRebalanceHarvestWithdrawE2ETest.t.sol @@ -11,7 +11,7 @@ import { TestERC20 } from "../common/FourSixTwoSixAggBase.t.sol"; -contract DepositRebalanceWithdrawE2ETest is FourSixTwoSixAggBase { +contract DepositRebalanceHarvestWithdrawE2ETest is FourSixTwoSixAggBase { uint256 user1InitialBalance = 100000e18; function setUp() public virtual override { @@ -173,8 +173,14 @@ contract DepositRebalanceWithdrawE2ETest is FourSixTwoSixAggBase { function testMultipleStrategy_WithYield() public { IEVault eTSTsecondary; { - eTSTsecondary = IEVault(coreProductLine.createVault(address(assetTST), address(oracle), unitOfAccount)); + eTSTsecondary = IEVault( + factory.createProxy( + address(0), true, abi.encodePacked(address(assetTST), address(oracle), unitOfAccount) + ) + ); eTSTsecondary.setInterestRateModel(address(new IRMTestDefault())); + eTSTsecondary.setMaxLiquidationDiscount(0.2e4); + eTSTsecondary.setFeeReceiver(feeReceiver); uint256 initialStrategyAllocationPoints = 1000e18; _addStrategy(manager, address(eTSTsecondary), initialStrategyAllocationPoints); @@ -355,8 +361,14 @@ contract DepositRebalanceWithdrawE2ETest is FourSixTwoSixAggBase { function testMultipleStrategy_WithYield_WithInterest() public { IEVault eTSTsecondary; { - eTSTsecondary = IEVault(coreProductLine.createVault(address(assetTST), address(oracle), unitOfAccount)); + eTSTsecondary = IEVault( + factory.createProxy( + address(0), true, abi.encodePacked(address(assetTST), address(oracle), unitOfAccount) + ) + ); eTSTsecondary.setInterestRateModel(address(new IRMTestDefault())); + eTSTsecondary.setMaxLiquidationDiscount(0.2e4); + eTSTsecondary.setFeeReceiver(feeReceiver); uint256 initialStrategyAllocationPoints = 1000e18; _addStrategy(manager, address(eTSTsecondary), initialStrategyAllocationPoints); @@ -476,4 +488,125 @@ contract DepositRebalanceWithdrawE2ETest is FourSixTwoSixAggBase { ); } } + + function testWithdraw_NotEnoughAssets() public { + IEVault eTSTsecondary; + { + eTSTsecondary = IEVault( + factory.createProxy( + address(0), true, abi.encodePacked(address(assetTST), address(oracle), unitOfAccount) + ) + ); + eTSTsecondary.setInterestRateModel(address(new IRMTestDefault())); + eTSTsecondary.setMaxLiquidationDiscount(0.2e4); + eTSTsecondary.setFeeReceiver(feeReceiver); + + uint256 initialStrategyAllocationPoints = 1000e18; + _addStrategy(manager, address(eTSTsecondary), initialStrategyAllocationPoints); + } + + uint256 amountToDeposit = 10000e18; + + // deposit into aggregator + { + uint256 balanceBefore = fourSixTwoSixAgg.balanceOf(user1); + uint256 totalSupplyBefore = fourSixTwoSixAgg.totalSupply(); + uint256 totalAssetsDepositedBefore = fourSixTwoSixAgg.totalAssetsDeposited(); + uint256 userAssetBalanceBefore = assetTST.balanceOf(user1); + + vm.startPrank(user1); + assetTST.approve(address(fourSixTwoSixAgg), amountToDeposit); + fourSixTwoSixAgg.deposit(amountToDeposit, user1); + vm.stopPrank(); + + assertEq(fourSixTwoSixAgg.balanceOf(user1), balanceBefore + amountToDeposit); + assertEq(fourSixTwoSixAgg.totalSupply(), totalSupplyBefore + amountToDeposit); + assertEq(fourSixTwoSixAgg.totalAssetsDeposited(), totalAssetsDepositedBefore + amountToDeposit); + assertEq(assetTST.balanceOf(user1), userAssetBalanceBefore - amountToDeposit); + } + + // rebalance into strategy + // 2500 total points; 1000 for reserve(40%), 500(20%) for eTST, 1000(40%) for eTSTsecondary + // 10k deposited; 4000 for reserve, 2000 for eTST, 4000 for eTSTsecondary + vm.warp(block.timestamp + 86400); + { + FourSixTwoSixAgg.Strategy memory eTSTstrategyBefore = fourSixTwoSixAgg.getStrategy(address(eTST)); + FourSixTwoSixAgg.Strategy memory eTSTsecondarystrategyBefore = + fourSixTwoSixAgg.getStrategy(address(eTSTsecondary)); + + assertEq(eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))), eTSTstrategyBefore.allocated); + assertEq( + eTSTsecondary.convertToAssets(eTSTsecondary.balanceOf(address(fourSixTwoSixAgg))), + eTSTsecondarystrategyBefore.allocated + ); + + uint256 expectedeTSTStrategyCash = fourSixTwoSixAgg.totalAssetsAllocatable() + * eTSTstrategyBefore.allocationPoints / fourSixTwoSixAgg.totalAllocationPoints(); + uint256 expectedeTSTsecondaryStrategyCash = fourSixTwoSixAgg.totalAssetsAllocatable() + * eTSTsecondarystrategyBefore.allocationPoints / fourSixTwoSixAgg.totalAllocationPoints(); + + assertTrue(expectedeTSTStrategyCash != 0); + assertTrue(expectedeTSTsecondaryStrategyCash != 0); + + address[] memory strategiesToRebalance = new address[](2); + strategiesToRebalance[0] = address(eTST); + strategiesToRebalance[1] = address(eTSTsecondary); + vm.prank(user1); + fourSixTwoSixAgg.rebalanceMultipleStrategies(strategiesToRebalance); + + assertEq(fourSixTwoSixAgg.totalAllocated(), expectedeTSTStrategyCash + expectedeTSTsecondaryStrategyCash); + assertEq(eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))), expectedeTSTStrategyCash); + assertEq( + eTSTsecondary.convertToAssets(eTSTsecondary.balanceOf(address(fourSixTwoSixAgg))), + expectedeTSTsecondaryStrategyCash + ); + assertEq((fourSixTwoSixAgg.getStrategy(address(eTST))).allocated, expectedeTSTStrategyCash); + assertEq( + (fourSixTwoSixAgg.getStrategy(address(eTSTsecondary))).allocated, expectedeTSTsecondaryStrategyCash + ); + assertEq( + assetTST.balanceOf(address(fourSixTwoSixAgg)), + amountToDeposit - (expectedeTSTStrategyCash + expectedeTSTsecondaryStrategyCash) + ); + } + + vm.warp(block.timestamp + 86400); + uint256 eTSTYield; + uint256 eTSTsecondaryYield; + { + // mock an increase of aggregator balance due to yield + uint256 aggrCurrenteTSTShareBalance = eTST.balanceOf(address(fourSixTwoSixAgg)); + uint256 aggrCurrenteTSTUnderlyingBalance = eTST.convertToAssets(aggrCurrenteTSTShareBalance); + uint256 aggrCurrenteTSTsecondaryShareBalance = eTSTsecondary.balanceOf(address(fourSixTwoSixAgg)); + uint256 aggrCurrenteTSTsecondaryUnderlyingBalance = + eTST.convertToAssets(aggrCurrenteTSTsecondaryShareBalance); + uint256 aggrNeweTSTUnderlyingBalance = aggrCurrenteTSTUnderlyingBalance * 11e17 / 1e18; + uint256 aggrNeweTSTsecondaryUnderlyingBalance = aggrCurrenteTSTsecondaryUnderlyingBalance * 11e17 / 1e18; + eTSTYield = aggrNeweTSTUnderlyingBalance - aggrCurrenteTSTUnderlyingBalance; + eTSTsecondaryYield = aggrNeweTSTsecondaryUnderlyingBalance - aggrCurrenteTSTsecondaryUnderlyingBalance; + + assetTST.mint(address(eTST), eTSTYield); + assetTST.mint(address(eTSTsecondary), eTSTsecondaryYield); + eTST.skim(type(uint256).max, address(fourSixTwoSixAgg)); + eTSTsecondary.skim(type(uint256).max, address(fourSixTwoSixAgg)); + } + + // harvest + address[] memory strategiesToHarvest = new address[](1); + strategiesToHarvest[0] = address(eTST); + vm.prank(user1); + fourSixTwoSixAgg.harvestMultipleStrategies(strategiesToHarvest); + vm.warp(block.timestamp + 2 weeks); + + vm.prank(manager); + fourSixTwoSixAgg.removeStrategy(address(eTSTsecondary)); + + { + uint256 amountToWithdraw = fourSixTwoSixAgg.balanceOf(user1); + + vm.prank(user1); + vm.expectRevert(FourSixTwoSixAgg.NotEnoughAssets.selector); + fourSixTwoSixAgg.redeem(amountToWithdraw, user1, user1); + } + } } diff --git a/test/unit/RebalanceTest.t.sol b/test/unit/RebalanceTest.t.sol new file mode 100644 index 00000000..c4a8ba97 --- /dev/null +++ b/test/unit/RebalanceTest.t.sol @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import { + FourSixTwoSixAggBase, + FourSixTwoSixAgg, + console2, + EVault, + IEVault, + IRMTestDefault, + TestERC20 +} from "../common/FourSixTwoSixAggBase.t.sol"; + +contract RebalanceTest is FourSixTwoSixAggBase { + uint256 user1InitialBalance = 100000e18; + + function setUp() public virtual override { + super.setUp(); + + uint256 initialStrategyAllocationPoints = 500e18; + _addStrategy(manager, address(eTST), initialStrategyAllocationPoints); + + assetTST.mint(user1, user1InitialBalance); + } + + function testRebalanceByDepositing() public { + uint256 amountToDeposit = 10000e18; + + // deposit into aggregator + { + uint256 balanceBefore = fourSixTwoSixAgg.balanceOf(user1); + uint256 totalSupplyBefore = fourSixTwoSixAgg.totalSupply(); + uint256 totalAssetsDepositedBefore = fourSixTwoSixAgg.totalAssetsDeposited(); + uint256 userAssetBalanceBefore = assetTST.balanceOf(user1); + + vm.startPrank(user1); + assetTST.approve(address(fourSixTwoSixAgg), amountToDeposit); + fourSixTwoSixAgg.deposit(amountToDeposit, user1); + vm.stopPrank(); + + assertEq(fourSixTwoSixAgg.balanceOf(user1), balanceBefore + amountToDeposit); + assertEq(fourSixTwoSixAgg.totalSupply(), totalSupplyBefore + amountToDeposit); + assertEq(fourSixTwoSixAgg.totalAssetsDeposited(), totalAssetsDepositedBefore + amountToDeposit); + assertEq(assetTST.balanceOf(user1), userAssetBalanceBefore - amountToDeposit); + } + + // rebalance into strategy + vm.warp(block.timestamp + 86400); + FourSixTwoSixAgg.Strategy memory strategyBefore = fourSixTwoSixAgg.getStrategy(address(eTST)); + + assertEq(eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))), strategyBefore.allocated); + + uint256 expectedStrategyCash = fourSixTwoSixAgg.totalAssetsAllocatable() * strategyBefore.allocationPoints + / fourSixTwoSixAgg.totalAllocationPoints(); + + vm.prank(user1); + fourSixTwoSixAgg.rebalance(address(eTST)); + + assertEq(fourSixTwoSixAgg.totalAllocated(), expectedStrategyCash); + assertEq(eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))), expectedStrategyCash); + assertEq( + (fourSixTwoSixAgg.getStrategy(address(eTST))).allocated, strategyBefore.allocated + expectedStrategyCash + ); + } + + function testRebalanceByDepositingWhenToDepositIsGreaterThanMaxDeposit() public { + uint256 amountToDeposit = 10000e18; + + // deposit into aggregator + { + uint256 balanceBefore = fourSixTwoSixAgg.balanceOf(user1); + uint256 totalSupplyBefore = fourSixTwoSixAgg.totalSupply(); + uint256 totalAssetsDepositedBefore = fourSixTwoSixAgg.totalAssetsDeposited(); + uint256 userAssetBalanceBefore = assetTST.balanceOf(user1); + + vm.startPrank(user1); + assetTST.approve(address(fourSixTwoSixAgg), amountToDeposit); + fourSixTwoSixAgg.deposit(amountToDeposit, user1); + vm.stopPrank(); + + assertEq(fourSixTwoSixAgg.balanceOf(user1), balanceBefore + amountToDeposit); + assertEq(fourSixTwoSixAgg.totalSupply(), totalSupplyBefore + amountToDeposit); + assertEq(fourSixTwoSixAgg.totalAssetsDeposited(), totalAssetsDepositedBefore + amountToDeposit); + assertEq(assetTST.balanceOf(user1), userAssetBalanceBefore - amountToDeposit); + } + + // rebalance into strategy + vm.warp(block.timestamp + 86400); + FourSixTwoSixAgg.Strategy memory strategyBefore = fourSixTwoSixAgg.getStrategy(address(eTST)); + + assertEq(eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))), strategyBefore.allocated); + + uint256 expectedStrategyCash = fourSixTwoSixAgg.totalAssetsAllocatable() * strategyBefore.allocationPoints + / fourSixTwoSixAgg.totalAllocationPoints(); + uint256 expectedToDeposit = expectedStrategyCash - strategyBefore.allocated; + uint256 eTSTMaxDeposit = expectedToDeposit * 7e17 / 1e18; + // mock max deposit + vm.mockCall( + address(eTST), abi.encodeCall(eTST.maxDeposit, (address(fourSixTwoSixAgg))), abi.encode(eTSTMaxDeposit) + ); + + vm.prank(user1); + fourSixTwoSixAgg.rebalance(address(eTST)); + + assertEq(fourSixTwoSixAgg.totalAllocated(), eTSTMaxDeposit); + assertEq(eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))), eTSTMaxDeposit); + assertEq((fourSixTwoSixAgg.getStrategy(address(eTST))).allocated, strategyBefore.allocated + eTSTMaxDeposit); + } + + function testRebalanceByDepositingWhenToDepositIsGreaterThanCashAvailable() public { + uint256 amountToDeposit = 10000e18; + + // deposit into aggregator + { + uint256 balanceBefore = fourSixTwoSixAgg.balanceOf(user1); + uint256 totalSupplyBefore = fourSixTwoSixAgg.totalSupply(); + uint256 totalAssetsDepositedBefore = fourSixTwoSixAgg.totalAssetsDeposited(); + uint256 userAssetBalanceBefore = assetTST.balanceOf(user1); + + vm.startPrank(user1); + assetTST.approve(address(fourSixTwoSixAgg), amountToDeposit); + fourSixTwoSixAgg.deposit(amountToDeposit, user1); + vm.stopPrank(); + + assertEq(fourSixTwoSixAgg.balanceOf(user1), balanceBefore + amountToDeposit); + assertEq(fourSixTwoSixAgg.totalSupply(), totalSupplyBefore + amountToDeposit); + assertEq(fourSixTwoSixAgg.totalAssetsDeposited(), totalAssetsDepositedBefore + amountToDeposit); + assertEq(assetTST.balanceOf(user1), userAssetBalanceBefore - amountToDeposit); + } + + // rebalance into first strategy + vm.warp(block.timestamp + 86400); + vm.prank(user1); + fourSixTwoSixAgg.rebalance(address(eTST)); + + // create new strategy & add it + IEVault eTSTsecondary; + uint256 eTSTsecondaryAllocationPoints = 1500e18; + { + eTSTsecondary = IEVault( + factory.createProxy( + address(0), true, abi.encodePacked(address(assetTST), address(oracle), unitOfAccount) + ) + ); + _addStrategy(manager, address(eTSTsecondary), eTSTsecondaryAllocationPoints); + } + + // rebalance into eTSTsecondary + vm.warp(block.timestamp + 86400); + { + FourSixTwoSixAgg.Strategy memory strategyBefore = fourSixTwoSixAgg.getStrategy(address(eTSTsecondary)); + + assertEq( + eTSTsecondary.convertToAssets(eTSTsecondary.balanceOf(address(fourSixTwoSixAgg))), + strategyBefore.allocated + ); + + uint256 targetCash = fourSixTwoSixAgg.totalAssetsAllocatable() + * fourSixTwoSixAgg.getStrategy(address(0)).allocationPoints / fourSixTwoSixAgg.totalAllocationPoints(); + uint256 currentCash = fourSixTwoSixAgg.totalAssetsAllocatable() - fourSixTwoSixAgg.totalAllocated(); + uint256 expectedStrategyCash = currentCash - targetCash; + + vm.prank(user1); + fourSixTwoSixAgg.rebalance(address(eTSTsecondary)); + + // assertEq(fourSixTwoSixAgg.totalAllocated(), eTSTsecondaryMaxDeposit); + assertEq( + eTSTsecondary.convertToAssets(eTSTsecondary.balanceOf(address(fourSixTwoSixAgg))), expectedStrategyCash + ); + assertEq( + (fourSixTwoSixAgg.getStrategy(address(eTSTsecondary))).allocated, + strategyBefore.allocated + expectedStrategyCash + ); + } + } + + function testRebalanceByDepositingWhenToDepositIsZero() public { + uint256 amountToDeposit = 10000e18; + + // deposit into aggregator + { + uint256 balanceBefore = fourSixTwoSixAgg.balanceOf(user1); + uint256 totalSupplyBefore = fourSixTwoSixAgg.totalSupply(); + uint256 totalAssetsDepositedBefore = fourSixTwoSixAgg.totalAssetsDeposited(); + uint256 userAssetBalanceBefore = assetTST.balanceOf(user1); + + vm.startPrank(user1); + assetTST.approve(address(fourSixTwoSixAgg), amountToDeposit); + fourSixTwoSixAgg.deposit(amountToDeposit, user1); + vm.stopPrank(); + + assertEq(fourSixTwoSixAgg.balanceOf(user1), balanceBefore + amountToDeposit); + assertEq(fourSixTwoSixAgg.totalSupply(), totalSupplyBefore + amountToDeposit); + assertEq(fourSixTwoSixAgg.totalAssetsDeposited(), totalAssetsDepositedBefore + amountToDeposit); + assertEq(assetTST.balanceOf(user1), userAssetBalanceBefore - amountToDeposit); + } + + // rebalance into strategy + vm.warp(block.timestamp + 86400); + FourSixTwoSixAgg.Strategy memory strategyBefore = fourSixTwoSixAgg.getStrategy(address(eTST)); + + assertEq(eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))), strategyBefore.allocated); + + uint256 eTSTMaxDeposit = 0; + // mock max deposit + vm.mockCall( + address(eTST), abi.encodeCall(eTST.maxDeposit, (address(fourSixTwoSixAgg))), abi.encode(eTSTMaxDeposit) + ); + + vm.prank(user1); + fourSixTwoSixAgg.rebalance(address(eTST)); + + assertEq(fourSixTwoSixAgg.totalAllocated(), strategyBefore.allocated); + assertEq(eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))), strategyBefore.allocated); + assertEq((fourSixTwoSixAgg.getStrategy(address(eTST))).allocated, strategyBefore.allocated); + } + + function testRebalanceByWithdrawing() public { + uint256 amountToDeposit = 10000e18; + + // deposit into aggregator + { + uint256 balanceBefore = fourSixTwoSixAgg.balanceOf(user1); + uint256 totalSupplyBefore = fourSixTwoSixAgg.totalSupply(); + uint256 totalAssetsDepositedBefore = fourSixTwoSixAgg.totalAssetsDeposited(); + uint256 userAssetBalanceBefore = assetTST.balanceOf(user1); + + vm.startPrank(user1); + assetTST.approve(address(fourSixTwoSixAgg), amountToDeposit); + fourSixTwoSixAgg.deposit(amountToDeposit, user1); + vm.stopPrank(); + + assertEq(fourSixTwoSixAgg.balanceOf(user1), balanceBefore + amountToDeposit); + assertEq(fourSixTwoSixAgg.totalSupply(), totalSupplyBefore + amountToDeposit); + assertEq(fourSixTwoSixAgg.totalAssetsDeposited(), totalAssetsDepositedBefore + amountToDeposit); + assertEq(assetTST.balanceOf(user1), userAssetBalanceBefore - amountToDeposit); + } + + // rebalance into strategy + vm.warp(block.timestamp + 86400); + vm.prank(user1); + fourSixTwoSixAgg.rebalance(address(eTST)); + + // decrease allocation points + uint256 newAllocationPoints = 300e18; + vm.prank(manager); + fourSixTwoSixAgg.adjustAllocationPoints(address(eTST), newAllocationPoints); + + vm.warp(block.timestamp + 86400); + + FourSixTwoSixAgg.Strategy memory strategyBefore = fourSixTwoSixAgg.getStrategy(address(eTST)); + + assertEq(eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))), strategyBefore.allocated); + + uint256 expectedStrategyCash = fourSixTwoSixAgg.totalAssetsAllocatable() * strategyBefore.allocationPoints + / fourSixTwoSixAgg.totalAllocationPoints(); + + vm.prank(user1); + fourSixTwoSixAgg.rebalance(address(eTST)); + + assertEq(fourSixTwoSixAgg.totalAllocated(), expectedStrategyCash); + assertEq(eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))), expectedStrategyCash); + assertEq( + (fourSixTwoSixAgg.getStrategy(address(eTST))).allocated, + strategyBefore.allocated - (strategyBefore.allocated - expectedStrategyCash) + ); + } + + function testRebalanceByWithdrawingWhenToWithdrawIsGreaterThanMaxWithdraw() public { + uint256 amountToDeposit = 10000e18; + + // deposit into aggregator + { + uint256 balanceBefore = fourSixTwoSixAgg.balanceOf(user1); + uint256 totalSupplyBefore = fourSixTwoSixAgg.totalSupply(); + uint256 totalAssetsDepositedBefore = fourSixTwoSixAgg.totalAssetsDeposited(); + uint256 userAssetBalanceBefore = assetTST.balanceOf(user1); + + vm.startPrank(user1); + assetTST.approve(address(fourSixTwoSixAgg), amountToDeposit); + fourSixTwoSixAgg.deposit(amountToDeposit, user1); + vm.stopPrank(); + + assertEq(fourSixTwoSixAgg.balanceOf(user1), balanceBefore + amountToDeposit); + assertEq(fourSixTwoSixAgg.totalSupply(), totalSupplyBefore + amountToDeposit); + assertEq(fourSixTwoSixAgg.totalAssetsDeposited(), totalAssetsDepositedBefore + amountToDeposit); + assertEq(assetTST.balanceOf(user1), userAssetBalanceBefore - amountToDeposit); + } + + // rebalance into strategy + vm.warp(block.timestamp + 86400); + vm.prank(user1); + fourSixTwoSixAgg.rebalance(address(eTST)); + + // decrease allocation points + uint256 newAllocationPoints = 300e18; + vm.prank(manager); + fourSixTwoSixAgg.adjustAllocationPoints(address(eTST), newAllocationPoints); + + vm.warp(block.timestamp + 86400); + + FourSixTwoSixAgg.Strategy memory strategyBefore = fourSixTwoSixAgg.getStrategy(address(eTST)); + + assertEq(eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))), strategyBefore.allocated); + + uint256 expectedStrategyCash = fourSixTwoSixAgg.totalAssetsAllocatable() * strategyBefore.allocationPoints + / fourSixTwoSixAgg.totalAllocationPoints(); + uint256 expectedToWithdraw = strategyBefore.allocated - expectedStrategyCash; + uint256 eTSTMaxWithdraw = expectedToWithdraw * 7e17 / 1e18; + // mock max withdraw + vm.mockCall( + address(eTST), abi.encodeCall(eTST.maxWithdraw, (address(fourSixTwoSixAgg))), abi.encode(eTSTMaxWithdraw) + ); + + vm.prank(user1); + fourSixTwoSixAgg.rebalance(address(eTST)); + + assertEq(fourSixTwoSixAgg.totalAllocated(), strategyBefore.allocated - eTSTMaxWithdraw); + assertEq( + eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))), strategyBefore.allocated - eTSTMaxWithdraw + ); + assertEq((fourSixTwoSixAgg.getStrategy(address(eTST))).allocated, strategyBefore.allocated - eTSTMaxWithdraw); + } +} diff --git a/test/unit/RemoveStrategy.t.sol b/test/unit/RemoveStrategy.t.sol index 8f9d1d4b..e471c02d 100644 --- a/test/unit/RemoveStrategy.t.sol +++ b/test/unit/RemoveStrategy.t.sol @@ -1,15 +1,17 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.0; -import {FourSixTwoSixAggBase, FourSixTwoSixAgg} from "../common/FourSixTwoSixAggBase.t.sol"; +import {FourSixTwoSixAggBase, FourSixTwoSixAgg, IEVault} from "../common/FourSixTwoSixAggBase.t.sol"; contract RemoveStrategyTest is FourSixTwoSixAggBase { uint256 strategyAllocationPoints; + IEVault anotherStrategy; + function setUp() public virtual override { super.setUp(); - strategyAllocationPoints = type(uint120).max; + strategyAllocationPoints = 1000e18; _addStrategy(manager, address(eTST), strategyAllocationPoints); } @@ -28,6 +30,26 @@ contract RemoveStrategyTest is FourSixTwoSixAggBase { assertEq(fourSixTwoSixAgg.withdrawalQueueLength(), withdrawalQueueLengthBefore - 1); } + function testRemoveStrategyWithMultipleStrategies() public { + anotherStrategy = IEVault( + factory.createProxy(address(0), true, abi.encodePacked(address(assetTST), address(oracle), unitOfAccount)) + ); + _addStrategy(manager, address(anotherStrategy), strategyAllocationPoints); + + uint256 totalAllocationPointsBefore = fourSixTwoSixAgg.totalAllocationPoints(); + uint256 withdrawalQueueLengthBefore = fourSixTwoSixAgg.withdrawalQueueLength(); + + vm.prank(manager); + fourSixTwoSixAgg.removeStrategy(address(eTST)); + + FourSixTwoSixAgg.Strategy memory strategyAfter = fourSixTwoSixAgg.getStrategy(address(eTST)); + + assertEq(strategyAfter.active, false); + assertEq(strategyAfter.allocationPoints, 0); + assertEq(fourSixTwoSixAgg.totalAllocationPoints(), totalAllocationPointsBefore - strategyAllocationPoints); + assertEq(fourSixTwoSixAgg.withdrawalQueueLength(), withdrawalQueueLengthBefore - 1); + } + function testRemoveStrategy_fromUnauthorized() public { vm.prank(deployer); vm.expectRevert(); diff --git a/test/unit/ReorderWithdrawalQueueTest.t.sol b/test/unit/ReorderWithdrawalQueueTest.t.sol new file mode 100644 index 00000000..a68e8341 --- /dev/null +++ b/test/unit/ReorderWithdrawalQueueTest.t.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {FourSixTwoSixAggBase, FourSixTwoSixAgg, IEVault} from "../common/FourSixTwoSixAggBase.t.sol"; + +contract ReorderWithdrawalQueueTest is FourSixTwoSixAggBase { + uint256 eTSTAllocationPoints = 500e18; + uint256 eTSTsecondaryAllocationPoints = 700e18; + + IEVault eTSTsecondary; + + function setUp() public virtual override { + super.setUp(); + + _addStrategy(manager, address(eTST), eTSTAllocationPoints); + + { + eTSTsecondary = IEVault( + factory.createProxy( + address(0), true, abi.encodePacked(address(assetTST), address(oracle), unitOfAccount) + ) + ); + } + _addStrategy(manager, address(eTSTsecondary), eTSTsecondaryAllocationPoints); + } + + function testReorderWithdrawalQueue() public { + assertEq( + fourSixTwoSixAgg.getStrategy(fourSixTwoSixAgg.withdrawalQueue(0)).allocationPoints, eTSTAllocationPoints + ); + assertEq( + fourSixTwoSixAgg.getStrategy(fourSixTwoSixAgg.withdrawalQueue(1)).allocationPoints, + eTSTsecondaryAllocationPoints + ); + + vm.prank(manager); + fourSixTwoSixAgg.reorderWithdrawalQueue(0, 1); + + assertEq( + fourSixTwoSixAgg.getStrategy(fourSixTwoSixAgg.withdrawalQueue(0)).allocationPoints, + eTSTsecondaryAllocationPoints + ); + assertEq( + fourSixTwoSixAgg.getStrategy(fourSixTwoSixAgg.withdrawalQueue(1)).allocationPoints, eTSTAllocationPoints + ); + } + + function testReorderWithdrawalQueueWhenOutOfBounds() public { + vm.startPrank(manager); + vm.expectRevert(FourSixTwoSixAgg.OutOfBounds.selector); + fourSixTwoSixAgg.reorderWithdrawalQueue(0, 3); + vm.stopPrank(); + } + + function testReorderWithdrawalQueueWhenSameIndex() public { + vm.startPrank(manager); + vm.expectRevert(FourSixTwoSixAgg.SameIndexes.selector); + fourSixTwoSixAgg.reorderWithdrawalQueue(1, 1); + vm.stopPrank(); + } +}