diff --git a/src/CPPIController.sol b/src/CPPIController.sol index 546357a..89f4ad1 100644 --- a/src/CPPIController.sol +++ b/src/CPPIController.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; +import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; import {CPPIMath} from "./libraries/CPPIMath.sol"; import {FloorPolicy} from "./libraries/FloorPolicy.sol"; import {RebalancePolicy} from "./libraries/RebalancePolicy.sol"; @@ -15,6 +16,7 @@ import {RebalancePolicy} from "./libraries/RebalancePolicy.sol"; /// per spec section 5. contract CPPIController { using FloorPolicy for FloorPolicy.State; + using FixedPointMathLib for uint256; uint256 public immutable multiplierWad; address public immutable vault; @@ -27,6 +29,11 @@ contract CPPIController { uint64 public termNumber; bool public termActive; + /// @dev Last aggregate floor (floorPerShare * supply) computed by assess(); + /// a convenience for the vault, dashboards, and invariant checks. The + /// authoritative floor is per-share, in floorState (audit H3). + uint256 public lastFloorAggregate; + uint256 internal constant MAX_RATE_WAD = 0.2e18; uint256 internal constant MAX_RATE_STEP_WAD = 0.02e18; uint256 internal lastRateWad; @@ -41,6 +48,7 @@ contract CPPIController { error TermNotActive(); error TermStillActive(); error TermNotMatured(); + error ZeroSupply(); modifier onlyVault() { if (msg.sender != vault) revert NotVault(); @@ -65,25 +73,31 @@ contract CPPIController { // ---------- term lifecycle ---------- - function startTerm(uint64 termStart, uint64 termEnd, uint256 nav) external onlyVault { + /// @param nav aggregate shareholder NAV at term start + /// @param supply total share supply at term start (protection is per-share) + function startTerm(uint64 termStart, uint64 termEnd, uint256 nav, uint256 supply) external onlyVault { if (termActive) revert TermStillActive(); + if (supply == 0) revert ZeroSupply(); floorConfig.termStart = termStart; floorConfig.termEnd = termEnd; FloorPolicy.validate(floorConfig); - FloorPolicy.initialize(floorState, floorConfig, nav); + FloorPolicy.initialize(floorState, floorConfig, nav.divWad(supply)); termActive = true; lastRebalanceAt = 0; unchecked { ++termNumber; } - emit TermStarted(termNumber, termStart, termEnd, nav, floorState.protectedAmount); + emit TermStarted(termNumber, termStart, termEnd, nav, floorState.protectedPerShareWad.mulWad(supply)); } /// @notice Settle a matured term. Emits realized shortfall (target: zero). - function settleTerm(uint256 nav) external onlyVault returns (uint256 shortfall) { + /// @dev Protection is per-share: the aggregate protected amount is + /// reconstructed from the current supply, so mid-term mint/burn can + /// no longer desync it from the promise (audit H3). + function settleTerm(uint256 nav, uint256 supply) external onlyVault returns (uint256 shortfall) { if (!termActive) revert TermNotActive(); if (block.timestamp < floorConfig.termEnd) revert TermNotMatured(); - uint256 protectedAmount = floorState.protectedAmount; + uint256 protectedAmount = floorState.protectedPerShareWad.mulWad(supply); shortfall = nav < protectedAmount ? protectedAmount - nav : 0; termActive = false; emit TermSettled(termNumber, nav, protectedAmount, shortfall); @@ -103,14 +117,22 @@ contract CPPIController { /// @notice Compute floor, target exposure, and which trigger (if any) may /// fire now. State-mutating: applies ratchet transitions and the /// monotone clamp. - function assess(uint256 nav, uint256 riskyValue, uint256 rawRateWad) + /// @param nav aggregate shareholder NAV + /// @param supply total share supply (the per-share floor scales to it) + function assess(uint256 nav, uint256 supply, uint256 riskyValue, uint256 rawRateWad) external onlyVault returns (Assessment memory a) { if (!termActive) revert TermNotActive(); + if (supply == 0) revert ZeroSupply(); uint256 rate = _clampRate(rawRateWad); - a.floor = floorState.currentFloor(floorConfig, nav, rate, block.timestamp); + uint256 navPerShare = nav.divWad(supply); + // per-share floor is supply-invariant; reconstruct the aggregate for + // the cushion/target/drift math the vault operates on + uint256 floorPerShare = floorState.currentFloor(floorConfig, navPerShare, rate, block.timestamp); + a.floor = floorPerShare.mulWad(supply); + lastFloorAggregate = a.floor; a.cushion = CPPIMath.cushion(nav, a.floor); a.targetRisky = CPPIMath.targetRisky(nav, a.floor, multiplierWad); a.driftBps = CPPIMath.driftBps(riskyValue, a.targetRisky, nav); @@ -126,13 +148,20 @@ contract CPPIController { // ---------- views ---------- - function protectedAmount() external view returns (uint256) { - return floorState.protectedAmount; + /// @notice Per-share protected amount (the authoritative floor state). + function protectedPerShareWad() external view returns (uint256) { + return floorState.protectedPerShareWad; + } + + /// @notice Aggregate protected amount at a given supply (per-share * supply). + function protectedAmount(uint256 supply) external view returns (uint256) { + return floorState.protectedPerShareWad.mulWad(supply); } - /// @notice Last computed floor (monotone within a term); invariant hook. + /// @notice Last aggregate floor from assess() (monotone within a term + /// at fixed supply); convenience for dashboards / invariant hooks. function lastFloor() external view returns (uint256) { - return floorState.lastFloor; + return lastFloorAggregate; } function maxSurvivableGapBps() external view returns (uint256) { diff --git a/src/CPPIVault.sol b/src/CPPIVault.sol index 26008e5..36da2d2 100644 --- a/src/CPPIVault.sol +++ b/src/CPPIVault.sol @@ -379,14 +379,17 @@ contract CPPIVault is ERC20, Ownable { // ---------- term lifecycle ---------- function startTerm(uint64 duration) external onlyKeeper { - controller.startTerm(uint64(block.timestamp), uint64(block.timestamp) + duration, shareholderNav()); + controller.startTerm( + uint64(block.timestamp), uint64(block.timestamp) + duration, shareholderNav(), totalSupply() + ); } function settleTerm() external onlyKeeper returns (uint256 shortfall) { _accrueManagementFee(); - uint256 protectedAmount = controller.protectedAmount(); + uint256 supply = totalSupply(); + uint256 protectedAmount = controller.protectedAmount(supply); uint256 nav = shareholderNav(); - shortfall = controller.settleTerm(nav); + shortfall = controller.settleTerm(nav, supply); if (performanceFeeBps > 0 && feeRecipient != address(0) && nav > protectedAmount) { uint256 gainWad = nav - protectedAmount; uint256 feeWad = gainWad * performanceFeeBps / 10_000; @@ -402,7 +405,8 @@ contract CPPIVault is ERC20, Ownable { /// keeper-gated; Emergency is permissionless and works while /// paused (spec invariant 5). function rebalance() external returns (RebalancePolicy.Trigger trigger) { - CPPIController.Assessment memory a = controller.assess(shareholderNav(), riskyLeg.value(), rateOracle.rateWad()); + CPPIController.Assessment memory a = + controller.assess(shareholderNav(), totalSupply(), riskyLeg.value(), rateOracle.rateWad()); trigger = a.trigger; if (trigger == RebalancePolicy.Trigger.None) revert NoTrigger(); if (trigger == RebalancePolicy.Trigger.Scheduled && msg.sender != keeper && msg.sender != owner()) { diff --git a/src/libraries/FloorPolicy.sol b/src/libraries/FloorPolicy.sol index 01ad9b6..e9de0a8 100644 --- a/src/libraries/FloorPolicy.sol +++ b/src/libraries/FloorPolicy.sol @@ -7,11 +7,15 @@ import {CPPIMath} from "./CPPIMath.sol"; /// @title FloorPolicy /// @notice The floor-policy family for a CPPI vault: Fixed, Step ratchet, and /// TIPP continuous ratchet, expressed as one floor function. -/// @dev floor = max(PV(protectedAmount, liveRate, timeLeft), policy term), -/// clamped monotone non-decreasing within a term. The Step policy raises -/// protectedAmount itself by k whenever NAV reaches T x floor, so the -/// ratchet survives rate changes and accretion consistently. All values -/// WAD unless noted. +/// @dev The floor is stored and computed PER SHARE, not as a whole-vault +/// aggregate (audit H3). Capital protection is a per-share promise, and +/// the vault's share supply changes mid-term via epoch settlement; a +/// per-share floor is invariant to those mint/burn flows, so a mid-term +/// deposit or redemption can no longer dilute existing holders' floor or +/// manufacture a phantom shortfall. Callers reconstruct the aggregate +/// floor as floorPerShare * totalSupply. All values WAD. +/// floorPerShare = max(PV(protectedPerShare, liveRate, timeLeft), policy), +/// clamped monotone non-decreasing within a term. library FloorPolicy { using FixedPointMathLib for uint256; @@ -25,21 +29,21 @@ library FloorPolicy { Kind kind; uint64 termStart; uint64 termEnd; - uint256 protectionWad; // P: fraction of term-start NAV promised at maturity - uint256 triggerWad; // T (Step only): step fires when nav >= T x floor - uint256 stepWad; // k (Step only): protectedAmount multiplier per step - uint256 ratchetWad; // TIPP only: floor >= ratchet x high-water NAV + uint256 protectionWad; // P: fraction of term-start navPerShare promised at maturity + uint256 triggerWad; // T (Step only): step fires when navPerShare >= T x floorPerShare + uint256 stepWad; // k (Step only): protectedPerShare multiplier per step + uint256 ratchetWad; // TIPP only: floorPerShare >= ratchet x high-water navPerShare } struct State { - uint256 protectedAmount; // absolute asset terms; Step raises this - uint256 hwmNav; // TIPP high-water NAV - uint256 lastFloor; // monotonicity clamp + uint256 protectedPerShareWad; // per-share protected amount; Step raises this + uint256 hwmNavPerShareWad; // TIPP high-water navPerShare + uint256 lastFloorPerShareWad; // monotonicity clamp (per share) uint32 stepCount; } /// @dev Bounds one update's step loop; with k >= MIN_STEP a gap large - /// enough to exhaust this cap cannot occur without NAV growing + /// enough to exhaust this cap cannot occur without navPerShare growing /// >= MIN_STEP^MAX_STEPS x floor in a single update. uint256 internal constant MAX_STEPS_PER_UPDATE = 10; uint256 internal constant MIN_STEP = 1.05e18; @@ -60,33 +64,33 @@ library FloorPolicy { } } - function initialize(State storage s, Config memory c, uint256 termStartNav) internal { - s.protectedAmount = termStartNav.mulWad(c.protectionWad); - s.hwmNav = termStartNav; - s.lastFloor = 0; + function initialize(State storage s, Config memory c, uint256 termStartNavPerShare) internal { + s.protectedPerShareWad = termStartNavPerShare.mulWad(c.protectionWad); + s.hwmNavPerShareWad = termStartNavPerShare; + s.lastFloorPerShareWad = 0; s.stepCount = 0; } - /// @notice Compute the current floor and apply ratchet state transitions. - /// @param nav current vault NAV in asset terms + /// @notice Compute the current per-share floor and apply ratchet transitions. + /// @param navPerShare current vault NAV per share, WAD /// @param rateWad live PT-implied yield (caller clamps per spec section 5) /// @param nowTs current timestamp - function currentFloor(State storage s, Config memory c, uint256 nav, uint256 rateWad, uint256 nowTs) + function currentFloor(State storage s, Config memory c, uint256 navPerShare, uint256 rateWad, uint256 nowTs) internal returns (uint256 floor) { uint256 timeLeft = c.termEnd > nowTs ? c.termEnd - nowTs : 0; - floor = CPPIMath.floorValue(s.protectedAmount, rateWad, timeLeft); + floor = CPPIMath.floorValue(s.protectedPerShareWad, rateWad, timeLeft); if (c.kind == Kind.Tipp) { - if (nav > s.hwmNav) s.hwmNav = nav; - uint256 ratchetFloor = s.hwmNav.mulWad(c.ratchetWad); + if (navPerShare > s.hwmNavPerShareWad) s.hwmNavPerShareWad = navPerShare; + uint256 ratchetFloor = s.hwmNavPerShareWad.mulWad(c.ratchetWad); if (ratchetFloor > floor) floor = ratchetFloor; } else if (c.kind == Kind.Step) { uint256 steps; - while (steps < MAX_STEPS_PER_UPDATE && floor != 0 && nav >= floor.mulWad(c.triggerWad)) { - s.protectedAmount = s.protectedAmount.mulWad(c.stepWad); - floor = CPPIMath.floorValue(s.protectedAmount, rateWad, timeLeft); + while (steps < MAX_STEPS_PER_UPDATE && floor != 0 && navPerShare >= floor.mulWad(c.triggerWad)) { + s.protectedPerShareWad = s.protectedPerShareWad.mulWad(c.stepWad); + floor = CPPIMath.floorValue(s.protectedPerShareWad, rateWad, timeLeft); unchecked { ++steps; } @@ -95,36 +99,36 @@ library FloorPolicy { } // Monotone within the term: rate moves and policy math may only raise it. - if (floor < s.lastFloor) { - floor = s.lastFloor; + if (floor < s.lastFloorPerShareWad) { + floor = s.lastFloorPerShareWad; } else { - s.lastFloor = floor; + s.lastFloorPerShareWad = floor; } } - /// @notice View variant: what the floor would be without state transitions. - function previewFloor(State memory s, Config memory c, uint256 nav, uint256 rateWad, uint256 nowTs) + /// @notice View variant: the per-share floor without state transitions. + function previewFloor(State memory s, Config memory c, uint256 navPerShare, uint256 rateWad, uint256 nowTs) internal pure returns (uint256 floor) { uint256 timeLeft = c.termEnd > nowTs ? c.termEnd - nowTs : 0; - floor = CPPIMath.floorValue(s.protectedAmount, rateWad, timeLeft); + floor = CPPIMath.floorValue(s.protectedPerShareWad, rateWad, timeLeft); if (c.kind == Kind.Tipp) { - uint256 hwm = nav > s.hwmNav ? nav : s.hwmNav; + uint256 hwm = navPerShare > s.hwmNavPerShareWad ? navPerShare : s.hwmNavPerShareWad; uint256 ratchetFloor = hwm.mulWad(c.ratchetWad); if (ratchetFloor > floor) floor = ratchetFloor; } else if (c.kind == Kind.Step) { - uint256 protectedAmount = s.protectedAmount; + uint256 protectedPerShare = s.protectedPerShareWad; uint256 steps; - while (steps < MAX_STEPS_PER_UPDATE && floor != 0 && nav >= floor.mulWad(c.triggerWad)) { - protectedAmount = protectedAmount.mulWad(c.stepWad); - floor = CPPIMath.floorValue(protectedAmount, rateWad, timeLeft); + while (steps < MAX_STEPS_PER_UPDATE && floor != 0 && navPerShare >= floor.mulWad(c.triggerWad)) { + protectedPerShare = protectedPerShare.mulWad(c.stepWad); + floor = CPPIMath.floorValue(protectedPerShare, rateWad, timeLeft); unchecked { ++steps; } } } - if (floor < s.lastFloor) floor = s.lastFloor; + if (floor < s.lastFloorPerShareWad) floor = s.lastFloorPerShareWad; } } diff --git a/test/CPPIController.t.sol b/test/CPPIController.t.sol index c8f8613..4fbc135 100644 --- a/test/CPPIController.t.sol +++ b/test/CPPIController.t.sol @@ -27,17 +27,17 @@ contract CPPIControllerTest is Test { controller = new CPPIController(vault, 2e18, fc, rc); vm.prank(vault); - controller.startTerm(uint64(block.timestamp), uint64(block.timestamp + 365 days), NAV0); + controller.startTerm(uint64(block.timestamp), uint64(block.timestamp + 365 days), NAV0, 1e18); } function test_onlyVault() public { vm.expectRevert(CPPIController.NotVault.selector); - controller.assess(NAV0, 27e18, RATE); + controller.assess(NAV0, 1e18, 27e18, RATE); } function test_assess_matchesWorkedExample() public { vm.prank(vault); - CPPIController.Assessment memory a = controller.assess(NAV0, 0, RATE); + CPPIController.Assessment memory a = controller.assess(NAV0, 1e18, 0, RATE); assertApproxEqRel(a.floor, 86.4710495237090888e18, 1e6); assertApproxEqRel(a.targetRisky, 27.0579009525818224e18, 1e6); // zero holdings vs 27% target: drift ~27%, emergency fires (first rebalance) @@ -47,20 +47,20 @@ contract CPPIControllerTest is Test { function test_assess_scheduledFlow() public { // start at target, drift small: nothing fires vm.startPrank(vault); - CPPIController.Assessment memory a = controller.assess(NAV0, 27.06e18, RATE); + CPPIController.Assessment memory a = controller.assess(NAV0, 1e18, 27.06e18, RATE); assertEq(uint8(a.trigger), uint8(RebalancePolicy.Trigger.None)); controller.recordRebalance(RebalancePolicy.Trigger.Scheduled, a.floor, a.targetRisky); // 1 day later with 3% drift: scheduled vm.warp(block.timestamp + 1 days); - a = controller.assess(NAV0, 24e18, RATE); + a = controller.assess(NAV0, 1e18, 24e18, RATE); assertEq(uint8(a.trigger), uint8(RebalancePolicy.Trigger.Scheduled)); // 2h after a rebalance with 6% drift: emergency (bypasses cadence) controller.recordRebalance(RebalancePolicy.Trigger.Scheduled, a.floor, a.targetRisky); vm.warp(block.timestamp + 2 hours); - a = controller.assess(NAV0, 33.5e18, RATE); + a = controller.assess(NAV0, 1e18, 33.5e18, RATE); assertEq(uint8(a.trigger), uint8(RebalancePolicy.Trigger.Emergency)); vm.stopPrank(); } @@ -71,7 +71,7 @@ contract CPPIControllerTest is Test { vm.startPrank(vault); controller.recordRebalance(RebalancePolicy.Trigger.Scheduled, 0, 0); vm.warp(block.timestamp + 2 hours); - CPPIController.Assessment memory a = controller.assess(92.4e18, 16.2e18, RATE); + CPPIController.Assessment memory a = controller.assess(92.4e18, 1e18, 16.2e18, RATE); assertApproxEqRel(a.cushion, 5.9e18, 0.05e18); assertEq(uint8(a.trigger), uint8(RebalancePolicy.Trigger.None)); vm.stopPrank(); @@ -83,7 +83,7 @@ contract CPPIControllerTest is Test { vm.startPrank(vault); controller.recordRebalance(RebalancePolicy.Trigger.Scheduled, 0, 0); vm.warp(block.timestamp + 2 hours); - CPPIController.Assessment memory a = controller.assess(90e18, 20e18, RATE); + CPPIController.Assessment memory a = controller.assess(90e18, 1e18, 20e18, RATE); assertApproxEqRel(a.cushion, 3.53e18, 0.02e18); assertEq(uint8(a.trigger), uint8(RebalancePolicy.Trigger.Emergency)); vm.stopPrank(); @@ -91,9 +91,9 @@ contract CPPIControllerTest is Test { function test_rateClamp_boundsManipulation() public { vm.startPrank(vault); - CPPIController.Assessment memory a1 = controller.assess(NAV0, 27e18, RATE); + CPPIController.Assessment memory a1 = controller.assess(NAV0, 1e18, 27e18, RATE); // oracle reports an absurd 50% yield: clamped to last + 2% = 6% - CPPIController.Assessment memory a2 = controller.assess(NAV0, 27e18, 0.5e18); + CPPIController.Assessment memory a2 = controller.assess(NAV0, 1e18, 27e18, 0.5e18); // higher rate lowers raw PV, but monotone clamp holds the floor assertGe(a2.floor, a1.floor); vm.stopPrank(); @@ -102,23 +102,23 @@ contract CPPIControllerTest is Test { function test_settleTerm_flow() public { vm.startPrank(vault); vm.expectRevert(CPPIController.TermNotMatured.selector); - controller.settleTerm(95e18); + controller.settleTerm(95e18, 1e18); vm.warp(block.timestamp + 365 days + 1); - uint256 shortfall = controller.settleTerm(95e18); + uint256 shortfall = controller.settleTerm(95e18, 1e18); assertEq(shortfall, 0); // 95 >= 90 protected // second term restarts cleanly - controller.startTerm(uint64(block.timestamp), uint64(block.timestamp + 365 days), 95e18); + controller.startTerm(uint64(block.timestamp), uint64(block.timestamp + 365 days), 95e18, 1e18); assertEq(controller.termNumber(), 2); - assertApproxEqRel(controller.protectedAmount(), 85.5e18, 1e6); // 90% of 95 + assertApproxEqRel(controller.protectedAmount(1e18), 85.5e18, 1e6); // 90% of 95 vm.stopPrank(); } function test_settleTerm_reportsShortfall() public { vm.startPrank(vault); vm.warp(block.timestamp + 365 days + 1); - uint256 shortfall = controller.settleTerm(88e18); + uint256 shortfall = controller.settleTerm(88e18, 1e18); assertEq(shortfall, 2e18); // breached by 2 under the 90 promise vm.stopPrank(); } diff --git a/test/FloorPolicy.t.sol b/test/FloorPolicy.t.sol index e496861..63bce8e 100644 --- a/test/FloorPolicy.t.sol +++ b/test/FloorPolicy.t.sol @@ -22,7 +22,7 @@ contract FloorPolicyHarness { } function protectedAmount() external view returns (uint256) { - return state.protectedAmount; + return state.protectedPerShareWad; } function stepCount() external view returns (uint32) { @@ -30,7 +30,7 @@ contract FloorPolicyHarness { } function hwmNav() external view returns (uint256) { - return state.hwmNav; + return state.hwmNavPerShareWad; } } diff --git a/test/VaultIntegration.t.sol b/test/VaultIntegration.t.sol index 2b7e369..accda20 100644 --- a/test/VaultIntegration.t.sol +++ b/test/VaultIntegration.t.sol @@ -49,6 +49,7 @@ contract VaultIntegrationTest is Test { address keeper = makeAddr("keeper"); address guardian = makeAddr("guardian"); address alice = makeAddr("alice"); + address bob = makeAddr("bob"); address feeTo = makeAddr("feeTo"); function setUp() public { @@ -95,12 +96,67 @@ contract VaultIntegrationTest is Test { usdc.mint(alice, 1_000_000e6); vm.prank(alice); usdc.approve(address(vault), type(uint256).max); + usdc.mint(bob, 1_000_000e6); + vm.prank(bob); + usdc.approve(address(vault), type(uint256).max); // router float usdc.mint(address(router), 10_000_000e6); weth.mint(address(router), 10_000e18); wsteth.mint(address(router), 10_000e18); } + // ---------- H3 regression: per-share floor is supply-invariant ---------- + + function test_h3_midTermDeposit_preservesPerShareFloor() public { + _enter(100_000e6); + // per-share protection at term start + uint256 protectedPerShare0 = controller.protectedPerShareWad(); + uint256 supply0 = vault.totalSupply(); + assertApproxEqRel(protectedPerShare0, 0.9e18, 0.001e18); // 90% of navPerShare (=1) + + // a mid-term depositor settles: mints shares while the term is live + vm.prank(bob); + vault.requestDeposit(100_000e6); + vm.prank(keeper); + vault.settleEpoch(); + + // per-share protection is UNCHANGED (the H3 bug halved it) + assertEq(controller.protectedPerShareWad(), protectedPerShare0); + // aggregate protection scaled up with supply, not frozen + uint256 supply1 = vault.totalSupply(); + assertGt(supply1, supply0); + assertApproxEqRel( + controller.protectedAmount(supply1), controller.protectedAmount(supply0) * supply1 / supply0, 0.0001e18 + ); + } + + function test_h3_midTermRedeem_noPhantomShortfall() public { + _enter(100_000e6); + uint256 protectedPerShare0 = controller.protectedPerShareWad(); + + // half the holder redeems mid-term at fair NAV + uint256 half = vault.balanceOf(alice) / 2; + vm.prank(alice); + vault.requestRedeem(half); + uint256 needWad = half * vault.navPerShare() / 1e18; + vm.prank(keeper); + vault.freeAssets(needWad * 10_100 / 10_000); + vm.prank(keeper); + vault.settleEpoch(); + vm.prank(alice); + vault.claimAssets(); + + // per-share floor unchanged, aggregate scaled down (no phantom breach) + assertEq(controller.protectedPerShareWad(), protectedPerShare0); + + // at maturity: zero shortfall despite the mid-term supply halving + // (inject the PT carry the mock omits so the safe leg accretes to par) + pt.simulateYield(3_000e18); + vm.warp(block.timestamp + 366 days); + vm.prank(keeper); + assertEq(vault.settleTerm(), 0); + } + function _enter(uint256 assets) internal { vm.prank(alice); vault.requestDeposit(assets);