Skip to content

Commit

Permalink
fit Strategy struct in one slot
Browse files Browse the repository at this point in the history
  • Loading branch information
haythemsellami committed Jul 23, 2024
1 parent 4bc449c commit 4c93aec
Show file tree
Hide file tree
Showing 11 changed files with 114 additions and 43 deletions.
7 changes: 4 additions & 3 deletions src/core/EulerAggregationVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {StorageLib as Storage, AggregationVaultStorage} from "./lib/StorageLib.sol";
import {AmountCap} from "./lib/AmountCapLib.sol";
import {ErrorsLib as Errors} from "./lib/ErrorsLib.sol";
import {EventsLib as Events} from "./lib/EventsLib.sol";

Expand Down Expand Up @@ -75,9 +76,9 @@ contract EulerAggregationVault is
$.balanceTracker = _initParams.balanceTracker;
$.strategies[address(0)] = IEulerAggregationVault.Strategy({
allocated: 0,
allocationPoints: _initParams.initialCashAllocationPoints.toUint120(),
allocationPoints: _initParams.initialCashAllocationPoints.toUint96(),
status: IEulerAggregationVault.StrategyStatus.Active,
cap: 0
cap: AmountCap.wrap(0)
});
$.totalAllocationPoints = _initParams.initialCashAllocationPoints;

Expand Down Expand Up @@ -163,7 +164,7 @@ contract EulerAggregationVault is
function removeStrategy(address _strategy) external override onlyRole(STRATEGY_OPERATOR) use(strategyModule) {}

/// @dev See {StrategyModule-setStrategyCap}.
function setStrategyCap(address _strategy, uint256 _cap) external override onlyRole(GUARDIAN) use(strategyModule) {}
function setStrategyCap(address _strategy, uint16 _cap) external override onlyRole(GUARDIAN) use(strategyModule) {}

/// @dev See {StrategyModule-adjustAllocationPoints}.
function adjustAllocationPoints(address _strategy, uint256 _newPoints)
Expand Down
6 changes: 4 additions & 2 deletions src/core/interface/IEulerAggregationVault.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import {AmountCap} from "../lib/AmountCapLib.sol";

interface IEulerAggregationVault {
/// @dev Struct to pass to constrcutor.
struct ConstructorParams {
Expand Down Expand Up @@ -29,8 +31,8 @@ interface IEulerAggregationVault {
/// status: an enum describing the strategy status. Check the enum definition for more details.
struct Strategy {
uint120 allocated;
uint120 allocationPoints;
uint120 cap;
uint96 allocationPoints;
AmountCap cap;
StrategyStatus status;
}

Expand Down
33 changes: 33 additions & 0 deletions src/core/lib/AmountCapLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

/// @title AmountCapLib
/// @dev This is copied from https://github.com/euler-xyz/euler-vault-kit/blob/20973e1dd2037d26e8dea2f4ab2849e53a77855e/src/EVault/shared/types/AmountCap.sol
/// @custom:security-contact [email protected]
/// @author Euler Labs (https://www.eulerlabs.com/)
/// @notice Library for `AmountCap` custom type
/// @dev AmountCaps are 16-bit decimal floating point values:
/// * The least significant 6 bits are the exponent
/// * The most significant 10 bits are the mantissa, scaled by 100
/// * The special value of 0 means limit is not set
/// * This is so that uninitialized storage implies no limit
/// * For an actual cap value of 0, use a zero mantissa and non-zero exponent
library AmountCapLib {
function resolve(AmountCap self) internal pure returns (uint256) {
uint256 amountCap = AmountCap.unwrap(self);

if (amountCap == 0) return type(uint256).max;

unchecked {
// Cannot overflow because this is less than 2**256:
// 10**(2**6 - 1) * (2**10 - 1) = 1.023e+66
return 10 ** (amountCap & 63) * (amountCap >> 6) / 100;
}
}

function toRawUint16(AmountCap self) internal pure returns (uint16) {
return AmountCap.unwrap(self);
}
}

type AmountCap is uint16;
1 change: 1 addition & 0 deletions src/core/lib/ErrorsLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ library ErrorsLib {
error CanNotToggleStrategyEmergencyStatus();
error CanNotRemoveStrategyInEmergencyStatus();
error CanNotReceiveWithdrawnAsset();
error BadStrategyCap();
}
5 changes: 4 additions & 1 deletion src/core/module/Rebalance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import {ContextUpgradeable} from "@openzeppelin-upgradeable/utils/ContextUpgrade
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import {StorageLib, AggregationVaultStorage} from "../lib/StorageLib.sol";
import {AmountCapLib, AmountCap} from "../lib/AmountCapLib.sol";
import {ErrorsLib as Errors} from "../lib/ErrorsLib.sol";
import {EventsLib as Events} from "../lib/EventsLib.sol";

abstract contract RebalanceModule is ContextUpgradeable, Shared {
using SafeERC20 for IERC20;
using SafeCast for uint256;
using AmountCapLib for AmountCap;

/// @notice Rebalance strategies allocation for a specific curated vault.
/// @param _strategies Strategies addresses.
Expand Down Expand Up @@ -51,7 +53,8 @@ abstract contract RebalanceModule is ContextUpgradeable, Shared {
uint256 targetAllocation =
totalAssetsAllocatableCache * strategyData.allocationPoints / totalAllocationPointsCache;

if ((strategyData.cap > 0) && (targetAllocation > strategyData.cap)) targetAllocation = strategyData.cap;
uint120 capAmount = uint120(strategyData.cap.resolve());
if ((AmountCap.unwrap(strategyData.cap) != 0) && (targetAllocation > capAmount)) targetAllocation = capAmount;

uint256 amountToRebalance;
bool isDeposit;
Expand Down
22 changes: 16 additions & 6 deletions src/core/module/Strategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {Shared} from "../common/Shared.sol";
// libs
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import {StorageLib, AggregationVaultStorage} from "../lib/StorageLib.sol";
import {AmountCapLib, AmountCap} from "../lib/AmountCapLib.sol";
import {ErrorsLib as Errors} from "../lib/ErrorsLib.sol";
import {EventsLib as Events} from "../lib/EventsLib.sol";

Expand All @@ -18,6 +19,10 @@ import {EventsLib as Events} from "../lib/EventsLib.sol";
/// @author Euler Labs (https://www.eulerlabs.com/)
abstract contract StrategyModule is Shared {
using SafeCast for uint256;
using AmountCapLib for AmountCap;

// max cap amount, which is the same as the max amount Strategy.allocated can hold.
uint256 public constant MAX_CAP_AMOUNT = type(uint120).max;

/// @notice Adjust a certain strategy's allocation points.
/// @dev Can only be called by an address that have the `GUARDIAN` role.
Expand All @@ -35,7 +40,7 @@ abstract contract StrategyModule is Shared {
revert Errors.InvalidAllocationPoints();
}

$.strategies[_strategy].allocationPoints = _newPoints.toUint120();
$.strategies[_strategy].allocationPoints = _newPoints.toUint96();
$.totalAllocationPoints = $.totalAllocationPoints + _newPoints - strategyDataCache.allocationPoints;

emit Events.AdjustAllocationPoints(_strategy, strategyDataCache.allocationPoints, _newPoints);
Expand All @@ -46,7 +51,7 @@ abstract contract StrategyModule is Shared {
/// @dev By default, cap is set to 0.
/// @param _strategy Strategy address.
/// @param _cap Cap amount
function setStrategyCap(address _strategy, uint256 _cap) external virtual nonReentrant {
function setStrategyCap(address _strategy, uint16 _cap) external virtual nonReentrant {
AggregationVaultStorage storage $ = StorageLib._getAggregationVaultStorage();

if ($.strategies[_strategy].status != IEulerAggregationVault.StrategyStatus.Active) {
Expand All @@ -57,7 +62,12 @@ abstract contract StrategyModule is Shared {
revert Errors.NoCapOnCashReserveStrategy();
}

$.strategies[_strategy].cap = _cap.toUint120();
AmountCap strategyCap = AmountCap.wrap(_cap);
// The raw uint16 cap amount == 0 is a special value. See comments in AmountCapLib.sol
// Max cap is max amount that can be allocated into strategy (max uint120).
if (_cap != 0 && strategyCap.resolve() > MAX_CAP_AMOUNT) revert Errors.BadStrategyCap();

$.strategies[_strategy].cap = strategyCap;

emit Events.SetStrategyCap(_strategy, _cap);
}
Expand Down Expand Up @@ -116,9 +126,9 @@ abstract contract StrategyModule is Shared {

$.strategies[_strategy] = IEulerAggregationVault.Strategy({
allocated: 0,
allocationPoints: _allocationPoints.toUint120(),
allocationPoints: _allocationPoints.toUint96(),
status: IEulerAggregationVault.StrategyStatus.Active,
cap: 0
cap: AmountCap.wrap(0)
});

$.totalAllocationPoints += _allocationPoints;
Expand Down Expand Up @@ -152,7 +162,7 @@ abstract contract StrategyModule is Shared {
$.totalAllocationPoints -= strategyStorage.allocationPoints;
strategyStorage.status = IEulerAggregationVault.StrategyStatus.Inactive;
strategyStorage.allocationPoints = 0;
strategyStorage.cap = 0;
strategyStorage.cap = AmountCap.wrap(0);

// remove from withdrawalQueue
IWithdrawalQueue($.withdrawalQueue).removeStrategyFromWithdrawalQueue(_strategy);
Expand Down
4 changes: 4 additions & 0 deletions test/common/EulerAggregationVaultBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ import {WithdrawalQueue} from "../../src/plugin/WithdrawalQueue.sol";
import {Strategy} from "../../src/core/module/Strategy.sol";
// libs
import {ErrorsLib} from "../../src/core/lib/ErrorsLib.sol";
import {ErrorsLib} from "../../src/core/lib/ErrorsLib.sol";
import {AmountCapLib as AggAmountCapLib, AmountCap as AggAmountCap} from "../../src/core/lib/AmountCapLib.sol";

contract EulerAggregationVaultBase is EVaultTestBase {
using AggAmountCapLib for AggAmountCap;

uint256 public constant CASH_RESERVE_ALLOCATION_POINTS = 1000e18;

address deployer;
Expand Down
57 changes: 33 additions & 24 deletions test/e2e/StrategyCapE2ETest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ import {
IRMTestDefault,
TestERC20,
IEulerAggregationVault,
ErrorsLib
ErrorsLib,
AggAmountCapLib,
AggAmountCap
} from "../common/EulerAggregationVaultBase.t.sol";

contract StrategyCapE2ETest is EulerAggregationVaultBase {
uint256 user1InitialBalance = 100000e18;

using AggAmountCapLib for AggAmountCap;

function setUp() public virtual override {
super.setUp();

Expand All @@ -26,40 +30,43 @@ contract StrategyCapE2ETest is EulerAggregationVaultBase {
}

function testSetCap() public {
uint256 cap = 1000000e18;
uint256 cap = 100e18;

assertEq((eulerAggregationVault.getStrategy(address(eTST))).cap, 0);
assertEq(AggAmountCap.unwrap(eulerAggregationVault.getStrategy(address(eTST)).cap), 0);

vm.prank(manager);
eulerAggregationVault.setStrategyCap(address(eTST), cap);
// 100e18 cap
eulerAggregationVault.setStrategyCap(address(eTST), 6420);

IEulerAggregationVault.Strategy memory strategy = eulerAggregationVault.getStrategy(address(eTST));

assertEq(strategy.cap, cap);
assertEq(strategy.cap.resolve(), cap);
assertEq(AggAmountCap.unwrap(strategy.cap), 6420);
}

function testSetCapForInactiveStrategy() public {
uint256 cap = 1000000e18;

vm.prank(manager);
vm.expectRevert(ErrorsLib.InactiveStrategy.selector);
eulerAggregationVault.setStrategyCap(address(0x2), cap);
eulerAggregationVault.setStrategyCap(address(0x2), 1);
}

function testSetCapForCashReserveStrategy() public {
uint256 cap = 1000000e18;

vm.prank(manager);
vm.expectRevert(ErrorsLib.NoCapOnCashReserveStrategy.selector);
eulerAggregationVault.setStrategyCap(address(0), cap);
eulerAggregationVault.setStrategyCap(address(0), 1);
}

function testRebalanceAfterHittingCap() public {
address[] memory strategiesToRebalance = new address[](1);

uint256 cap = 3333333333333333333333;
uint120 cappedBalance = 3000000000000000000000;
// 3000000000000000000000 cap
uint16 cap = 19221;
vm.prank(manager);
eulerAggregationVault.setStrategyCap(address(eTST), cap);
IEulerAggregationVault.Strategy memory strategy = eulerAggregationVault.getStrategy(address(eTST));
assertEq(strategy.cap.resolve(), cappedBalance);
assertEq(AggAmountCap.unwrap(strategy.cap), cap);

uint256 amountToDeposit = 10000e18;

Expand Down Expand Up @@ -95,11 +102,11 @@ contract StrategyCapE2ETest is EulerAggregationVaultBase {
strategiesToRebalance[0] = address(eTST);
eulerAggregationVault.rebalance(strategiesToRebalance);

assertEq(eulerAggregationVault.totalAllocated(), expectedStrategyCash);
assertEq(eTST.convertToAssets(eTST.balanceOf(address(eulerAggregationVault))), expectedStrategyCash);
assertTrue(expectedStrategyCash > cappedBalance);
assertEq(eulerAggregationVault.totalAllocated(), cappedBalance);
assertEq(eTST.convertToAssets(eTST.balanceOf(address(eulerAggregationVault))), cappedBalance);
assertEq(
(eulerAggregationVault.getStrategy(address(eTST))).allocated,
strategyBefore.allocated + expectedStrategyCash
(eulerAggregationVault.getStrategy(address(eTST))).allocated, strategyBefore.allocated + cappedBalance
);
}

Expand Down Expand Up @@ -147,11 +154,8 @@ contract StrategyCapE2ETest is EulerAggregationVaultBase {

assertEq(eTST.convertToAssets(eTST.balanceOf(address(eulerAggregationVault))), strategyBefore.allocated);

uint256 expectedStrategyCash = eulerAggregationVault.totalAssetsAllocatable()
* strategyBefore.allocationPoints / eulerAggregationVault.totalAllocationPoints();

// set cap 10% less than target allocation
uint256 cap = expectedStrategyCash * 9e17 / 1e18;
// set cap at around 10% less than target allocation
uint16 cap = 19219;
vm.prank(manager);
eulerAggregationVault.setStrategyCap(address(eTST), cap);

Expand All @@ -160,9 +164,14 @@ contract StrategyCapE2ETest is EulerAggregationVaultBase {
strategiesToRebalance[0] = address(eTST);
eulerAggregationVault.rebalance(strategiesToRebalance);

assertEq(eulerAggregationVault.totalAllocated(), cap);
assertEq(eTST.convertToAssets(eTST.balanceOf(address(eulerAggregationVault))), cap);
assertEq((eulerAggregationVault.getStrategy(address(eTST))).allocated, strategyBefore.allocated + cap);
assertEq(eulerAggregationVault.totalAllocated(), AggAmountCap.wrap(cap).resolve());
assertEq(
eTST.convertToAssets(eTST.balanceOf(address(eulerAggregationVault))), AggAmountCap.wrap(cap).resolve()
);
assertEq(
(eulerAggregationVault.getStrategy(address(eTST))).allocated,
strategyBefore.allocated + AggAmountCap.wrap(cap).resolve()
);
}
}
}
2 changes: 1 addition & 1 deletion test/fuzz/AdjustAllocationPointsFuzzTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ contract AdjustAllocationsPointsFuzzTest is EulerAggregationVaultBase {
}

function testFuzzAdjustAllocationPoints(uint256 _newAllocationPoints) public {
_newAllocationPoints = bound(_newAllocationPoints, 1, type(uint120).max);
_newAllocationPoints = bound(_newAllocationPoints, 1, type(uint96).max);

uint256 strategyAllocationPoints = (eulerAggregationVault.getStrategy(address(eTST))).allocationPoints;
uint256 totalAllocationPointsBefore = eulerAggregationVault.totalAllocationPoints();
Expand Down
5 changes: 3 additions & 2 deletions test/invariant/EulerAggregationLayerInvariants.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
IWithdrawalQueue,
IEVault,
TestERC20,
IEulerAggregationVault
IEulerAggregationVault,
AggAmountCap
} from "../common/EulerAggregationVaultBase.t.sol";
import {Actor} from "./util/Actor.sol";
import {Strategy} from "./util/Strategy.sol";
Expand Down Expand Up @@ -170,7 +171,7 @@ contract EulerAggregationVaultInvariants is EulerAggregationVaultBase {
}

function invariant_cashReserveStrategyCap() public view {
assertEq(eulerAggregationVault.getStrategy(address(0)).cap, 0);
assertEq(AggAmountCap.unwrap(eulerAggregationVault.getStrategy(address(0)).cap), 0);
}

function invariant_votingPower() public view {
Expand Down
15 changes: 11 additions & 4 deletions test/invariant/handler/EulerAggregationVaultHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@ import {
IEulerAggregationVault,
ErrorsLib,
IERC4626,
WithdrawalQueue
WithdrawalQueue,
AggAmountCapLib,
AggAmountCap
} from "../../common/EulerAggregationVaultBase.t.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {Actor} from "../util/Actor.sol";
import {Strategy} from "../util/Strategy.sol";

contract EulerAggregationVaultHandler is Test {
using AggAmountCapLib for AggAmountCap;

Actor internal actorUtil;
Strategy internal strategyUtil;
EulerAggregationVault internal eulerAggVault;
Expand Down Expand Up @@ -101,9 +105,12 @@ contract EulerAggregationVaultHandler is Test {
assertEq(strategyAfter.allocationPoints, ghost_allocationPoints[strategyAddr]);
}

function setStrategyCap(uint256 _strategyIndexSeed, uint256 _cap) external {
function setStrategyCap(uint256 _strategyIndexSeed, uint16 _cap) external {
address strategyAddr = strategyUtil.fetchStrategy(_strategyIndexSeed);

uint256 strategyCapAmount = AggAmountCap.wrap(_cap).resolve();
vm.assume(strategyCapAmount <= eulerAggVault.MAX_CAP_AMOUNT());

IEulerAggregationVault.Strategy memory strategyBefore = eulerAggVault.getStrategy(strategyAddr);

(currentActor, success, returnData) = actorUtil.initiateExactActorCall(
Expand All @@ -114,9 +121,9 @@ contract EulerAggregationVaultHandler is Test {

IEulerAggregationVault.Strategy memory strategyAfter = eulerAggVault.getStrategy(strategyAddr);
if (success) {
assertEq(strategyAfter.cap, _cap);
assertEq(AggAmountCap.unwrap(strategyAfter.cap), _cap);
} else {
assertEq(strategyAfter.cap, strategyBefore.cap);
assertEq(AggAmountCap.unwrap(strategyAfter.cap), AggAmountCap.unwrap(strategyBefore.cap));
}
}

Expand Down

0 comments on commit 4c93aec

Please sign in to comment.