diff --git a/src/core/EulerAggregationVault.sol b/src/core/EulerAggregationVault.sol index ccb224d4..840b677c 100644 --- a/src/core/EulerAggregationVault.sol +++ b/src/core/EulerAggregationVault.sol @@ -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"; @@ -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; @@ -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) diff --git a/src/core/interface/IEulerAggregationVault.sol b/src/core/interface/IEulerAggregationVault.sol index ca3f5160..03e2e9ba 100644 --- a/src/core/interface/IEulerAggregationVault.sol +++ b/src/core/interface/IEulerAggregationVault.sol @@ -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 { @@ -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; } diff --git a/src/core/lib/AmountCapLib.sol b/src/core/lib/AmountCapLib.sol new file mode 100644 index 00000000..d256ebc7 --- /dev/null +++ b/src/core/lib/AmountCapLib.sol @@ -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 security@euler.xyz +/// @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; diff --git a/src/core/lib/ErrorsLib.sol b/src/core/lib/ErrorsLib.sol index 93545216..21324bdf 100644 --- a/src/core/lib/ErrorsLib.sol +++ b/src/core/lib/ErrorsLib.sol @@ -29,4 +29,5 @@ library ErrorsLib { error CanNotToggleStrategyEmergencyStatus(); error CanNotRemoveStrategyInEmergencyStatus(); error CanNotReceiveWithdrawnAsset(); + error BadStrategyCap(); } diff --git a/src/core/module/Rebalance.sol b/src/core/module/Rebalance.sol index 621b3309..29ae5e26 100644 --- a/src/core/module/Rebalance.sol +++ b/src/core/module/Rebalance.sol @@ -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. @@ -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; diff --git a/src/core/module/Strategy.sol b/src/core/module/Strategy.sol index 38a80003..a46c823e 100644 --- a/src/core/module/Strategy.sol +++ b/src/core/module/Strategy.sol @@ -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"; @@ -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. @@ -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); @@ -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) { @@ -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); } @@ -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; @@ -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); diff --git a/test/common/EulerAggregationVaultBase.t.sol b/test/common/EulerAggregationVaultBase.t.sol index eb909388..055dd956 100644 --- a/test/common/EulerAggregationVaultBase.t.sol +++ b/test/common/EulerAggregationVaultBase.t.sol @@ -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; diff --git a/test/e2e/StrategyCapE2ETest.t.sol b/test/e2e/StrategyCapE2ETest.t.sol index 010a0b26..4b91edac 100644 --- a/test/e2e/StrategyCapE2ETest.t.sol +++ b/test/e2e/StrategyCapE2ETest.t.sol @@ -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(); @@ -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; @@ -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 ); } @@ -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); @@ -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() + ); } } } diff --git a/test/fuzz/AdjustAllocationPointsFuzzTest.t.sol b/test/fuzz/AdjustAllocationPointsFuzzTest.t.sol index b9762996..13b89404 100644 --- a/test/fuzz/AdjustAllocationPointsFuzzTest.t.sol +++ b/test/fuzz/AdjustAllocationPointsFuzzTest.t.sol @@ -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(); diff --git a/test/invariant/EulerAggregationLayerInvariants.t.sol b/test/invariant/EulerAggregationLayerInvariants.t.sol index 3b78d1d7..0fab410d 100644 --- a/test/invariant/EulerAggregationLayerInvariants.t.sol +++ b/test/invariant/EulerAggregationLayerInvariants.t.sol @@ -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"; @@ -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 { diff --git a/test/invariant/handler/EulerAggregationVaultHandler.sol b/test/invariant/handler/EulerAggregationVaultHandler.sol index f95ad938..5774abe4 100644 --- a/test/invariant/handler/EulerAggregationVaultHandler.sol +++ b/test/invariant/handler/EulerAggregationVaultHandler.sol @@ -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; @@ -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( @@ -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)); } }