Skip to content

Commit

Permalink
Merge pull request #15 from euler-xyz/feat/negative-yield-socialization
Browse files Browse the repository at this point in the history
Feat: negative yield
  • Loading branch information
haythemsellami authored Jun 4, 2024
2 parents ced8cc3 + 970ead0 commit 69f74df
Show file tree
Hide file tree
Showing 3 changed files with 272 additions and 16 deletions.
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

0 comments on commit 69f74df

Please sign in to comment.