From 8851afa466a089ccd38e178c73611a4b702c97a0 Mon Sep 17 00:00:00 2001 From: Haythem Sellami <17862704+haythemsellami@users.noreply.github.com> Date: Tue, 14 May 2024 15:43:39 +0300 Subject: [PATCH 01/11] fix --- src/FourSixTwoSixAgg.sol | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/FourSixTwoSixAgg.sol b/src/FourSixTwoSixAgg.sol index 9aa127ac..ed09ad8e 100644 --- a/src/FourSixTwoSixAgg.sol +++ b/src/FourSixTwoSixAgg.sol @@ -8,6 +8,9 @@ import {ERC4626, IERC4626} from "openzeppelin-contracts/token/ERC20/extensions/E import {EVCUtil, IEVC} from "ethereum-vault-connector/utils/EVCUtil.sol"; import {AccessControlEnumerable} from "openzeppelin-contracts/access/AccessControlEnumerable.sol"; + +import {console2} from "forge-std/Test.sol"; + // @note Do NOT use with fee on transfer tokens // @note Do NOT use with rebasing tokens // @note Based on https://github.com/euler-xyz/euler-vault-kit/blob/master/src/Synths/EulerSavingsRate.sol @@ -246,7 +249,7 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares) internal override - { + { totalAssetsDeposited -= assets; uint256 assetsRetrieved = IERC20(asset()).balanceOf(address(this)); @@ -258,7 +261,8 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { Strategy memory strategyData = strategies[withdrawalQueue[i]]; IERC4626 strategy = IERC4626(withdrawalQueue[i]); - harvest(address(strategy)); + _harvest(address(strategy)); + _gulp(); uint256 sharesBalance = strategy.balanceOf(address(this)); uint256 underlyingBalance = strategy.convertToAssets(sharesBalance); @@ -285,6 +289,10 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { } function gulp() public nonReentrant { + _gulp(); + } + + function _gulp() internal { ESRSlot memory esrSlotCache = updateInterestAndReturnESRSlotCache(); uint256 toGulp = totalAssetsAllocatable() - totalAssetsDeposited - esrSlotCache.interestLeft; @@ -324,10 +332,12 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { return; //nothing to rebalance as this is the cash reserve } + Strategy memory strategyData = strategies[strategy]; + // Harvest profits, also gulps and updates interest - harvest(strategy); + _harvest(strategy); + _gulp(); - Strategy memory strategyData = strategies[strategy]; uint256 totalAllocationPointsCache = totalAllocationPoints; uint256 totalAssetsAllocatableCache = totalAssetsAllocatable(); uint256 targetAllocation = @@ -381,12 +391,23 @@ 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 _harvest(address strategy) internal { Strategy memory strategyData = strategies[strategy]; + + if (strategyData.allocated == 0) return; + uint256 sharesBalance = IERC4626(strategy).balanceOf(address(this)); uint256 underlyingBalance = IERC4626(strategy).convertToAssets(sharesBalance); - // There's yield! - if (underlyingBalance > strategyData.allocated) { + if (underlyingBalance == strategyData.allocated) { + return; + } else if (underlyingBalance > strategyData.allocated) { + // There's yield! uint256 yield = underlyingBalance - strategyData.allocated; strategies[strategy].allocated = uint120(underlyingBalance); totalAllocated += yield; @@ -395,8 +416,6 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { // TODO handle losses revert NegativeYield(); } - - gulp(); } /// @notice Adjust a certain strategy's allocation points. From 5b882b71c01e718c98a915a5dfc035ebcaae6c58 Mon Sep 17 00:00:00 2001 From: Haythem Sellami <17862704+haythemsellami@users.noreply.github.com> Date: Tue, 14 May 2024 15:43:51 +0300 Subject: [PATCH 02/11] test: init e2e tests --- .../e2e/DepositRebalanceWithdrawE2ETest.t.sol | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 test/e2e/DepositRebalanceWithdrawE2ETest.t.sol diff --git a/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol b/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol new file mode 100644 index 00000000..e5afc1d1 --- /dev/null +++ b/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {FourSixTwoSixAggBase, FourSixTwoSixAgg, console2} from "../common/FourSixTwoSixAggBase.t.sol"; + +contract DepositRebalanceWithdrawE2ETest 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 testSingleStrategy_NoYield() 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, expectedStrategyCash); + } + + vm.warp(block.timestamp + 86400); + // partial withdraw, no need to withdraw from strategy as cash reserve is enough + uint256 amountToWithdraw = 6000e18; + { + FourSixTwoSixAgg.Strategy memory strategyBefore = fourSixTwoSixAgg.getStrategy(address(eTST)); + uint256 strategyShareBalanceBefore = eTST.balanceOf(address(fourSixTwoSixAgg)); + uint256 totalAssetsDepositedBefore = fourSixTwoSixAgg.totalAssetsDeposited(); + uint256 aggregatorTotalSupplyBefore = fourSixTwoSixAgg.totalSupply(); + uint256 user1AssetTSTBalanceBefore = assetTST.balanceOf(user1); + + vm.prank(user1); + fourSixTwoSixAgg.withdraw(amountToWithdraw, user1, user1); + + assertEq(eTST.balanceOf(address(fourSixTwoSixAgg)), strategyShareBalanceBefore); + assertEq(fourSixTwoSixAgg.totalAssetsDeposited(), totalAssetsDepositedBefore - amountToWithdraw); + assertEq(fourSixTwoSixAgg.totalSupply(), aggregatorTotalSupplyBefore - amountToWithdraw); + assertEq(assetTST.balanceOf(user1), user1AssetTSTBalanceBefore + amountToWithdraw); + } + + // full withdraw, will have to withdraw from strategy as cash reserve is not enough + { + amountToWithdraw = amountToDeposit - amountToWithdraw; + FourSixTwoSixAgg.Strategy memory strategyBefore = fourSixTwoSixAgg.getStrategy(address(eTST)); + uint256 totalAssetsDepositedBefore = fourSixTwoSixAgg.totalAssetsDeposited(); + uint256 aggregatorTotalSupplyBefore = fourSixTwoSixAgg.totalSupply(); + uint256 user1AssetTSTBalanceBefore = assetTST.balanceOf(user1); + + vm.prank(user1); + fourSixTwoSixAgg.withdraw(amountToWithdraw, user1, user1); + + assertEq(eTST.balanceOf(address(fourSixTwoSixAgg)), 0); + assertEq(fourSixTwoSixAgg.totalAssetsDeposited(), totalAssetsDepositedBefore - amountToWithdraw); + assertEq(fourSixTwoSixAgg.totalSupply(), aggregatorTotalSupplyBefore - amountToWithdraw); + assertEq(assetTST.balanceOf(user1), user1AssetTSTBalanceBefore + amountToWithdraw); + } + } +} From b282b3466b972b2f22a43e8454621ef3789409b4 Mon Sep 17 00:00:00 2001 From: Haythem Sellami <17862704+haythemsellami@users.noreply.github.com> Date: Tue, 14 May 2024 15:44:05 +0300 Subject: [PATCH 03/11] lint --- src/FourSixTwoSixAgg.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/FourSixTwoSixAgg.sol b/src/FourSixTwoSixAgg.sol index ed09ad8e..8c6fbc04 100644 --- a/src/FourSixTwoSixAgg.sol +++ b/src/FourSixTwoSixAgg.sol @@ -8,7 +8,6 @@ import {ERC4626, IERC4626} from "openzeppelin-contracts/token/ERC20/extensions/E import {EVCUtil, IEVC} from "ethereum-vault-connector/utils/EVCUtil.sol"; import {AccessControlEnumerable} from "openzeppelin-contracts/access/AccessControlEnumerable.sol"; - import {console2} from "forge-std/Test.sol"; // @note Do NOT use with fee on transfer tokens @@ -249,7 +248,7 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares) internal override - { + { totalAssetsDeposited -= assets; uint256 assetsRetrieved = IERC20(asset()).balanceOf(address(this)); From d18e619c8374350d8cbc59f706b035c404a33db9 Mon Sep 17 00:00:00 2001 From: Haythem Sellami <17862704+haythemsellami@users.noreply.github.com> Date: Tue, 14 May 2024 20:45:16 +0300 Subject: [PATCH 04/11] test: more tests --- src/FourSixTwoSixAgg.sol | 2 - test/common/FourSixTwoSixAggBase.t.sol | 2 +- .../e2e/DepositRebalanceWithdrawE2ETest.t.sol | 75 ++++++++++++++++++- 3 files changed, 74 insertions(+), 5 deletions(-) diff --git a/src/FourSixTwoSixAgg.sol b/src/FourSixTwoSixAgg.sol index 8c6fbc04..9c143d6e 100644 --- a/src/FourSixTwoSixAgg.sol +++ b/src/FourSixTwoSixAgg.sol @@ -8,8 +8,6 @@ import {ERC4626, IERC4626} from "openzeppelin-contracts/token/ERC20/extensions/E import {EVCUtil, IEVC} from "ethereum-vault-connector/utils/EVCUtil.sol"; import {AccessControlEnumerable} from "openzeppelin-contracts/access/AccessControlEnumerable.sol"; -import {console2} from "forge-std/Test.sol"; - // @note Do NOT use with fee on transfer tokens // @note Do NOT use with rebasing tokens // @note Based on https://github.com/euler-xyz/euler-vault-kit/blob/master/src/Synths/EulerSavingsRate.sol diff --git a/test/common/FourSixTwoSixAggBase.t.sol b/test/common/FourSixTwoSixAggBase.t.sol index 8c39c86d..1ec3c7d7 100644 --- a/test/common/FourSixTwoSixAggBase.t.sol +++ b/test/common/FourSixTwoSixAggBase.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.0; -import {EVaultTestBase, TestERC20, console2} from "evk/test/unit/evault/EVaultTestBase.t.sol"; +import {EVaultTestBase, TestERC20, console2, EVault} from "evk/test/unit/evault/EVaultTestBase.t.sol"; import {FourSixTwoSixAgg} from "../../src/FourSixTwoSixAgg.sol"; contract FourSixTwoSixAggBase is EVaultTestBase { diff --git a/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol b/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol index e5afc1d1..d6eb3295 100644 --- a/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol +++ b/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.0; -import {FourSixTwoSixAggBase, FourSixTwoSixAgg, console2} from "../common/FourSixTwoSixAggBase.t.sol"; +import {FourSixTwoSixAggBase, FourSixTwoSixAgg, console2, EVault} from "../common/FourSixTwoSixAggBase.t.sol"; contract DepositRebalanceWithdrawE2ETest is FourSixTwoSixAggBase { uint256 user1InitialBalance = 100000e18; @@ -51,7 +51,9 @@ contract DepositRebalanceWithdrawE2ETest is FourSixTwoSixAggBase { assertEq(fourSixTwoSixAgg.totalAllocated(), expectedStrategyCash); assertEq(eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))), expectedStrategyCash); - assertEq((fourSixTwoSixAgg.getStrategy(address(eTST))).allocated, expectedStrategyCash); + assertEq( + (fourSixTwoSixAgg.getStrategy(address(eTST))).allocated, strategyBefore.allocated + expectedStrategyCash + ); } vm.warp(block.timestamp + 86400); @@ -71,6 +73,7 @@ contract DepositRebalanceWithdrawE2ETest is FourSixTwoSixAggBase { assertEq(fourSixTwoSixAgg.totalAssetsDeposited(), totalAssetsDepositedBefore - amountToWithdraw); assertEq(fourSixTwoSixAgg.totalSupply(), aggregatorTotalSupplyBefore - amountToWithdraw); assertEq(assetTST.balanceOf(user1), user1AssetTSTBalanceBefore + amountToWithdraw); + assertEq((fourSixTwoSixAgg.getStrategy(address(eTST))).allocated, strategyBefore.allocated); } // full withdraw, will have to withdraw from strategy as cash reserve is not enough @@ -88,6 +91,74 @@ contract DepositRebalanceWithdrawE2ETest is FourSixTwoSixAggBase { assertEq(fourSixTwoSixAgg.totalAssetsDeposited(), totalAssetsDepositedBefore - amountToWithdraw); assertEq(fourSixTwoSixAgg.totalSupply(), aggregatorTotalSupplyBefore - amountToWithdraw); assertEq(assetTST.balanceOf(user1), user1AssetTSTBalanceBefore + amountToWithdraw); + assertEq((fourSixTwoSixAgg.getStrategy(address(eTST))).allocated, 0); + } + } + + function testSingleStrategy_WithYield() 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, expectedStrategyCash); + } + + vm.warp(block.timestamp + 86400); + uint256 aggrCurrentStrategyBalance = eTST.balanceOf(address(fourSixTwoSixAgg)); + // mock an increase of strategy balance by 10% + vm.mockCall( + address(eTST), + abi.encodeWithSelector(EVault.balanceOf.selector, address(fourSixTwoSixAgg)), + abi.encode(aggrCurrentStrategyBalance * 11e17 / 1e18) + ); + + // full withdraw, will have to withdraw from strategy as cash reserve is not enough + { + uint256 amountToWithdraw = fourSixTwoSixAgg.balanceOf(user1); + FourSixTwoSixAgg.Strategy memory strategyBefore = fourSixTwoSixAgg.getStrategy(address(eTST)); + uint256 totalAssetsDepositedBefore = fourSixTwoSixAgg.totalAssetsDeposited(); + uint256 aggregatorTotalSupplyBefore = fourSixTwoSixAgg.totalSupply(); + uint256 user1AssetTSTBalanceBefore = assetTST.balanceOf(user1); + + vm.prank(user1); + fourSixTwoSixAgg.redeem(amountToWithdraw, user1, user1); + vm.clearMockedCalls(); + + assertEq(eTST.balanceOf(address(fourSixTwoSixAgg)), 0); + assertEq(fourSixTwoSixAgg.totalAssetsDeposited(), totalAssetsDepositedBefore - amountToWithdraw); + assertEq(fourSixTwoSixAgg.totalSupply(), aggregatorTotalSupplyBefore - amountToWithdraw); + assertEq(assetTST.balanceOf(user1), user1AssetTSTBalanceBefore + fourSixTwoSixAgg.convertToAssets(amountToWithdraw)); } } } From f22d65e703b19fec878d6a9fadc03c87a5953dba Mon Sep 17 00:00:00 2001 From: Haythem Sellami <17862704+haythemsellami@users.noreply.github.com> Date: Tue, 14 May 2024 20:45:25 +0300 Subject: [PATCH 05/11] lint --- test/e2e/DepositRebalanceWithdrawE2ETest.t.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol b/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol index d6eb3295..dc7d85e9 100644 --- a/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol +++ b/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol @@ -158,7 +158,10 @@ contract DepositRebalanceWithdrawE2ETest is FourSixTwoSixAggBase { assertEq(eTST.balanceOf(address(fourSixTwoSixAgg)), 0); assertEq(fourSixTwoSixAgg.totalAssetsDeposited(), totalAssetsDepositedBefore - amountToWithdraw); assertEq(fourSixTwoSixAgg.totalSupply(), aggregatorTotalSupplyBefore - amountToWithdraw); - assertEq(assetTST.balanceOf(user1), user1AssetTSTBalanceBefore + fourSixTwoSixAgg.convertToAssets(amountToWithdraw)); + assertEq( + assetTST.balanceOf(user1), + user1AssetTSTBalanceBefore + fourSixTwoSixAgg.convertToAssets(amountToWithdraw) + ); } } } From 039974937b27872cd30bc26d3f6dd203e9a708fe Mon Sep 17 00:00:00 2001 From: Haythem Sellami <17862704+haythemsellami@users.noreply.github.com> Date: Tue, 14 May 2024 21:33:20 +0300 Subject: [PATCH 06/11] chore: update config --- .github/workflows/test.yml | 2 +- foundry.toml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cc22c240..6c33e4bc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: run: forge fmt --check - name: Run foundry tests - run: forge test -vv --gas-report --ast + run: FOUNDRY_PROFILE=test forge test -vv --gas-report --ast - name: Run foundry fuzzing run: FOUNDRY_PROFILE=ci_fuzz forge test -vv diff --git a/foundry.toml b/foundry.toml index b625e05f..e199dc18 100644 --- a/foundry.toml +++ b/foundry.toml @@ -18,6 +18,11 @@ quote_style = "double" number_underscore = "preserve" override_spacing = true +[profile.test] +no_match_test = "Fuzz" +no_match_contract = "Fuzz" +gas_reports = ["*"] + [profile.fuzz] runs = 1000 max_local_rejects = 1024 From a1f6ca64cadad12d8fc0feeb2f9d5475fde8ec8d Mon Sep 17 00:00:00 2001 From: Haythem Sellami <17862704+haythemsellami@users.noreply.github.com> Date: Tue, 14 May 2024 21:33:32 +0300 Subject: [PATCH 07/11] test: harvest() unit tests --- test/unit/HarvestTest.t.sol | 114 ++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 test/unit/HarvestTest.t.sol diff --git a/test/unit/HarvestTest.t.sol b/test/unit/HarvestTest.t.sol new file mode 100644 index 00000000..0f68a5ae --- /dev/null +++ b/test/unit/HarvestTest.t.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {FourSixTwoSixAggBase, FourSixTwoSixAgg, console2, EVault} from "../common/FourSixTwoSixAggBase.t.sol"; + +contract HarvestTest is FourSixTwoSixAggBase { + uint256 user1InitialBalance = 100000e18; + uint256 amountToDeposit = 10000e18; + + function setUp() public virtual override { + super.setUp(); + + uint256 initialStrategyAllocationPoints = 500e18; + _addStrategy(manager, address(eTST), initialStrategyAllocationPoints); + + assetTST.mint(user1, user1InitialBalance); + + // 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, expectedStrategyCash); + } + } + + function testHarvest() public { + // no yield increase + FourSixTwoSixAgg.Strategy memory strategyBefore = fourSixTwoSixAgg.getStrategy(address(eTST)); + uint256 totalAllocatedBefore = fourSixTwoSixAgg.totalAllocated(); + + assertTrue(eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))) == strategyBefore.allocated); + + vm.prank(user1); + fourSixTwoSixAgg.harvest(address(eTST)); + + assertEq((fourSixTwoSixAgg.getStrategy(address(eTST))).allocated, strategyBefore.allocated); + assertEq(fourSixTwoSixAgg.totalAllocated(), totalAllocatedBefore); + + // positive yield + vm.warp(block.timestamp + 86400); + + // mock an increase of strategy balance by 10% + uint256 aggrCurrentStrategyBalance = eTST.balanceOf(address(fourSixTwoSixAgg)); + vm.mockCall( + address(eTST), + abi.encodeWithSelector(EVault.balanceOf.selector, address(fourSixTwoSixAgg)), + abi.encode(aggrCurrentStrategyBalance * 11e17 / 1e18) + ); + + assertTrue(eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))) > strategyBefore.allocated); + + vm.prank(user1); + fourSixTwoSixAgg.harvest(address(eTST)); + + assertEq( + (fourSixTwoSixAgg.getStrategy(address(eTST))).allocated, + eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))) + ); + assertEq( + fourSixTwoSixAgg.totalAllocated(), + totalAllocatedBefore + + (eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))) - strategyBefore.allocated) + ); + } + + function testHarvestNegativeYield() public { + vm.warp(block.timestamp + 86400); + + // mock an increase of strategy balance by 10% + uint256 aggrCurrentStrategyBalance = eTST.balanceOf(address(fourSixTwoSixAgg)); + vm.mockCall( + address(eTST), + abi.encodeWithSelector(EVault.balanceOf.selector, address(fourSixTwoSixAgg)), + abi.encode(aggrCurrentStrategyBalance * 9e17 / 1e18) + ); + + FourSixTwoSixAgg.Strategy memory strategyBefore = fourSixTwoSixAgg.getStrategy(address(eTST)); + + assertTrue(eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))) < strategyBefore.allocated); + + vm.startPrank(user1); + vm.expectRevert(FourSixTwoSixAgg.NegativeYield.selector); + fourSixTwoSixAgg.harvest(address(eTST)); + } +} From 0aa32728369cfde139fd49bb5a3f134e86807fa7 Mon Sep 17 00:00:00 2001 From: Haythem Sellami <17862704+haythemsellami@users.noreply.github.com> Date: Wed, 15 May 2024 17:15:08 +0300 Subject: [PATCH 08/11] fix: load strategyData after harvesting --- src/FourSixTwoSixAgg.sol | 46 ++++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/src/FourSixTwoSixAgg.sol b/src/FourSixTwoSixAgg.sol index 9c143d6e..7951b357 100644 --- a/src/FourSixTwoSixAgg.sol +++ b/src/FourSixTwoSixAgg.sol @@ -8,6 +8,8 @@ import {ERC4626, IERC4626} from "openzeppelin-contracts/token/ERC20/extensions/E import {EVCUtil, IEVC} from "ethereum-vault-connector/utils/EVCUtil.sol"; import {AccessControlEnumerable} from "openzeppelin-contracts/access/AccessControlEnumerable.sol"; +import {console2} from "forge-std/Test.sol"; + // @note Do NOT use with fee on transfer tokens // @note Do NOT use with rebasing tokens // @note Based on https://github.com/euler-xyz/euler-vault-kit/blob/master/src/Synths/EulerSavingsRate.sol @@ -250,23 +252,31 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { totalAssetsDeposited -= assets; uint256 assetsRetrieved = IERC20(asset()).balanceOf(address(this)); - for (uint256 i = 0; i < withdrawalQueue.length; i++) { + for (uint256 i; i < withdrawalQueue.length; i++) { if (assetsRetrieved >= assets) { break; } - Strategy memory strategyData = strategies[withdrawalQueue[i]]; IERC4626 strategy = IERC4626(withdrawalQueue[i]); _harvest(address(strategy)); _gulp(); + Strategy memory strategyData = strategies[withdrawalQueue[i]]; + uint256 sharesBalance = strategy.balanceOf(address(this)); uint256 underlyingBalance = strategy.convertToAssets(sharesBalance); + console2.log("assets", assets); + console2.log("assetsRetrieved", assetsRetrieved); + uint256 desiredAssets = assets - assetsRetrieved; uint256 withdrawAmount = (underlyingBalance >= desiredAssets) ? desiredAssets : underlyingBalance; + console2.log("strategyData.allocated", strategyData.allocated); + console2.log("withdrawAmount", withdrawAmount); + console2.log("totalAllocated", totalAllocated); + // Update allocated assets strategies[withdrawalQueue[i]].allocated = strategyData.allocated - uint120(withdrawAmount); totalAllocated -= withdrawAmount; @@ -318,6 +328,12 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { 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. @@ -325,16 +341,20 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { /// - 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 { - if (strategy == address(0)) { + _rebalance(strategy); + } + + function _rebalance(address _strategy) internal { + if (_strategy == address(0)) { return; //nothing to rebalance as this is the cash reserve } - Strategy memory strategyData = strategies[strategy]; - // Harvest profits, also gulps and updates interest - _harvest(strategy); + _harvest(_strategy); _gulp(); + Strategy memory strategyData = strategies[_strategy]; + uint256 totalAllocationPointsCache = totalAllocationPoints; uint256 totalAssetsAllocatableCache = totalAssetsAllocatable(); uint256 targetAllocation = @@ -345,13 +365,13 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { // Withdraw uint256 toWithdraw = currentAllocation - targetAllocation; - uint256 maxWithdraw = IERC4626(strategy).maxWithdraw(address(this)); + uint256 maxWithdraw = IERC4626(_strategy).maxWithdraw(address(this)); if (toWithdraw > maxWithdraw) { toWithdraw = maxWithdraw; } - IERC4626(strategy).withdraw(toWithdraw, address(this), address(this)); - strategies[strategy].allocated = uint120(currentAllocation - toWithdraw); + IERC4626(_strategy).withdraw(toWithdraw, address(this), address(this)); + strategies[_strategy].allocated = uint120(currentAllocation - toWithdraw); totalAllocated -= toWithdraw; } else if (currentAllocation < targetAllocation) { // Deposit @@ -367,7 +387,7 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { toDeposit = cashAvailable; } - uint256 maxDeposit = IERC4626(strategy).maxDeposit(address(this)); + uint256 maxDeposit = IERC4626(_strategy).maxDeposit(address(this)); if (toDeposit > maxDeposit) { toDeposit = maxDeposit; } @@ -377,9 +397,9 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { } // Do required approval (safely) and deposit - IERC20(asset()).safeApprove(strategy, toDeposit); - IERC4626(strategy).deposit(toDeposit, address(this)); - strategies[strategy].allocated = uint120(currentAllocation + toDeposit); + IERC20(asset()).safeApprove(_strategy, toDeposit); + IERC4626(_strategy).deposit(toDeposit, address(this)); + strategies[_strategy].allocated = uint120(currentAllocation + toDeposit); totalAllocated += toDeposit; } } From 0a30414525dde9513794a37859d963a533da6f11 Mon Sep 17 00:00:00 2001 From: Haythem Sellami <17862704+haythemsellami@users.noreply.github.com> Date: Thu, 16 May 2024 13:11:24 +0300 Subject: [PATCH 09/11] more tests --- src/FourSixTwoSixAgg.sol | 9 -- test/common/FourSixTwoSixAggBase.t.sol | 2 +- .../e2e/DepositRebalanceWithdrawE2ETest.t.sol | 136 ++++++++++++++++-- 3 files changed, 129 insertions(+), 18 deletions(-) diff --git a/src/FourSixTwoSixAgg.sol b/src/FourSixTwoSixAgg.sol index 7951b357..1200d172 100644 --- a/src/FourSixTwoSixAgg.sol +++ b/src/FourSixTwoSixAgg.sol @@ -8,8 +8,6 @@ import {ERC4626, IERC4626} from "openzeppelin-contracts/token/ERC20/extensions/E import {EVCUtil, IEVC} from "ethereum-vault-connector/utils/EVCUtil.sol"; import {AccessControlEnumerable} from "openzeppelin-contracts/access/AccessControlEnumerable.sol"; -import {console2} from "forge-std/Test.sol"; - // @note Do NOT use with fee on transfer tokens // @note Do NOT use with rebasing tokens // @note Based on https://github.com/euler-xyz/euler-vault-kit/blob/master/src/Synths/EulerSavingsRate.sol @@ -267,16 +265,9 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { uint256 sharesBalance = strategy.balanceOf(address(this)); uint256 underlyingBalance = strategy.convertToAssets(sharesBalance); - console2.log("assets", assets); - console2.log("assetsRetrieved", assetsRetrieved); - uint256 desiredAssets = assets - assetsRetrieved; uint256 withdrawAmount = (underlyingBalance >= desiredAssets) ? desiredAssets : underlyingBalance; - console2.log("strategyData.allocated", strategyData.allocated); - console2.log("withdrawAmount", withdrawAmount); - console2.log("totalAllocated", totalAllocated); - // Update allocated assets strategies[withdrawalQueue[i]].allocated = strategyData.allocated - uint120(withdrawAmount); totalAllocated -= withdrawAmount; diff --git a/test/common/FourSixTwoSixAggBase.t.sol b/test/common/FourSixTwoSixAggBase.t.sol index 1ec3c7d7..bf3298d2 100644 --- a/test/common/FourSixTwoSixAggBase.t.sol +++ b/test/common/FourSixTwoSixAggBase.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.0; -import {EVaultTestBase, TestERC20, console2, EVault} from "evk/test/unit/evault/EVaultTestBase.t.sol"; +import "evk/test/unit/evault/EVaultTestBase.t.sol"; import {FourSixTwoSixAgg} from "../../src/FourSixTwoSixAgg.sol"; contract FourSixTwoSixAggBase is EVaultTestBase { diff --git a/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol b/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol index dc7d85e9..029f6b2c 100644 --- a/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol +++ b/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol @@ -1,7 +1,15 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.0; -import {FourSixTwoSixAggBase, FourSixTwoSixAgg, console2, EVault} from "../common/FourSixTwoSixAggBase.t.sol"; +import { + FourSixTwoSixAggBase, + FourSixTwoSixAgg, + console2, + EVault, + IEVault, + IRMTestDefault, + TestERC20 +} from "../common/FourSixTwoSixAggBase.t.sol"; contract DepositRebalanceWithdrawE2ETest is FourSixTwoSixAggBase { uint256 user1InitialBalance = 100000e18; @@ -135,13 +143,126 @@ contract DepositRebalanceWithdrawE2ETest is FourSixTwoSixAggBase { } vm.warp(block.timestamp + 86400); - uint256 aggrCurrentStrategyBalance = eTST.balanceOf(address(fourSixTwoSixAgg)); // mock an increase of strategy balance by 10% - vm.mockCall( - address(eTST), - abi.encodeWithSelector(EVault.balanceOf.selector, address(fourSixTwoSixAgg)), - abi.encode(aggrCurrentStrategyBalance * 11e17 / 1e18) - ); + uint256 aggrCurrentStrategyShareBalance = eTST.balanceOf(address(fourSixTwoSixAgg)); + uint256 aggrCurrentStrategyUnderlyingBalance = eTST.convertToAssets(aggrCurrentStrategyShareBalance); + uint256 aggrNewStrategyShareBalance = aggrCurrentStrategyShareBalance * 11e17 / 1e18; + uint256 aggrNewStrategyUnderlyingBalance = aggrCurrentStrategyUnderlyingBalance * 11e17 / 1e18; + uint256 yield = aggrNewStrategyUnderlyingBalance - aggrCurrentStrategyUnderlyingBalance; + assetTST.mint(address(eTST), yield); + eTST.skim(type(uint256).max, address(fourSixTwoSixAgg)); + + // full withdraw, will have to withdraw from strategy as cash reserve is not enough + { + uint256 amountToWithdraw = fourSixTwoSixAgg.balanceOf(user1); + FourSixTwoSixAgg.Strategy memory strategyBefore = fourSixTwoSixAgg.getStrategy(address(eTST)); + uint256 totalAssetsDepositedBefore = fourSixTwoSixAgg.totalAssetsDeposited(); + uint256 aggregatorTotalSupplyBefore = fourSixTwoSixAgg.totalSupply(); + uint256 user1AssetTSTBalanceBefore = assetTST.balanceOf(user1); + + vm.prank(user1); + fourSixTwoSixAgg.redeem(amountToWithdraw, user1, user1); + + assertEq(eTST.balanceOf(address(fourSixTwoSixAgg)), yield); + assertEq(fourSixTwoSixAgg.totalAssetsDeposited(), totalAssetsDepositedBefore - amountToWithdraw); + assertEq(fourSixTwoSixAgg.totalSupply(), aggregatorTotalSupplyBefore - amountToWithdraw); + assertEq( + assetTST.balanceOf(user1), + user1AssetTSTBalanceBefore + fourSixTwoSixAgg.convertToAssets(amountToWithdraw) + ); + } + } + + function testMultipleStrategy_WithYield() public { + IEVault eTSTsecondary; + { + eTSTsecondary = IEVault(coreProductLine.createVault(address(assetTST), address(oracle), unitOfAccount)); + eTSTsecondary.setInterestRateModel(address(new IRMTestDefault())); + + 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); + // 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; + uint256 eTSTYield = aggrNeweTSTUnderlyingBalance - aggrCurrenteTSTUnderlyingBalance; + uint256 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)); // full withdraw, will have to withdraw from strategy as cash reserve is not enough { @@ -153,7 +274,6 @@ contract DepositRebalanceWithdrawE2ETest is FourSixTwoSixAggBase { vm.prank(user1); fourSixTwoSixAgg.redeem(amountToWithdraw, user1, user1); - vm.clearMockedCalls(); assertEq(eTST.balanceOf(address(fourSixTwoSixAgg)), 0); assertEq(fourSixTwoSixAgg.totalAssetsDeposited(), totalAssetsDepositedBefore - amountToWithdraw); From 66722469599a68e76f14cd4405cb1eb1bb2fa07b Mon Sep 17 00:00:00 2001 From: Haythem Sellami <17862704+haythemsellami@users.noreply.github.com> Date: Thu, 16 May 2024 13:47:48 +0300 Subject: [PATCH 10/11] more tests --- .../e2e/DepositRebalanceWithdrawE2ETest.t.sol | 75 ++++++++++++++++++- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol b/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol index 029f6b2c..1405391d 100644 --- a/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol +++ b/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol @@ -87,7 +87,6 @@ contract DepositRebalanceWithdrawE2ETest is FourSixTwoSixAggBase { // full withdraw, will have to withdraw from strategy as cash reserve is not enough { amountToWithdraw = amountToDeposit - amountToWithdraw; - FourSixTwoSixAgg.Strategy memory strategyBefore = fourSixTwoSixAgg.getStrategy(address(eTST)); uint256 totalAssetsDepositedBefore = fourSixTwoSixAgg.totalAssetsDeposited(); uint256 aggregatorTotalSupplyBefore = fourSixTwoSixAgg.totalSupply(); uint256 user1AssetTSTBalanceBefore = assetTST.balanceOf(user1); @@ -146,7 +145,6 @@ contract DepositRebalanceWithdrawE2ETest is FourSixTwoSixAggBase { // mock an increase of strategy balance by 10% uint256 aggrCurrentStrategyShareBalance = eTST.balanceOf(address(fourSixTwoSixAgg)); uint256 aggrCurrentStrategyUnderlyingBalance = eTST.convertToAssets(aggrCurrentStrategyShareBalance); - uint256 aggrNewStrategyShareBalance = aggrCurrentStrategyShareBalance * 11e17 / 1e18; uint256 aggrNewStrategyUnderlyingBalance = aggrCurrentStrategyUnderlyingBalance * 11e17 / 1e18; uint256 yield = aggrNewStrategyUnderlyingBalance - aggrCurrentStrategyUnderlyingBalance; assetTST.mint(address(eTST), yield); @@ -155,7 +153,6 @@ contract DepositRebalanceWithdrawE2ETest is FourSixTwoSixAggBase { // full withdraw, will have to withdraw from strategy as cash reserve is not enough { uint256 amountToWithdraw = fourSixTwoSixAgg.balanceOf(user1); - FourSixTwoSixAgg.Strategy memory strategyBefore = fourSixTwoSixAgg.getStrategy(address(eTST)); uint256 totalAssetsDepositedBefore = fourSixTwoSixAgg.totalAssetsDeposited(); uint256 aggregatorTotalSupplyBefore = fourSixTwoSixAgg.totalSupply(); uint256 user1AssetTSTBalanceBefore = assetTST.balanceOf(user1); @@ -267,7 +264,6 @@ contract DepositRebalanceWithdrawE2ETest is FourSixTwoSixAggBase { // full withdraw, will have to withdraw from strategy as cash reserve is not enough { uint256 amountToWithdraw = fourSixTwoSixAgg.balanceOf(user1); - FourSixTwoSixAgg.Strategy memory strategyBefore = fourSixTwoSixAgg.getStrategy(address(eTST)); uint256 totalAssetsDepositedBefore = fourSixTwoSixAgg.totalAssetsDeposited(); uint256 aggregatorTotalSupplyBefore = fourSixTwoSixAgg.totalSupply(); uint256 user1AssetTSTBalanceBefore = assetTST.balanceOf(user1); @@ -284,4 +280,75 @@ contract DepositRebalanceWithdrawE2ETest is FourSixTwoSixAggBase { ); } } + + function testSingleStrategy_WithYield_WithInterest() 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, expectedStrategyCash); + } + + vm.warp(block.timestamp + 86400); + // mock an increase of strategy balance by 10% + uint256 aggrCurrentStrategyShareBalance = eTST.balanceOf(address(fourSixTwoSixAgg)); + uint256 aggrCurrentStrategyUnderlyingBalance = eTST.convertToAssets(aggrCurrentStrategyShareBalance); + uint256 aggrNewStrategyUnderlyingBalance = aggrCurrentStrategyUnderlyingBalance * 11e17 / 1e18; + uint256 yield = aggrNewStrategyUnderlyingBalance - aggrCurrentStrategyUnderlyingBalance; + assetTST.mint(address(eTST), yield); + eTST.skim(type(uint256).max, address(fourSixTwoSixAgg)); + + // harvest + vm.prank(user1); + fourSixTwoSixAgg.harvest(address(eTST)); + vm.warp(block.timestamp + 2 weeks); + + // full withdraw, will have to withdraw from strategy as cash reserve is not enough + { + uint256 amountToWithdraw = fourSixTwoSixAgg.balanceOf(user1); + uint256 totalAssetsDepositedBefore = fourSixTwoSixAgg.totalAssetsDeposited(); + uint256 aggregatorTotalSupplyBefore = fourSixTwoSixAgg.totalSupply(); + uint256 user1AssetTSTBalanceBefore = assetTST.balanceOf(user1); + + vm.prank(user1); + fourSixTwoSixAgg.redeem(amountToWithdraw, user1, user1); + + // all yield is distributed + assertApproxEqAbs(eTST.balanceOf(address(fourSixTwoSixAgg)), 0, 1); + assertApproxEqAbs(fourSixTwoSixAgg.totalAssetsDeposited(), totalAssetsDepositedBefore - amountToWithdraw, 1); + assertEq(fourSixTwoSixAgg.totalSupply(), aggregatorTotalSupplyBefore - amountToWithdraw); + assertApproxEqAbs(assetTST.balanceOf(user1), user1AssetTSTBalanceBefore + amountToDeposit + yield, 1); + } + } } From 214303eb774188a842b61a498056ad493b34203b Mon Sep 17 00:00:00 2001 From: Haythem Sellami <17862704+haythemsellami@users.noreply.github.com> Date: Thu, 16 May 2024 14:08:28 +0300 Subject: [PATCH 11/11] more tests --- src/FourSixTwoSixAgg.sol | 9 +- .../e2e/DepositRebalanceWithdrawE2ETest.t.sol | 125 ++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) diff --git a/src/FourSixTwoSixAgg.sol b/src/FourSixTwoSixAgg.sol index 1200d172..4074a31a 100644 --- a/src/FourSixTwoSixAgg.sol +++ b/src/FourSixTwoSixAgg.sol @@ -395,7 +395,6 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { } } - /// ToDo: possibly allow batch harvest /// @notice Harvest positive yield. /// @param strategy address of strategy function harvest(address strategy) public nonReentrant { @@ -404,6 +403,14 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { _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]; diff --git a/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol b/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol index 1405391d..437b73b6 100644 --- a/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol +++ b/test/e2e/DepositRebalanceWithdrawE2ETest.t.sol @@ -351,4 +351,129 @@ contract DepositRebalanceWithdrawE2ETest is FourSixTwoSixAggBase { assertApproxEqAbs(assetTST.balanceOf(user1), user1AssetTSTBalanceBefore + amountToDeposit + yield, 1); } } + + function testMultipleStrategy_WithYield_WithInterest() public { + IEVault eTSTsecondary; + { + eTSTsecondary = IEVault(coreProductLine.createVault(address(assetTST), address(oracle), unitOfAccount)); + eTSTsecondary.setInterestRateModel(address(new IRMTestDefault())); + + 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[](2); + strategiesToHarvest[0] = address(eTST); + strategiesToHarvest[1] = address(eTSTsecondary); + vm.prank(user1); + fourSixTwoSixAgg.harvestMultipleStrategies(strategiesToHarvest); + vm.warp(block.timestamp + 2 weeks); + + // full withdraw, will have to withdraw from strategy as cash reserve is not enough + { + uint256 amountToWithdraw = fourSixTwoSixAgg.balanceOf(user1); + uint256 totalAssetsDepositedBefore = fourSixTwoSixAgg.totalAssetsDeposited(); + uint256 aggregatorTotalSupplyBefore = fourSixTwoSixAgg.totalSupply(); + uint256 user1AssetTSTBalanceBefore = assetTST.balanceOf(user1); + + vm.prank(user1); + fourSixTwoSixAgg.redeem(amountToWithdraw, user1, user1); + + assertApproxEqAbs(eTST.balanceOf(address(fourSixTwoSixAgg)), 0, 0); + assertApproxEqAbs(fourSixTwoSixAgg.totalAssetsDeposited(), totalAssetsDepositedBefore - amountToWithdraw, 1); + assertEq(fourSixTwoSixAgg.totalSupply(), aggregatorTotalSupplyBefore - amountToWithdraw); + assertApproxEqAbs( + assetTST.balanceOf(user1), + user1AssetTSTBalanceBefore + amountToDeposit + eTSTYield + eTSTsecondaryYield, + 1 + ); + } + } }