From 10a6db8d23ecb0a6d10f86413b19efeb0060f10c Mon Sep 17 00:00:00 2001 From: Haythem Sellami <17862704+haythemsellami@users.noreply.github.com> Date: Thu, 30 May 2024 13:42:59 +0100 Subject: [PATCH 1/8] feat: socialize negative yield --- src/FourSixTwoSixAgg.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/FourSixTwoSixAgg.sol b/src/FourSixTwoSixAgg.sol index 5f79f742..607dbbc1 100644 --- a/src/FourSixTwoSixAgg.sol +++ b/src/FourSixTwoSixAgg.sol @@ -620,8 +620,8 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn _accruePerformanceFee(yield); } else { - // TODO handle losses - revert NegativeYield(); + uint256 socializedLoss = strategyData.allocated - underlyingBalance; + totalAssetsDeposited -= socializedLoss; } } From 61a19e3acc110760d6b9a2617b8ba470c7f67164 Mon Sep 17 00:00:00 2001 From: Haythem Sellami <17862704+haythemsellami@users.noreply.github.com> Date: Fri, 31 May 2024 16:52:35 +0100 Subject: [PATCH 2/8] fix and tests' --- src/FourSixTwoSixAgg.sol | 7 +-- test/unit/HarvestTest.t.sol | 85 ++++++++++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/src/FourSixTwoSixAgg.sol b/src/FourSixTwoSixAgg.sol index 607dbbc1..38d758b4 100644 --- a/src/FourSixTwoSixAgg.sol +++ b/src/FourSixTwoSixAgg.sol @@ -10,6 +10,8 @@ import {EVCUtil, IEVC} from "ethereum-vault-connector/utils/EVCUtil.sol"; import {BalanceForwarder, IBalanceForwarder} from "./BalanceForwarder.sol"; import {IRewardStreams} from "reward-streams/interfaces/IRewardStreams.sol"; +import {Test, console2, stdError} from "forge-std/Test.sol"; + /// @dev Do NOT use with fee on transfer tokens /// @dev Do NOT use with rebasing tokens /// @dev Based on https://github.com/euler-xyz/euler-vault-kit/blob/master/src/Synths/EulerSavingsRate.sol @@ -437,7 +439,6 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn 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); @@ -524,7 +525,6 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn if (totalAssetsDeposited == 0) return; uint256 toGulp = totalAssetsAllocatable() - totalAssetsDeposited - esrSlotCache.interestLeft; - uint256 maxGulp = type(uint168).max - esrSlotCache.interestLeft; if (toGulp > maxGulp) toGulp = maxGulp; // cap interest, allowing the vault to function @@ -606,7 +606,6 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn Strategy memory strategyData = strategies[strategy]; if (strategyData.allocated == 0) return; - uint256 sharesBalance = IERC4626(strategy).balanceOf(address(this)); uint256 underlyingBalance = IERC4626(strategy).convertToAssets(sharesBalance); @@ -621,6 +620,8 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn _accruePerformanceFee(yield); } else { uint256 socializedLoss = strategyData.allocated - underlyingBalance; + strategies[strategy].allocated = uint120(underlyingBalance); + // totalAllocated -= socializedLoss; totalAssetsDeposited -= socializedLoss; } } diff --git a/test/unit/HarvestTest.t.sol b/test/unit/HarvestTest.t.sol index 0f68a5ae..e5039756 100644 --- a/test/unit/HarvestTest.t.sol +++ b/test/unit/HarvestTest.t.sol @@ -95,7 +95,7 @@ contract HarvestTest is FourSixTwoSixAggBase { function testHarvestNegativeYield() public { vm.warp(block.timestamp + 86400); - // mock an increase of strategy balance by 10% + // mock a decrease of strategy balance by 10% uint256 aggrCurrentStrategyBalance = eTST.balanceOf(address(fourSixTwoSixAgg)); vm.mockCall( address(eTST), @@ -107,8 +107,89 @@ contract HarvestTest is FourSixTwoSixAggBase { assertTrue(eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))) < strategyBefore.allocated); + vm.prank(user1); + fourSixTwoSixAgg.harvest(address(eTST)); + } + + function testHarvestNegativeYieldAndWithdrawSingleUser() public { + vm.warp(block.timestamp + 86400); + + // mock a decrease of strategy balance by 10% + uint256 aggrCurrentStrategyBalance = eTST.balanceOf(address(fourSixTwoSixAgg)); + uint256 aggrCurrentStrategyBalanceAfterNegYield = aggrCurrentStrategyBalance * 9e17 / 1e18; + vm.mockCall( + address(eTST), + abi.encodeWithSelector(EVault.balanceOf.selector, address(fourSixTwoSixAgg)), + abi.encode(aggrCurrentStrategyBalanceAfterNegYield) + ); + + FourSixTwoSixAgg.Strategy memory strategyBefore = fourSixTwoSixAgg.getStrategy(address(eTST)); + assertTrue(eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))) < strategyBefore.allocated); + uint256 negativeYield = + strategyBefore.allocated - eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))); + + uint256 user1SharesBefore = fourSixTwoSixAgg.balanceOf(user1); + uint256 user1SocializedLoss = user1SharesBefore * negativeYield / fourSixTwoSixAgg.totalSupply(); + uint256 expectedUser1Assets = + user1SharesBefore * amountToDeposit / fourSixTwoSixAgg.totalSupply() - user1SocializedLoss; + uint256 user1AssetTSTBalanceBefore = assetTST.balanceOf(user1); + vm.startPrank(user1); - vm.expectRevert(FourSixTwoSixAgg.NegativeYield.selector); fourSixTwoSixAgg.harvest(address(eTST)); + fourSixTwoSixAgg.redeem(user1SharesBefore, user1, user1); + vm.stopPrank(); + + uint256 user1SharesAfter = fourSixTwoSixAgg.balanceOf(user1); + + assertEq(user1SharesAfter, 0); + assertApproxEqAbs(assetTST.balanceOf(user1), user1AssetTSTBalanceBefore + expectedUser1Assets, 1); + } + + function testHarvestNegativeYieldwMultipleUser() public { + uint256 user2InitialBalance = 5000e18; + assetTST.mint(user2, user2InitialBalance); + // deposit into aggregator + { + vm.startPrank(user2); + assetTST.approve(address(fourSixTwoSixAgg), user2InitialBalance); + fourSixTwoSixAgg.deposit(user2InitialBalance, user2); + vm.stopPrank(); + } + + vm.warp(block.timestamp + 86400); + + // mock a decrease of strategy balance by 10% + uint256 aggrCurrentStrategyBalance = eTST.balanceOf(address(fourSixTwoSixAgg)); + uint256 aggrCurrentStrategyBalanceAfterNegYield = aggrCurrentStrategyBalance * 9e17 / 1e18; + vm.mockCall( + address(eTST), + abi.encodeWithSelector(EVault.balanceOf.selector, address(fourSixTwoSixAgg)), + abi.encode(aggrCurrentStrategyBalanceAfterNegYield) + ); + + FourSixTwoSixAgg.Strategy memory strategyBefore = fourSixTwoSixAgg.getStrategy(address(eTST)); + assertTrue(eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))) < strategyBefore.allocated); + uint256 negativeYield = + strategyBefore.allocated - eTST.convertToAssets(eTST.balanceOf(address(fourSixTwoSixAgg))); + uint256 user1SharesBefore = fourSixTwoSixAgg.balanceOf(user1); + uint256 user1SocializedLoss = user1SharesBefore * negativeYield / fourSixTwoSixAgg.totalSupply(); + uint256 user2SharesBefore = fourSixTwoSixAgg.balanceOf(user2); + uint256 user2SocializedLoss = user2SharesBefore * negativeYield / fourSixTwoSixAgg.totalSupply(); + + uint256 expectedUser1Assets = user1SharesBefore * (amountToDeposit + user2InitialBalance) + / fourSixTwoSixAgg.totalSupply() - user1SocializedLoss; + uint256 expectedUser2Assets = user2SharesBefore * (amountToDeposit + user2InitialBalance) + / fourSixTwoSixAgg.totalSupply() - user2SocializedLoss; + + vm.prank(user1); + fourSixTwoSixAgg.harvest(address(eTST)); + + uint256 user1SharesAfter = fourSixTwoSixAgg.balanceOf(user1); + uint256 user1AssetsAfter = fourSixTwoSixAgg.convertToAssets(user1SharesAfter); + uint256 user2SharesAfter = fourSixTwoSixAgg.balanceOf(user2); + uint256 user2AssetsAfter = fourSixTwoSixAgg.convertToAssets(user2SharesAfter); + + assertApproxEqAbs(user1AssetsAfter, expectedUser1Assets, 1); + assertApproxEqAbs(user2AssetsAfter, expectedUser2Assets, 1); } } From 5c4746b1492bfefc1ba9f41fdfa4cd37d2ed84a6 Mon Sep 17 00:00:00 2001 From: Haythem Sellami <17862704+haythemsellami@users.noreply.github.com> Date: Fri, 31 May 2024 16:53:10 +0100 Subject: [PATCH 3/8] decrease totalAllocated --- src/FourSixTwoSixAgg.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FourSixTwoSixAgg.sol b/src/FourSixTwoSixAgg.sol index 38d758b4..cc63807c 100644 --- a/src/FourSixTwoSixAgg.sol +++ b/src/FourSixTwoSixAgg.sol @@ -621,7 +621,7 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn } else { uint256 socializedLoss = strategyData.allocated - underlyingBalance; strategies[strategy].allocated = uint120(underlyingBalance); - // totalAllocated -= socializedLoss; + totalAllocated -= socializedLoss; totalAssetsDeposited -= socializedLoss; } } From 034c2e514dbb388f5a6f738b54cf98a367315099 Mon Sep 17 00:00:00 2001 From: Haythem Sellami <17862704+haythemsellami@users.noreply.github.com> Date: Mon, 3 Jun 2024 14:45:09 +0100 Subject: [PATCH 4/8] fix --- src/FourSixTwoSixAgg.sol | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/FourSixTwoSixAgg.sol b/src/FourSixTwoSixAgg.sol index cc63807c..57804dc0 100644 --- a/src/FourSixTwoSixAgg.sol +++ b/src/FourSixTwoSixAgg.sol @@ -525,6 +525,7 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn if (totalAssetsDeposited == 0) return; uint256 toGulp = totalAssetsAllocatable() - totalAssetsDeposited - esrSlotCache.interestLeft; + /// TODO return if toGulp == 0 uint256 maxGulp = type(uint168).max - esrSlotCache.interestLeft; if (toGulp > maxGulp) toGulp = maxGulp; // cap interest, allowing the vault to function @@ -619,10 +620,19 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn _accruePerformanceFee(yield); } else { - uint256 socializedLoss = strategyData.allocated - underlyingBalance; + uint256 loss = strategyData.allocated - underlyingBalance; + strategies[strategy].allocated = uint120(underlyingBalance); - totalAllocated -= socializedLoss; - totalAssetsDeposited -= socializedLoss; + totalAllocated -= loss; + + ESRSlot memory esrSlotCache = esrSlot; + if (esrSlotCache.interestLeft >= loss) { + esrSlotCache.interestLeft -= uint168(loss); + } else { + totalAssetsDeposited -= loss - esrSlotCache.interestLeft; + esrSlotCache.interestLeft = 0; + } + esrSlot = esrSlotCache; } } From 0262537185ba9cc113a75ff945c81d5c84d1cb92 Mon Sep 17 00:00:00 2001 From: Haythem Sellami <17862704+haythemsellami@users.noreply.github.com> Date: Mon, 3 Jun 2024 14:45:19 +0100 Subject: [PATCH 5/8] more gulping tests --- test/unit/GulpTest.t.sol | 157 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 test/unit/GulpTest.t.sol diff --git a/test/unit/GulpTest.t.sol b/test/unit/GulpTest.t.sol new file mode 100644 index 00000000..c96cb3d1 --- /dev/null +++ b/test/unit/GulpTest.t.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {FourSixTwoSixAggBase, FourSixTwoSixAgg, console2, EVault} from "../common/FourSixTwoSixAggBase.t.sol"; + +contract GulpTest 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 testGulp() public { + // console2.log("#testGulp"); + // fourSixTwoSixAgg.gulp(); + // FourSixTwoSixAgg.ESRSlot memory ers = fourSixTwoSixAgg.getESRSlot(); + // assertEq(fourSixTwoSixAgg.interestAccrued(), 0); + // assertEq(ers.interestLeft, 0); + + // vm.warp(block.timestamp + 2 days); + // fourSixTwoSixAgg.gulp(); + // assertEq(fourSixTwoSixAgg.interestAccrued(), 0); + + // vm.warp(block.timestamp + 1 days); + // assertEq(fourSixTwoSixAgg.interestAccrued(), 0); + // uint256 yield; + // { + // uint256 aggrCurrentStrategyShareBalance = eTST.balanceOf(address(fourSixTwoSixAgg)); + // uint256 aggrCurrentStrategyUnderlyingBalance = eTST.convertToAssets(aggrCurrentStrategyShareBalance); + // uint256 aggrNewStrategyUnderlyingBalance = aggrCurrentStrategyUnderlyingBalance * 11e17 / 1e18; + // yield = aggrNewStrategyUnderlyingBalance - aggrCurrentStrategyUnderlyingBalance; + // assetTST.mint(address(eTST), yield); + // eTST.skim(type(uint256).max, address(fourSixTwoSixAgg)); + // } + // vm.prank(user1); + // fourSixTwoSixAgg.harvest(address(eTST)); + + // assertEq(fourSixTwoSixAgg.interestAccrued(), 0); + + // vm.warp(block.timestamp + 1 days); + // // interest per day 23.809523809523 + // assertEq(fourSixTwoSixAgg.interestAccrued(), 23809523809523809523); + // fourSixTwoSixAgg.gulp(); + // ers = fourSixTwoSixAgg.getESRSlot(); + // assertEq(ers.interestLeft, yield - 23809523809523809523); + + // // move close to end of smearing + // vm.warp(block.timestamp + 11 days); + // fourSixTwoSixAgg.gulp(); + // ers = fourSixTwoSixAgg.getESRSlot(); + // // assertEq(ers.interestLeft, yield - 23809523809523809523*11); + + // // mock a decrease of strategy balance by ers.interestLeft + // uint256 aggrCurrentStrategyBalance = eTST.balanceOf(address(fourSixTwoSixAgg)); + // uint256 aggrCurrentStrategyBalanceAfterNegYield = aggrCurrentStrategyBalance - ers.interestLeft; + // vm.mockCall( + // address(eTST), + // abi.encodeWithSelector(EVault.balanceOf.selector, address(fourSixTwoSixAgg)), + // abi.encode(aggrCurrentStrategyBalanceAfterNegYield) + // ); + // vm.prank(user1); + // fourSixTwoSixAgg.harvest(address(eTST)); + // } + + function testGulp() public { + fourSixTwoSixAgg.gulp(); + FourSixTwoSixAgg.ESRSlot memory ers = fourSixTwoSixAgg.getESRSlot(); + assertEq(fourSixTwoSixAgg.interestAccrued(), 0); + assertEq(ers.interestLeft, 0); + + vm.warp(block.timestamp + 2 days); + fourSixTwoSixAgg.gulp(); + assertEq(fourSixTwoSixAgg.interestAccrued(), 0); + + vm.warp(block.timestamp + 1 days); + assertEq(fourSixTwoSixAgg.interestAccrued(), 0); + uint256 yield; + { + uint256 aggrCurrentStrategyShareBalance = eTST.balanceOf(address(fourSixTwoSixAgg)); + uint256 aggrCurrentStrategyUnderlyingBalance = eTST.convertToAssets(aggrCurrentStrategyShareBalance); + uint256 aggrNewStrategyUnderlyingBalance = aggrCurrentStrategyUnderlyingBalance * 11e17 / 1e18; + yield = aggrNewStrategyUnderlyingBalance - aggrCurrentStrategyUnderlyingBalance; + assetTST.mint(address(eTST), yield); + eTST.skim(type(uint256).max, address(fourSixTwoSixAgg)); + } + vm.prank(user1); + fourSixTwoSixAgg.harvest(address(eTST)); + + assertEq(fourSixTwoSixAgg.interestAccrued(), 0); + + vm.warp(block.timestamp + 1 days); + // interest per day 23.809523809523 + assertEq(fourSixTwoSixAgg.interestAccrued(), 23809523809523809523); + fourSixTwoSixAgg.gulp(); + ers = fourSixTwoSixAgg.getESRSlot(); + assertEq(ers.interestLeft, yield - 23809523809523809523); + + // move close to end of smearing + vm.warp(block.timestamp + 11 days); + fourSixTwoSixAgg.gulp(); + ers = fourSixTwoSixAgg.getESRSlot(); + // assertEq(ers.interestLeft, yield - 23809523809523809523*11); + + // mock a decrease of strategy balance by ers.interestLeft + uint256 aggrCurrentStrategyBalance = eTST.balanceOf(address(fourSixTwoSixAgg)); + uint256 aggrCurrentStrategyBalanceAfterNegYield = aggrCurrentStrategyBalance - (ers.interestLeft * 2); + vm.mockCall( + address(eTST), + abi.encodeWithSelector(EVault.balanceOf.selector, address(fourSixTwoSixAgg)), + abi.encode(aggrCurrentStrategyBalanceAfterNegYield) + ); + vm.prank(user1); + fourSixTwoSixAgg.harvest(address(eTST)); + } +} From 0ce8f6592f085db2f11cf4fd77266707d79c2aad Mon Sep 17 00:00:00 2001 From: Haythem Sellami <17862704+haythemsellami@users.noreply.github.com> Date: Mon, 3 Jun 2024 16:44:23 +0100 Subject: [PATCH 6/8] clean --- src/FourSixTwoSixAgg.sol | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/FourSixTwoSixAgg.sol b/src/FourSixTwoSixAgg.sol index 57804dc0..d15dd4fb 100644 --- a/src/FourSixTwoSixAgg.sol +++ b/src/FourSixTwoSixAgg.sol @@ -224,6 +224,8 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn /// @param _strategy Address of strategy to rebalance. function rebalance(address _strategy) external nonReentrant { _rebalance(_strategy); + + _gulp(); } /// @notice Rebalance multiple strategies. @@ -232,9 +234,11 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn for (uint256 i; i < _strategies.length; ++i) { _rebalance(_strategies[i]); } + + _gulp(); } - /// @notice Harvest positive yield. + /// @notice Harvest strategy. /// @param strategy address of strategy function harvest(address strategy) external nonReentrant { _harvest(strategy); @@ -242,6 +246,8 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn _gulp(); } + /// @notice Harvest multiple strategie. + /// @param _strategies an array of strategy addresses. function harvestMultipleStrategies(address[] calldata _strategies) external nonReentrant { for (uint256 i; i < _strategies.length; ++i) { _harvest(_strategies[i]); @@ -340,6 +346,10 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn withdrawalQueue.pop(); } + function updateInterestAccrued() external returns (ESRSlot memory) { + return _updateInterestAccrued(); + } + function gulp() external nonReentrant { _gulp(); } @@ -366,7 +376,7 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn /// @notice Return the accrued interest /// @return uint256 accrued interest function interestAccrued() external view returns (uint256) { - return interestAccruedFromCache(esrSlot); + return _interestAccruedFromCache(esrSlot); } /// @notice Transfers a certain amount of tokens to a recipient. @@ -418,7 +428,7 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn returns (uint256 shares) { // Move interest to totalAssetsDeposited - updateInterestAndReturnESRSlotCache(); + _updateInterestAccrued(); return super.withdraw(assets, receiver, owner); } @@ -432,13 +442,13 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn returns (uint256 assets) { // Move interest to totalAssetsDeposited - updateInterestAndReturnESRSlotCache(); + _updateInterestAccrued(); return super.redeem(shares, receiver, owner); } - function updateInterestAndReturnESRSlotCache() public returns (ESRSlot memory) { + function _updateInterestAccrued() internal returns (ESRSlot memory) { ESRSlot memory esrSlotCache = esrSlot; - uint256 accruedInterest = interestAccruedFromCache(esrSlotCache); + 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); @@ -453,7 +463,7 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn /// @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); + return totalAssetsDeposited + _interestAccruedFromCache(esrSlot); } /// @notice get the total assets allocatable @@ -520,12 +530,15 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn super._withdraw(caller, receiver, owner, assets, shares); } + /// @dev gulp positive yield and increment the left interest function _gulp() internal { - ESRSlot memory esrSlotCache = updateInterestAndReturnESRSlotCache(); + ESRSlot memory esrSlotCache = _updateInterestAccrued(); if (totalAssetsDeposited == 0) return; uint256 toGulp = totalAssetsAllocatable() - totalAssetsDeposited - esrSlotCache.interestLeft; - /// TODO return if toGulp == 0 + + if (toGulp == 0) return; + uint256 maxGulp = type(uint168).max - esrSlotCache.interestLeft; if (toGulp > maxGulp) toGulp = maxGulp; // cap interest, allowing the vault to function @@ -550,7 +563,6 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn // Harvest profits, also gulps and updates interest _harvest(_strategy); - _gulp(); Strategy memory strategyData = strategies[_strategy]; @@ -663,7 +675,7 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn /// @dev Get accrued interest without updating it. /// @param esrSlotCache Cached esrSlot /// @return uint256 accrued interest - function interestAccruedFromCache(ESRSlot memory esrSlotCache) internal view returns (uint256) { + function _interestAccruedFromCache(ESRSlot memory esrSlotCache) internal view returns (uint256) { // If distribution ended, full amount is accrued if (block.timestamp > esrSlotCache.interestSmearEnd) { return esrSlotCache.interestLeft; From d55d61b1dd26b5085a98233b37310b46e0410a47 Mon Sep 17 00:00:00 2001 From: Haythem Sellami <17862704+haythemsellami@users.noreply.github.com> Date: Mon, 3 Jun 2024 17:08:44 +0100 Subject: [PATCH 7/8] more tests --- test/unit/GulpTest.t.sol | 105 +++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 54 deletions(-) diff --git a/test/unit/GulpTest.t.sol b/test/unit/GulpTest.t.sol index c96cb3d1..3f6da94d 100644 --- a/test/unit/GulpTest.t.sol +++ b/test/unit/GulpTest.t.sol @@ -52,59 +52,57 @@ contract GulpTest is FourSixTwoSixAggBase { } } - // function testGulp() public { - // console2.log("#testGulp"); - // fourSixTwoSixAgg.gulp(); - // FourSixTwoSixAgg.ESRSlot memory ers = fourSixTwoSixAgg.getESRSlot(); - // assertEq(fourSixTwoSixAgg.interestAccrued(), 0); - // assertEq(ers.interestLeft, 0); - - // vm.warp(block.timestamp + 2 days); - // fourSixTwoSixAgg.gulp(); - // assertEq(fourSixTwoSixAgg.interestAccrued(), 0); - - // vm.warp(block.timestamp + 1 days); - // assertEq(fourSixTwoSixAgg.interestAccrued(), 0); - // uint256 yield; - // { - // uint256 aggrCurrentStrategyShareBalance = eTST.balanceOf(address(fourSixTwoSixAgg)); - // uint256 aggrCurrentStrategyUnderlyingBalance = eTST.convertToAssets(aggrCurrentStrategyShareBalance); - // uint256 aggrNewStrategyUnderlyingBalance = aggrCurrentStrategyUnderlyingBalance * 11e17 / 1e18; - // yield = aggrNewStrategyUnderlyingBalance - aggrCurrentStrategyUnderlyingBalance; - // assetTST.mint(address(eTST), yield); - // eTST.skim(type(uint256).max, address(fourSixTwoSixAgg)); - // } - // vm.prank(user1); - // fourSixTwoSixAgg.harvest(address(eTST)); - - // assertEq(fourSixTwoSixAgg.interestAccrued(), 0); - - // vm.warp(block.timestamp + 1 days); - // // interest per day 23.809523809523 - // assertEq(fourSixTwoSixAgg.interestAccrued(), 23809523809523809523); - // fourSixTwoSixAgg.gulp(); - // ers = fourSixTwoSixAgg.getESRSlot(); - // assertEq(ers.interestLeft, yield - 23809523809523809523); - - // // move close to end of smearing - // vm.warp(block.timestamp + 11 days); - // fourSixTwoSixAgg.gulp(); - // ers = fourSixTwoSixAgg.getESRSlot(); - // // assertEq(ers.interestLeft, yield - 23809523809523809523*11); - - // // mock a decrease of strategy balance by ers.interestLeft - // uint256 aggrCurrentStrategyBalance = eTST.balanceOf(address(fourSixTwoSixAgg)); - // uint256 aggrCurrentStrategyBalanceAfterNegYield = aggrCurrentStrategyBalance - ers.interestLeft; - // vm.mockCall( - // address(eTST), - // abi.encodeWithSelector(EVault.balanceOf.selector, address(fourSixTwoSixAgg)), - // abi.encode(aggrCurrentStrategyBalanceAfterNegYield) - // ); - // vm.prank(user1); - // fourSixTwoSixAgg.harvest(address(eTST)); - // } - - function testGulp() public { + function testGulpAfterNegativeYieldEqualToInterestLeft() public { + fourSixTwoSixAgg.gulp(); + FourSixTwoSixAgg.ESRSlot memory ers = fourSixTwoSixAgg.getESRSlot(); + assertEq(fourSixTwoSixAgg.interestAccrued(), 0); + assertEq(ers.interestLeft, 0); + + vm.warp(block.timestamp + 2 days); + fourSixTwoSixAgg.gulp(); + assertEq(fourSixTwoSixAgg.interestAccrued(), 0); + + vm.warp(block.timestamp + 1 days); + assertEq(fourSixTwoSixAgg.interestAccrued(), 0); + uint256 yield; + { + uint256 aggrCurrentStrategyShareBalance = eTST.balanceOf(address(fourSixTwoSixAgg)); + uint256 aggrCurrentStrategyUnderlyingBalance = eTST.convertToAssets(aggrCurrentStrategyShareBalance); + uint256 aggrNewStrategyUnderlyingBalance = aggrCurrentStrategyUnderlyingBalance * 11e17 / 1e18; + yield = aggrNewStrategyUnderlyingBalance - aggrCurrentStrategyUnderlyingBalance; + assetTST.mint(address(eTST), yield); + eTST.skim(type(uint256).max, address(fourSixTwoSixAgg)); + } + vm.prank(user1); + fourSixTwoSixAgg.harvest(address(eTST)); + + assertEq(fourSixTwoSixAgg.interestAccrued(), 0); + + vm.warp(block.timestamp + 1 days); + // interest per day 23.809523809523 + assertEq(fourSixTwoSixAgg.interestAccrued(), 23809523809523809523); + fourSixTwoSixAgg.gulp(); + ers = fourSixTwoSixAgg.getESRSlot(); + assertEq(ers.interestLeft, yield - 23809523809523809523); + + // move close to end of smearing + vm.warp(block.timestamp + 11 days); + fourSixTwoSixAgg.gulp(); + ers = fourSixTwoSixAgg.getESRSlot(); + + // mock a decrease of strategy balance by ers.interestLeft + uint256 aggrCurrentStrategyBalance = eTST.balanceOf(address(fourSixTwoSixAgg)); + uint256 aggrCurrentStrategyBalanceAfterNegYield = aggrCurrentStrategyBalance - ers.interestLeft; + vm.mockCall( + address(eTST), + abi.encodeWithSelector(EVault.balanceOf.selector, address(fourSixTwoSixAgg)), + abi.encode(aggrCurrentStrategyBalanceAfterNegYield) + ); + vm.prank(user1); + fourSixTwoSixAgg.harvest(address(eTST)); + } + + function testGulpAfterNegativeYieldBiggerThanInterestLeft() public { fourSixTwoSixAgg.gulp(); FourSixTwoSixAgg.ESRSlot memory ers = fourSixTwoSixAgg.getESRSlot(); assertEq(fourSixTwoSixAgg.interestAccrued(), 0); @@ -141,7 +139,6 @@ contract GulpTest is FourSixTwoSixAggBase { vm.warp(block.timestamp + 11 days); fourSixTwoSixAgg.gulp(); ers = fourSixTwoSixAgg.getESRSlot(); - // assertEq(ers.interestLeft, yield - 23809523809523809523*11); // mock a decrease of strategy balance by ers.interestLeft uint256 aggrCurrentStrategyBalance = eTST.balanceOf(address(fourSixTwoSixAgg)); From 970ead04526ae40ec8ad0f62c1b945f0fe0e06e8 Mon Sep 17 00:00:00 2001 From: Haythem Sellami <17862704+haythemsellami@users.noreply.github.com> Date: Mon, 3 Jun 2024 17:18:16 +0100 Subject: [PATCH 8/8] clean --- src/FourSixTwoSixAgg.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/FourSixTwoSixAgg.sol b/src/FourSixTwoSixAgg.sol index d15dd4fb..4ef2915d 100644 --- a/src/FourSixTwoSixAgg.sol +++ b/src/FourSixTwoSixAgg.sol @@ -10,8 +10,6 @@ import {EVCUtil, IEVC} from "ethereum-vault-connector/utils/EVCUtil.sol"; import {BalanceForwarder, IBalanceForwarder} from "./BalanceForwarder.sol"; import {IRewardStreams} from "reward-streams/interfaces/IRewardStreams.sol"; -import {Test, console2, stdError} from "forge-std/Test.sol"; - /// @dev Do NOT use with fee on transfer tokens /// @dev Do NOT use with rebasing tokens /// @dev Based on https://github.com/euler-xyz/euler-vault-kit/blob/master/src/Synths/EulerSavingsRate.sol @@ -246,7 +244,7 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn _gulp(); } - /// @notice Harvest multiple strategie. + /// @notice Harvest multiple strategies. /// @param _strategies an array of strategy addresses. function harvestMultipleStrategies(address[] calldata _strategies) external nonReentrant { for (uint256 i; i < _strategies.length; ++i) {