Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: negative yield #15

Merged
merged 8 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 35 additions & 14 deletions src/FourSixTwoSixAgg.sol
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,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.
Expand All @@ -230,16 +232,20 @@ 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);

_gulp();
}

/// @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) {
_harvest(_strategies[i]);
Expand Down Expand Up @@ -338,6 +344,10 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn
withdrawalQueue.pop();
}

function updateInterestAccrued() external returns (ESRSlot memory) {
return _updateInterestAccrued();
}

function gulp() external nonReentrant {
_gulp();
}
Expand All @@ -364,7 +374,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.
Expand Down Expand Up @@ -416,7 +426,7 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn
returns (uint256 shares)
{
// Move interest to totalAssetsDeposited
updateInterestAndReturnESRSlotCache();
_updateInterestAccrued();
return super.withdraw(assets, receiver, owner);
}

Expand All @@ -430,14 +440,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);
Expand All @@ -452,7 +461,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
Expand Down Expand Up @@ -519,12 +528,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;

if (toGulp == 0) return;

uint256 maxGulp = type(uint168).max - esrSlotCache.interestLeft;
if (toGulp > maxGulp) toGulp = maxGulp; // cap interest, allowing the vault to function

Expand All @@ -549,7 +561,6 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn

// Harvest profits, also gulps and updates interest
_harvest(_strategy);
_gulp();

Strategy memory strategyData = strategies[_strategy];

Expand Down Expand Up @@ -606,7 +617,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);

Expand All @@ -620,8 +630,19 @@ contract FourSixTwoSixAgg is BalanceForwarder, EVCUtil, ERC4626, AccessControlEn

_accruePerformanceFee(yield);
} else {
// TODO handle losses
revert NegativeYield();
uint256 loss = strategyData.allocated - underlyingBalance;

strategies[strategy].allocated = uint120(underlyingBalance);
totalAllocated -= loss;

ESRSlot memory esrSlotCache = esrSlot;
if (esrSlotCache.interestLeft >= loss) {
esrSlotCache.interestLeft -= uint168(loss);
} else {
totalAssetsDeposited -= loss - esrSlotCache.interestLeft;
esrSlotCache.interestLeft = 0;
}
esrSlot = esrSlotCache;
}
}

Expand Down Expand Up @@ -652,7 +673,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;
Expand Down
154 changes: 154 additions & 0 deletions test/unit/GulpTest.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// 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 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);
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 * 2);
vm.mockCall(
address(eTST),
abi.encodeWithSelector(EVault.balanceOf.selector, address(fourSixTwoSixAgg)),
abi.encode(aggrCurrentStrategyBalanceAfterNegYield)
);
vm.prank(user1);
fourSixTwoSixAgg.harvest(address(eTST));
}
}
Loading
Loading