From 2ea467f0074ae623b9ef4de7378079352723bd22 Mon Sep 17 00:00:00 2001 From: saintsama98 Date: Wed, 13 May 2026 22:52:16 +0530 Subject: [PATCH 1/2] first iteration of morpho strategy --- src/facets/strategies/MorphoStrategyFacet.sol | 90 +++++++++++++++++++ src/interfaces/external/IMorpho.sol | 6 ++ 2 files changed, 96 insertions(+) create mode 100644 src/facets/strategies/MorphoStrategyFacet.sol create mode 100644 src/interfaces/external/IMorpho.sol diff --git a/src/facets/strategies/MorphoStrategyFacet.sol b/src/facets/strategies/MorphoStrategyFacet.sol new file mode 100644 index 0000000..e160313 --- /dev/null +++ b/src/facets/strategies/MorphoStrategyFacet.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; + +import {LibDiamond} from "../../libraries/LibDiamond.sol"; +import {IMorpho} from "../../interfaces/external/IMorpho.sol"; + +contract MorphoStrategyFacet { + using SafeERC20 for IERC20; + + //errors + + error MorphoVaultNotConfigured(); + error MorphoAssetNotConfigured(); + error MorphoAssetMismatch(); + error MorphoSlippage(uint256 expected, uint256 received); + + //events + + event MorphoVaultSet(IMorpho indexed vault); + + + bytes32 internal constant MORPHO_STORAGE_SLOT = ""; + + struct MorphoStorage{ + IMorpho vault; + } + + function _ms() internal pure returns (MorphoStorage storage s) { + bytes32 slot = MORPHO_STORAGE_SLOT; + assembly { + s.slot := slot + } + } + + + function MorphoSetVaultConfig(IMorpho vault) external { + //gates + LibDiamond.enforceIsContractOwner(); + if (address(vault) == address(0)) revert MorphoVaultNotConfigured(); + //imp check for underlying asset + if (address(vault.asset()) == address(0)) revert MorphoAssetNotConfigured(); + //impt check for underlying asset is the same as the vault's asset + if (address(vault.asset()) != address(IERC4626(address(this)).asset())) revert MorphoAssetMismatch(); + + MorphoStorage storage s = _ms(); + s.vault = vault; + emit MorphoVaultSet(vault); + } + + //@notice there are three priciple functionalities that strategy user + //can access: "checkTotalAssets", "morphoDeposit", "morphoWithdraw" + //"morphoHarvest" + + function morphoTotalAssets() external view returns (uint256){ + MorphoStorage storage s = _ms(); + if (address(s.vault) == address(0)) revert MorphoVaultNotConfigured(); + return s.vault.convertToAssets(s.vault.balanceOf(address(this))); + + } + + function morphoDeposit (uint256 amount) external { + MorphoStorage storage s = _ms(); + if (address(s.vault) == address(0)) revert MorphoVaultNotConfigured(); + IERC20 underlying = IERC20(IERC4626(address(this)).asset()); + underlying.forceApprove(address(s.vault), amount); + uint256 expected = s.vault.previewDeposit(amount); + uint256 shares = s.vault.deposit(amount, address(this)); + if (shares < expected) revert MorphoSlippage(expected, shares); + } + + function morphoWithdraw (uint256 assets) external{ + MorphoStorage storage s = _ms(); + if (address(s.vault) == address(0)) revert MorphoVaultNotConfigured(); + s.vault.withdraw(assets,address(this), address(this)); + } + + //view + + function morphoVault() external view returns (IMorpho) { + MorphoStorage storage s = _ms(); + if (address(s.vault) == address(0)) revert MorphoVaultNotConfigured(); + return s.vault; + } + + +} \ No newline at end of file diff --git a/src/interfaces/external/IMorpho.sol b/src/interfaces/external/IMorpho.sol new file mode 100644 index 0000000..39658eb --- /dev/null +++ b/src/interfaces/external/IMorpho.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; + +interface IMorpho is IERC4626 {} \ No newline at end of file From ddfa6ebc13f0c3bb19a2b4f0751dd08fc9c4f2a5 Mon Sep 17 00:00:00 2001 From: saintsama98 Date: Wed, 13 May 2026 23:03:12 +0530 Subject: [PATCH 2/2] morpho strategy --- src/Vault.sol | 36 +++--- src/facets/AllocatorFacet.sol | 22 ++-- src/facets/DiamondCutFacet.sol | 4 +- src/facets/DiamondLoupeFacet.sol | 4 +- src/facets/FeeFacet.sol | 4 +- src/facets/HarvestFacet.sol | 10 +- src/facets/OwnershipFacet.sol | 4 +- src/facets/strategies/AaveStrategyFacet.sol | 15 ++- src/facets/strategies/IdleStrategyFacet.sol | 4 +- src/facets/strategies/MorphoStrategyFacet.sol | 83 ++++++++++---- src/interfaces/IDiamondCut.sol | 2 +- src/interfaces/external/IMorpho.sol | 4 +- src/libraries/LibDiamond.sol | 11 +- src/libraries/LibFees.sol | 7 +- test/integration/AaveStrategy.fork.t.sol | 61 +++++----- test/mocks/MockProtocol.sol | 2 +- test/mocks/MockStrategyFacet.sol | 9 +- test/unit/Allocator.t.sol | 104 ++++++++---------- test/unit/Fees.t.sol | 74 ++++++------- test/unit/Harvest.t.sol | 46 ++++---- test/unit/Vault.t.sol | 40 +++---- 21 files changed, 275 insertions(+), 271 deletions(-) diff --git a/src/Vault.sol b/src/Vault.sol index ada8102..f7f140d 100644 --- a/src/Vault.sol +++ b/src/Vault.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ERC4626 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IDiamond} from "./interfaces/IDiamond.sol"; -import {LibDiamond} from "./libraries/LibDiamond.sol"; -import {LibAllocator} from "./libraries/LibAllocator.sol"; -import {LibFees} from "./libraries/LibFees.sol"; +import { IDiamond } from "./interfaces/IDiamond.sol"; +import { LibDiamond } from "./libraries/LibDiamond.sol"; +import { LibAllocator } from "./libraries/LibAllocator.sol"; +import { LibFees } from "./libraries/LibFees.sol"; /// @title Vault Router — modular ERC-4626 vault on the EIP-2535 Diamond pattern. /// @notice Vault.sol owns the ERC-4626 surface (deposit/withdraw/totalAssets) and @@ -28,7 +28,10 @@ contract Vault is ERC4626 { IDiamond.FacetCut[] memory diamondCut_, address init_, bytes memory initCalldata_ - ) ERC20(name_, symbol_) ERC4626(asset_) { + ) + ERC20(name_, symbol_) + ERC4626(asset_) + { LibDiamond.setContractOwner(initialOwner); LibDiamond.diamondCut(diamondCut_, init_, initCalldata_); } @@ -52,8 +55,7 @@ contract Vault is ERC4626 { bytes32 id = s.strategyIds[i]; LibAllocator.StrategyConfig storage cfg = s.configs[id]; if (!cfg.active) continue; - (bool ok, bytes memory data) = - address(this).staticcall(abi.encodeWithSelector(cfg.totalAssetsSelector)); + (bool ok, bytes memory data) = address(this).staticcall(abi.encodeWithSelector(cfg.totalAssetsSelector)); if (!ok) revert StrategyTotalAssetsCallFailed(id); total += abi.decode(data, (uint256)); } @@ -64,12 +66,7 @@ contract Vault is ERC4626 { // Fee-accrual hooks // ----------------------------------------------------------------------- - function _deposit( - address caller, - address receiver, - uint256 assets, - uint256 shares - ) internal override { + function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal override { // Pre-accrue so the new depositor doesn't dilute the perf-fee owed on // yield earned by existing holders. No-op on the very first deposit // (supply == 0 → early return). @@ -86,7 +83,10 @@ contract Vault is ERC4626 { address owner, uint256 assets, uint256 shares - ) internal override { + ) + internal + override + { _accrueFees(); super._withdraw(caller, receiver, owner, assets, shares); _accrueFees(); @@ -157,5 +157,5 @@ contract Vault is ERC4626 { } } - receive() external payable {} + receive() external payable { } } diff --git a/src/facets/AllocatorFacet.sol b/src/facets/AllocatorFacet.sol index e57bdee..94bf1b8 100644 --- a/src/facets/AllocatorFacet.sol +++ b/src/facets/AllocatorFacet.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; -import {LibDiamond} from "../libraries/LibDiamond.sol"; -import {LibAllocator} from "../libraries/LibAllocator.sol"; +import { LibDiamond } from "../libraries/LibDiamond.sol"; +import { LibAllocator } from "../libraries/LibAllocator.sol"; /// @title AllocatorFacet /// @notice Curator-controlled allocation policy. Registers strategy facets, @@ -203,8 +203,7 @@ contract AllocatorFacet { function strategyTotalAssets(bytes32 strategyId) external view returns (uint256) { LibAllocator.StrategyConfig memory cfg = LibAllocator.allocatorStorage().configs[strategyId]; if (!cfg.active) revert StrategyNotRegistered(strategyId); - (bool ok, bytes memory data) = - address(this).staticcall(abi.encodeWithSelector(cfg.totalAssetsSelector)); + (bool ok, bytes memory data) = address(this).staticcall(abi.encodeWithSelector(cfg.totalAssetsSelector)); if (!ok) revert StrategyTotalAssetsCallFailed(strategyId); return abi.decode(data, (uint256)); } @@ -235,7 +234,10 @@ contract AllocatorFacet { return IERC20(asset).balanceOf(address(this)); } - function _strategyTotalAssetsInternal(LibAllocator.StrategyConfig memory cfg, bytes32 strategyId) + function _strategyTotalAssetsInternal( + LibAllocator.StrategyConfig memory cfg, + bytes32 strategyId + ) internal view returns (uint256) @@ -264,11 +266,7 @@ contract AllocatorFacet { } } - function _effectiveCap(LibAllocator.AllocatorStorage storage s, bytes32 strategyId) - internal - view - returns (uint16) - { + function _effectiveCap(LibAllocator.AllocatorStorage storage s, bytes32 strategyId) internal view returns (uint16) { uint16 perStrategy = s.configs[strategyId].capBps; uint16 glob = s.globalMaxStrategyCapBps; if (perStrategy == 0 && glob == 0) return LibAllocator.BPS_DENOMINATOR; diff --git a/src/facets/DiamondCutFacet.sol b/src/facets/DiamondCutFacet.sol index 801e41c..83cdfbd 100644 --- a/src/facets/DiamondCutFacet.sol +++ b/src/facets/DiamondCutFacet.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {IDiamondCut} from "../interfaces/IDiamondCut.sol"; -import {LibDiamond} from "../libraries/LibDiamond.sol"; +import { IDiamondCut } from "../interfaces/IDiamondCut.sol"; +import { LibDiamond } from "../libraries/LibDiamond.sol"; contract DiamondCutFacet is IDiamondCut { /// @inheritdoc IDiamondCut diff --git a/src/facets/DiamondLoupeFacet.sol b/src/facets/DiamondLoupeFacet.sol index 8bd2edb..ca7fc36 100644 --- a/src/facets/DiamondLoupeFacet.sol +++ b/src/facets/DiamondLoupeFacet.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {IDiamondLoupe} from "../interfaces/IDiamondLoupe.sol"; -import {LibDiamond} from "../libraries/LibDiamond.sol"; +import { IDiamondLoupe } from "../interfaces/IDiamondLoupe.sol"; +import { LibDiamond } from "../libraries/LibDiamond.sol"; contract DiamondLoupeFacet is IDiamondLoupe { /// @inheritdoc IDiamondLoupe diff --git a/src/facets/FeeFacet.sol b/src/facets/FeeFacet.sol index 72b5ede..ac150e6 100644 --- a/src/facets/FeeFacet.sol +++ b/src/facets/FeeFacet.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {LibDiamond} from "../libraries/LibDiamond.sol"; -import {LibFees} from "../libraries/LibFees.sol"; +import { LibDiamond } from "../libraries/LibDiamond.sol"; +import { LibFees } from "../libraries/LibFees.sol"; /// @title FeeFacet /// @notice Curator-gated setters and public readers for the vault's fee parameters. diff --git a/src/facets/HarvestFacet.sol b/src/facets/HarvestFacet.sol index 53c3812..aa579bb 100644 --- a/src/facets/HarvestFacet.sol +++ b/src/facets/HarvestFacet.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {LibDiamond} from "../libraries/LibDiamond.sol"; -import {LibAllocator} from "../libraries/LibAllocator.sol"; +import { LibDiamond } from "../libraries/LibDiamond.sol"; +import { LibAllocator } from "../libraries/LibAllocator.sol"; /// @title HarvestFacet /// @notice Curator-triggered harvest. Invokes the per-strategy harvest selector @@ -27,8 +27,7 @@ contract HarvestFacet { LibAllocator.StrategyConfig memory cfg = LibAllocator.allocatorStorage().configs[strategyId]; if (!cfg.active) revert StrategyNotRegistered(strategyId); if (cfg.harvestSelector != bytes4(0)) { - (bool ok, bytes memory ret) = - address(this).call(abi.encodeWithSelector(cfg.harvestSelector)); + (bool ok, bytes memory ret) = address(this).call(abi.encodeWithSelector(cfg.harvestSelector)); if (!ok) { if (ret.length > 0) { assembly { @@ -51,8 +50,7 @@ contract HarvestFacet { LibAllocator.StrategyConfig memory cfg = s.configs[id]; if (!cfg.active) continue; if (cfg.harvestSelector != bytes4(0)) { - (bool ok, bytes memory ret) = - address(this).call(abi.encodeWithSelector(cfg.harvestSelector)); + (bool ok, bytes memory ret) = address(this).call(abi.encodeWithSelector(cfg.harvestSelector)); if (!ok) { if (ret.length > 0) { assembly { diff --git a/src/facets/OwnershipFacet.sol b/src/facets/OwnershipFacet.sol index 8aa3b2e..5b83ed4 100644 --- a/src/facets/OwnershipFacet.sol +++ b/src/facets/OwnershipFacet.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {IERC173} from "../interfaces/IERC173.sol"; -import {LibDiamond} from "../libraries/LibDiamond.sol"; +import { IERC173 } from "../interfaces/IERC173.sol"; +import { LibDiamond } from "../libraries/LibDiamond.sol"; contract OwnershipFacet is IERC173 { /// @inheritdoc IERC173 diff --git a/src/facets/strategies/AaveStrategyFacet.sol b/src/facets/strategies/AaveStrategyFacet.sol index 42ba12e..6bb1eec 100644 --- a/src/facets/strategies/AaveStrategyFacet.sol +++ b/src/facets/strategies/AaveStrategyFacet.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; -import {LibDiamond} from "../../libraries/LibDiamond.sol"; -import {IAavePool} from "../../interfaces/external/IAavePool.sol"; +import { LibDiamond } from "../../libraries/LibDiamond.sol"; +import { IAavePool } from "../../interfaces/external/IAavePool.sol"; /// @title AaveStrategyFacet /// @notice Strategy facet that supplies the vault's asset to Aave V3 and @@ -23,8 +23,7 @@ contract AaveStrategyFacet { event AaveConfigSet(IAavePool indexed pool, IERC20 indexed aToken); /// @dev erc7201:vaultrouter.strategy.aave - bytes32 internal constant AAVE_STORAGE_SLOT = - 0x340080245a7d3e67835fb5055646777827d09fc7212fda4d8d724367e1215700; + bytes32 internal constant AAVE_STORAGE_SLOT = 0x340080245a7d3e67835fb5055646777827d09fc7212fda4d8d724367e1215700; struct AaveStorage { IAavePool pool; @@ -88,7 +87,7 @@ contract AaveStrategyFacet { /// @notice No-op for Aave V3 — supply yield auto-accrues into the aToken's /// rebasing balance, so there's nothing to claim. - function aaveHarvest() external pure {} + function aaveHarvest() external pure { } // ----------------------------------------------------------------------- // Readers diff --git a/src/facets/strategies/IdleStrategyFacet.sol b/src/facets/strategies/IdleStrategyFacet.sol index 9e32f5e..3c573dc 100644 --- a/src/facets/strategies/IdleStrategyFacet.sol +++ b/src/facets/strategies/IdleStrategyFacet.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; /// @title IdleStrategyFacet /// @notice Trivial strategy that holds the vault's underlying asset idle in the diff --git a/src/facets/strategies/MorphoStrategyFacet.sol b/src/facets/strategies/MorphoStrategyFacet.sol index e160313..8a04108 100644 --- a/src/facets/strategies/MorphoStrategyFacet.sol +++ b/src/facets/strategies/MorphoStrategyFacet.sol @@ -1,34 +1,53 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; - -import {LibDiamond} from "../../libraries/LibDiamond.sol"; -import {IMorpho} from "../../interfaces/external/IMorpho.sol"; - +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; + +import { LibDiamond } from "../../libraries/LibDiamond.sol"; +import { IMorpho } from "../../interfaces/external/IMorpho.sol"; + +/// @title MorphoStrategyFacet +/// @notice Strategy facet that supplies the diamond's underlying asset to a +/// Metamorpho ERC4626 vault and reports its position via the vault's +/// share balance. +/// @dev Selectors are prefixed with `morpho*` so the facet coexists with other +/// strategy facets in the same Diamond without selector collisions. +/// State lives at EIP-7201 slot `vaultrouter.strategy.morpho`. contract MorphoStrategyFacet { using SafeERC20 for IERC20; //errors + /// @notice Thrown when the Metamorpho vault has not yet been configured. error MorphoVaultNotConfigured(); + /// @notice Thrown when the configured vault returns a zero underlying asset. error MorphoAssetNotConfigured(); + /// @notice Thrown when the vault's underlying asset differs from the diamond's asset. error MorphoAssetMismatch(); + /// @notice Thrown when shares minted by `deposit` are below the slippage bound. + /// @param expected Shares predicted by `previewDeposit`. + /// @param received Shares actually minted by the vault. error MorphoSlippage(uint256 expected, uint256 received); //events + /// @notice Emitted when the Metamorpho vault is configured (or reconfigured). + /// @param vault The Metamorpho ERC4626 vault now active for this strategy. event MorphoVaultSet(IMorpho indexed vault); - - bytes32 internal constant MORPHO_STORAGE_SLOT = ""; + /// @notice EIP-7201 namespaced storage slot for the Morpho strategy state. + /// @dev erc7201:vaultrouter.strategy.morpho + bytes32 internal constant MORPHO_STORAGE_SLOT = 0xf4b3fd2d8603f5a74e31f8c3250c4c70408eaa33a47a7d4535036bfa6799e900; - struct MorphoStorage{ + /// @notice Storage layout for the Morpho strategy facet. + /// @dev `vault` is the configured Metamorpho ERC4626 vault. + struct MorphoStorage { IMorpho vault; } + /// @dev Returns the EIP-7201 namespaced storage struct for this facet. function _ms() internal pure returns (MorphoStorage storage s) { bytes32 slot = MORPHO_STORAGE_SLOT; assembly { @@ -36,16 +55,19 @@ contract MorphoStrategyFacet { } } - + /// @notice Configure the Metamorpho vault this strategy routes capital to. + /// @dev Owner-gated. Validates that the vault's underlying asset matches + /// the diamond's ERC4626 underlying asset before persisting. + /// @param vault The Metamorpho ERC4626 vault to associate with this strategy. function MorphoSetVaultConfig(IMorpho vault) external { //gates LibDiamond.enforceIsContractOwner(); if (address(vault) == address(0)) revert MorphoVaultNotConfigured(); - //imp check for underlying asset + //imp check for underlying asset if (address(vault.asset()) == address(0)) revert MorphoAssetNotConfigured(); //impt check for underlying asset is the same as the vault's asset if (address(vault.asset()) != address(IERC4626(address(this)).asset())) revert MorphoAssetMismatch(); - + MorphoStorage storage s = _ms(); s.vault = vault; emit MorphoVaultSet(vault); @@ -55,14 +77,26 @@ contract MorphoStrategyFacet { //can access: "checkTotalAssets", "morphoDeposit", "morphoWithdraw" //"morphoHarvest" - function morphoTotalAssets() external view returns (uint256){ + /// @notice Report the underlying-asset value of the diamond's Morpho position. + /// @dev Returns `vault.convertToAssets(vault.balanceOf(diamond))`. This is the + /// share-price NAV; it ignores withdrawal caps and fees that would affect + /// realizable liquidity. For redemption-headroom checks use + /// `vault.maxWithdraw(diamond)` instead. + /// @return The current value of the strategy's position denominated in the + /// underlying asset. + function morphoTotalAssets() external view returns (uint256) { MorphoStorage storage s = _ms(); if (address(s.vault) == address(0)) revert MorphoVaultNotConfigured(); return s.vault.convertToAssets(s.vault.balanceOf(address(this))); - } - function morphoDeposit (uint256 amount) external { + /// @notice Deposit `amount` of the diamond's underlying asset into Metamorpho. + /// @dev Strategy-internal primitive. The diamond is always the share + /// receiver — there is no caller-chosen receiver. Reverts with + /// `MorphoSlippage` if the vault mints fewer shares than + /// `previewDeposit(amount)` predicted. + /// @param amount Quantity of underlying asset to allocate to Morpho. + function morphoDeposit(uint256 amount) external { MorphoStorage storage s = _ms(); if (address(s.vault) == address(0)) revert MorphoVaultNotConfigured(); IERC20 underlying = IERC20(IERC4626(address(this)).asset()); @@ -72,19 +106,24 @@ contract MorphoStrategyFacet { if (shares < expected) revert MorphoSlippage(expected, shares); } - function morphoWithdraw (uint256 assets) external{ + /// @notice Withdraw `assets` of underlying from Metamorpho back to the diamond. + /// @dev Strategy-internal primitive. Underlying always returns to the diamond + /// itself — forwarding to a user-chosen receiver is the responsibility + /// of the user-facing redeem path that also burns shares. + /// @param assets Quantity of underlying asset to pull out of Morpho. + function morphoWithdraw(uint256 assets) external { MorphoStorage storage s = _ms(); if (address(s.vault) == address(0)) revert MorphoVaultNotConfigured(); - s.vault.withdraw(assets,address(this), address(this)); + s.vault.withdraw(assets, address(this), address(this)); } - //view + //view + /// @notice Return the currently configured Metamorpho vault. + /// @return The Metamorpho ERC4626 vault this strategy routes capital to. function morphoVault() external view returns (IMorpho) { MorphoStorage storage s = _ms(); if (address(s.vault) == address(0)) revert MorphoVaultNotConfigured(); return s.vault; } - - -} \ No newline at end of file +} diff --git a/src/interfaces/IDiamondCut.sol b/src/interfaces/IDiamondCut.sol index 7d60474..53cc4f8 100644 --- a/src/interfaces/IDiamondCut.sol +++ b/src/interfaces/IDiamondCut.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {IDiamond} from "./IDiamond.sol"; +import { IDiamond } from "./IDiamond.sol"; interface IDiamondCut is IDiamond { /// @notice Add/replace/remove any number of functions and optionally execute diff --git a/src/interfaces/external/IMorpho.sol b/src/interfaces/external/IMorpho.sol index 39658eb..924cac1 100644 --- a/src/interfaces/external/IMorpho.sol +++ b/src/interfaces/external/IMorpho.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; -interface IMorpho is IERC4626 {} \ No newline at end of file +interface IMorpho is IERC4626 { } diff --git a/src/libraries/LibDiamond.sol b/src/libraries/LibDiamond.sol index 70fc580..d41ab9a 100644 --- a/src/libraries/LibDiamond.sol +++ b/src/libraries/LibDiamond.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {IDiamond} from "../interfaces/IDiamond.sol"; -import {IDiamondCut} from "../interfaces/IDiamondCut.sol"; +import { IDiamond } from "../interfaces/IDiamond.sol"; +import { IDiamondCut } from "../interfaces/IDiamondCut.sol"; library LibDiamond { bytes32 internal constant DIAMOND_STORAGE_POSITION = keccak256("diamond.standard.diamond.storage"); @@ -139,7 +139,12 @@ library LibDiamond { ds.facetAddresses.push(_facetAddress); } - function addFunction(DiamondStorage storage ds, bytes4 _selector, uint96 _selectorPosition, address _facetAddress) + function addFunction( + DiamondStorage storage ds, + bytes4 _selector, + uint96 _selectorPosition, + address _facetAddress + ) internal { ds.selectorToFacetAndPosition[_selector].functionSelectorPosition = _selectorPosition; diff --git a/src/libraries/LibFees.sol b/src/libraries/LibFees.sol index d03bf93..99790e7 100644 --- a/src/libraries/LibFees.sol +++ b/src/libraries/LibFees.sol @@ -10,13 +10,12 @@ pragma solidity ^0.8.24; /// keccak256(abi.encode(uint256(keccak256("vaultrouter.storage.fees")) - 1)) & ~bytes32(uint256(0xff)) library LibFees { uint16 internal constant BPS_DENOMINATOR = 10_000; - uint16 internal constant MAX_PERFORMANCE_FEE_BPS = 5_000; // 50% sanity ceiling - uint16 internal constant MAX_MANAGEMENT_FEE_BPS = 1_000; // 10% / year sanity ceiling + uint16 internal constant MAX_PERFORMANCE_FEE_BPS = 5000; // 50% sanity ceiling + uint16 internal constant MAX_MANAGEMENT_FEE_BPS = 1000; // 10% / year sanity ceiling uint256 internal constant SECONDS_PER_YEAR = 365 days; /// @dev erc7201:vaultrouter.storage.fees - bytes32 internal constant FEE_STORAGE_SLOT = - 0xd8263cd2923de1a73423e53eeb7d7ffc12f7b4ef6a8eadaee1bbca5e38dbe600; + bytes32 internal constant FEE_STORAGE_SLOT = 0xd8263cd2923de1a73423e53eeb7d7ffc12f7b4ef6a8eadaee1bbca5e38dbe600; struct FeeStorage { address feeRecipient; diff --git a/test/integration/AaveStrategy.fork.t.sol b/test/integration/AaveStrategy.fork.t.sol index 94ba98b..05db42e 100644 --- a/test/integration/AaveStrategy.fork.t.sol +++ b/test/integration/AaveStrategy.fork.t.sol @@ -1,21 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {Test} from "forge-std/Test.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {Vault} from "../../src/Vault.sol"; -import {IDiamond} from "../../src/interfaces/IDiamond.sol"; -import {IDiamondCut} from "../../src/interfaces/IDiamondCut.sol"; -import {IDiamondLoupe} from "../../src/interfaces/IDiamondLoupe.sol"; -import {IERC173} from "../../src/interfaces/IERC173.sol"; -import {DiamondCutFacet} from "../../src/facets/DiamondCutFacet.sol"; -import {DiamondLoupeFacet} from "../../src/facets/DiamondLoupeFacet.sol"; -import {OwnershipFacet} from "../../src/facets/OwnershipFacet.sol"; -import {AllocatorFacet} from "../../src/facets/AllocatorFacet.sol"; -import {AaveStrategyFacet} from "../../src/facets/strategies/AaveStrategyFacet.sol"; -import {IAavePool} from "../../src/interfaces/external/IAavePool.sol"; -import {LibAllocator} from "../../src/libraries/LibAllocator.sol"; +import { Test } from "forge-std/Test.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { Vault } from "../../src/Vault.sol"; +import { IDiamond } from "../../src/interfaces/IDiamond.sol"; +import { IDiamondCut } from "../../src/interfaces/IDiamondCut.sol"; +import { IDiamondLoupe } from "../../src/interfaces/IDiamondLoupe.sol"; +import { IERC173 } from "../../src/interfaces/IERC173.sol"; +import { DiamondCutFacet } from "../../src/facets/DiamondCutFacet.sol"; +import { DiamondLoupeFacet } from "../../src/facets/DiamondLoupeFacet.sol"; +import { OwnershipFacet } from "../../src/facets/OwnershipFacet.sol"; +import { AllocatorFacet } from "../../src/facets/AllocatorFacet.sol"; +import { AaveStrategyFacet } from "../../src/facets/strategies/AaveStrategyFacet.sol"; +import { IAavePool } from "../../src/interfaces/external/IAavePool.sol"; +import { LibAllocator } from "../../src/libraries/LibAllocator.sol"; /// @title AaveStrategyForkTest /// @notice Exercises the AaveStrategyFacet end-to-end against the real Aave V3 @@ -51,7 +51,7 @@ contract AaveStrategyForkTest is Test { vm.startPrank(owner); AaveStrategyFacet(address(vault)).aaveSetConfig(IAavePool(BASE_AAVE_POOL), IERC20(BASE_AUSDC)); AllocatorFacet(address(vault)).registerStrategy(AAVE_ID, _aaveStrategyConfig()); - _setSingleAllocation(AAVE_ID, 8_000); // 80% to Aave + _setSingleAllocation(AAVE_ID, 8000); // 80% to Aave vm.stopPrank(); } @@ -60,9 +60,9 @@ contract AaveStrategyForkTest is Test { // ----------------------------------------------------------------------- function test_DepositRebalanceDeploysToAUsdc() public { - _seedAndDeposit(alice, 1_000 * 1e6); + _seedAndDeposit(alice, 1000 * 1e6); - assertEq(IERC20(BASE_USDC).balanceOf(address(vault)), 1_000 * 1e6, "USDC sits idle pre-rebalance"); + assertEq(IERC20(BASE_USDC).balanceOf(address(vault)), 1000 * 1e6, "USDC sits idle pre-rebalance"); assertEq(IERC20(BASE_AUSDC).balanceOf(address(vault)), 0, "no aUSDC yet"); vm.roll(block.number + 1); @@ -77,11 +77,11 @@ contract AaveStrategyForkTest is Test { 1, // 1 wei tolerance for aave's internal rounding "80% deployed to aave as aUSDC" ); - assertApproxEqAbs(vault.totalAssets(), 1_000 * 1e6, 1, "totalAssets unchanged"); + assertApproxEqAbs(vault.totalAssets(), 1000 * 1e6, 1, "totalAssets unchanged"); } function test_InterestAccruesIntoATokenBalance() public { - _seedAndDeposit(alice, 1_000 * 1e6); + _seedAndDeposit(alice, 1000 * 1e6); vm.roll(block.number + 1); vm.prank(owner); AllocatorFacet(address(vault)).rebalance(); @@ -96,11 +96,11 @@ contract AaveStrategyForkTest is Test { assertGt(aAfter, aBefore, "aUSDC balance grew from supply interest"); // totalAssets reflects the gain. - assertGt(vault.totalAssets(), 1_000 * 1e6, "vault TVL grew"); + assertGt(vault.totalAssets(), 1000 * 1e6, "vault TVL grew"); } function test_RedeemWithdrawsFromAaveAndReturnsAssets() public { - _seedAndDeposit(alice, 1_000 * 1e6); + _seedAndDeposit(alice, 1000 * 1e6); vm.roll(block.number + 1); vm.prank(owner); AllocatorFacet(address(vault)).rebalance(); @@ -113,14 +113,9 @@ contract AaveStrategyForkTest is Test { vm.prank(alice); uint256 assetsReturned = vault.redeem(aliceShares, alice, alice); - assertGe(assetsReturned, 1_000 * 1e6, "alice gets back at least her principal"); + assertGe(assetsReturned, 1000 * 1e6, "alice gets back at least her principal"); assertEq(IERC20(BASE_USDC).balanceOf(alice), assetsReturned, "alice's wallet credited"); - assertApproxEqAbs( - IERC20(BASE_AUSDC).balanceOf(address(vault)), - 0, - 1, - "aUSDC drained back to idle on redeem" - ); + assertApproxEqAbs(IERC20(BASE_AUSDC).balanceOf(address(vault)), 0, 1, "aUSDC drained back to idle on redeem"); } // ----------------------------------------------------------------------- @@ -152,9 +147,7 @@ contract AaveStrategyForkTest is Test { IDiamond.FacetCut[] memory cuts = new IDiamond.FacetCut[](5); cuts[0] = IDiamond.FacetCut({ - facetAddress: address(cut), - action: IDiamond.FacetCutAction.Add, - functionSelectors: _diamondCutSelectors() + facetAddress: address(cut), action: IDiamond.FacetCutAction.Add, functionSelectors: _diamondCutSelectors() }); cuts[1] = IDiamond.FacetCut({ facetAddress: address(loupe), @@ -172,9 +165,7 @@ contract AaveStrategyForkTest is Test { functionSelectors: _allocatorSelectors() }); cuts[4] = IDiamond.FacetCut({ - facetAddress: address(aave), - action: IDiamond.FacetCutAction.Add, - functionSelectors: _aaveSelectors() + facetAddress: address(aave), action: IDiamond.FacetCutAction.Add, functionSelectors: _aaveSelectors() }); return new Vault(IERC20(BASE_USDC), "Vault Router", "vUSDC", owner, cuts, address(0), ""); diff --git a/test/mocks/MockProtocol.sol b/test/mocks/MockProtocol.sol index de78f90..666cd5a 100644 --- a/test/mocks/MockProtocol.sol +++ b/test/mocks/MockProtocol.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; interface IMintable { function mint(address to, uint256 amount) external; diff --git a/test/mocks/MockStrategyFacet.sol b/test/mocks/MockStrategyFacet.sol index 20cb1b5..389d683 100644 --- a/test/mocks/MockStrategyFacet.sol +++ b/test/mocks/MockStrategyFacet.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; -import {MockProtocol} from "./MockProtocol.sol"; +import { MockProtocol } from "./MockProtocol.sol"; /// @title MockStrategyFacet /// @notice Test-only strategy facet that bridges the Diamond to a `MockProtocol`. @@ -12,8 +12,7 @@ import {MockProtocol} from "./MockProtocol.sol"; /// doesn't collide with the vault's ERC-4626 storage or with LibAllocator. /// @dev keccak256(abi.encode(uint256(keccak256("vaultrouter.test.mockstrategy")) - 1)) & ~bytes32(uint256(0xff)) contract MockStrategyFacet { - bytes32 internal constant MOCK_STORAGE_SLOT = - 0x2a93387479f60fbd0b1454d20ad1a7e5268ff2625b39049c9905b150353cb300; + bytes32 internal constant MOCK_STORAGE_SLOT = 0x2a93387479f60fbd0b1454d20ad1a7e5268ff2625b39049c9905b150353cb300; struct MockStorage { MockProtocol protocol; diff --git a/test/unit/Allocator.t.sol b/test/unit/Allocator.t.sol index fe0c3bc..c5f9fbc 100644 --- a/test/unit/Allocator.t.sol +++ b/test/unit/Allocator.t.sol @@ -1,27 +1,27 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {Test} from "forge-std/Test.sol"; -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {Vault} from "../../src/Vault.sol"; -import {IDiamond} from "../../src/interfaces/IDiamond.sol"; -import {IDiamondCut} from "../../src/interfaces/IDiamondCut.sol"; -import {IDiamondLoupe} from "../../src/interfaces/IDiamondLoupe.sol"; -import {IERC173} from "../../src/interfaces/IERC173.sol"; -import {DiamondCutFacet} from "../../src/facets/DiamondCutFacet.sol"; -import {DiamondLoupeFacet} from "../../src/facets/DiamondLoupeFacet.sol"; -import {OwnershipFacet} from "../../src/facets/OwnershipFacet.sol"; -import {IdleStrategyFacet} from "../../src/facets/strategies/IdleStrategyFacet.sol"; -import {AllocatorFacet} from "../../src/facets/AllocatorFacet.sol"; -import {LibAllocator} from "../../src/libraries/LibAllocator.sol"; - -import {MockProtocol} from "../mocks/MockProtocol.sol"; -import {MockStrategyFacet} from "../mocks/MockStrategyFacet.sol"; +import { Test } from "forge-std/Test.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { Vault } from "../../src/Vault.sol"; +import { IDiamond } from "../../src/interfaces/IDiamond.sol"; +import { IDiamondCut } from "../../src/interfaces/IDiamondCut.sol"; +import { IDiamondLoupe } from "../../src/interfaces/IDiamondLoupe.sol"; +import { IERC173 } from "../../src/interfaces/IERC173.sol"; +import { DiamondCutFacet } from "../../src/facets/DiamondCutFacet.sol"; +import { DiamondLoupeFacet } from "../../src/facets/DiamondLoupeFacet.sol"; +import { OwnershipFacet } from "../../src/facets/OwnershipFacet.sol"; +import { IdleStrategyFacet } from "../../src/facets/strategies/IdleStrategyFacet.sol"; +import { AllocatorFacet } from "../../src/facets/AllocatorFacet.sol"; +import { LibAllocator } from "../../src/libraries/LibAllocator.sol"; + +import { MockProtocol } from "../mocks/MockProtocol.sol"; +import { MockStrategyFacet } from "../mocks/MockStrategyFacet.sol"; contract MockUSDC is ERC20 { - constructor() ERC20("USD Coin", "USDC") {} + constructor() ERC20("USD Coin", "USDC") { } function mint(address to, uint256 amount) external { _mint(to, amount); @@ -84,30 +84,28 @@ contract AllocatorTest is Test { function test_SetAllocation_RejectsBudgetExceeded() public { vm.prank(owner); - AllocatorFacet(address(vault)).setIdleReserve(2_000); // 20% + AllocatorFacet(address(vault)).setIdleReserve(2000); // 20% bytes32[] memory ids = new bytes32[](1); uint16[] memory bps = new uint16[](1); ids[0] = MOCK_ID; - bps[0] = 9_000; // 90%, but only 80% is allocatable + bps[0] = 9000; // 90%, but only 80% is allocatable vm.prank(owner); - vm.expectRevert(abi.encodeWithSelector(AllocatorFacet.AllocationExceedsBudget.selector, 9_000, 8_000)); + vm.expectRevert(abi.encodeWithSelector(AllocatorFacet.AllocationExceedsBudget.selector, 9000, 8000)); AllocatorFacet(address(vault)).setAllocation(ids, bps); } function test_SetAllocation_RejectsCapExceeded() public { vm.startPrank(owner); - AllocatorFacet(address(vault)).setStrategyCap(MOCK_ID, 5_000); // 50% cap on mock + AllocatorFacet(address(vault)).setStrategyCap(MOCK_ID, 5000); // 50% cap on mock bytes32[] memory ids = new bytes32[](1); uint16[] memory bps = new uint16[](1); ids[0] = MOCK_ID; - bps[0] = 6_000; + bps[0] = 6000; - vm.expectRevert( - abi.encodeWithSelector(AllocatorFacet.AllocationExceedsCap.selector, MOCK_ID, 5_000, 6_000) - ); + vm.expectRevert(abi.encodeWithSelector(AllocatorFacet.AllocationExceedsCap.selector, MOCK_ID, 5000, 6000)); AllocatorFacet(address(vault)).setAllocation(ids, bps); vm.stopPrank(); } @@ -116,12 +114,12 @@ contract AllocatorTest is Test { bytes32[] memory ids = new bytes32[](1); uint16[] memory bps = new uint16[](1); ids[0] = MOCK_ID; - bps[0] = 8_000; + bps[0] = 8000; vm.prank(owner); AllocatorFacet(address(vault)).setAllocation(ids, bps); - assertEq(AllocatorFacet(address(vault)).targetAllocation(MOCK_ID), 8_000); + assertEq(AllocatorFacet(address(vault)).targetAllocation(MOCK_ID), 8000); } // ----------------------------------------------------------------------- @@ -129,8 +127,8 @@ contract AllocatorTest is Test { // ----------------------------------------------------------------------- function test_Rebalance_DistributesAssetsToStrategy() public { - _depositToVault(alice, 1_000 * 1e6); - _setSingleAllocation(MOCK_ID, 8_000); + _depositToVault(alice, 1000 * 1e6); + _setSingleAllocation(MOCK_ID, 8000); vm.roll(block.number + 1); vm.prank(owner); @@ -141,8 +139,8 @@ contract AllocatorTest is Test { } function test_Rebalance_PullsBackWhenAllocationDrops() public { - _depositToVault(alice, 1_000 * 1e6); - _setSingleAllocation(MOCK_ID, 8_000); + _depositToVault(alice, 1000 * 1e6); + _setSingleAllocation(MOCK_ID, 8000); vm.roll(block.number + 1); vm.prank(owner); AllocatorFacet(address(vault)).rebalance(); @@ -160,31 +158,29 @@ contract AllocatorTest is Test { AllocatorFacet(address(vault)).rebalance(); assertEq(mockProtocol.balanceOf(address(vault)), 0, "mock protocol drained"); - assertEq(usdc.balanceOf(address(vault)), 1_000 * 1e6, "all assets back idle"); + assertEq(usdc.balanceOf(address(vault)), 1000 * 1e6, "all assets back idle"); } function test_Rebalance_RevertsSameBlock() public { - _depositToVault(alice, 1_000 * 1e6); - _setSingleAllocation(MOCK_ID, 5_000); + _depositToVault(alice, 1000 * 1e6); + _setSingleAllocation(MOCK_ID, 5000); vm.roll(block.number + 1); vm.startPrank(owner); AllocatorFacet(address(vault)).rebalance(); - vm.expectRevert( - abi.encodeWithSelector(AllocatorFacet.RebalanceTooSoon.selector, block.number, block.number) - ); + vm.expectRevert(abi.encodeWithSelector(AllocatorFacet.RebalanceTooSoon.selector, block.number, block.number)); AllocatorFacet(address(vault)).rebalance(); vm.stopPrank(); } function test_VaultTotalAssets_SumsIdleAndStrategyPositions() public { - _depositToVault(alice, 1_000 * 1e6); + _depositToVault(alice, 1000 * 1e6); // Before rebalance, all 1_000 sits idle in the vault. - assertEq(vault.totalAssets(), 1_000 * 1e6, "all idle before rebalance"); + assertEq(vault.totalAssets(), 1000 * 1e6, "all idle before rebalance"); - _setSingleAllocation(MOCK_ID, 7_000); + _setSingleAllocation(MOCK_ID, 7000); vm.roll(block.number + 1); vm.prank(owner); AllocatorFacet(address(vault)).rebalance(); @@ -192,12 +188,12 @@ contract AllocatorTest is Test { // After: 300 idle + 700 in mock protocol = 1_000 reported. assertEq(usdc.balanceOf(address(vault)), 300 * 1e6, "idle = 30%"); assertEq(mockProtocol.balanceOf(address(vault)), 700 * 1e6, "deployed = 70%"); - assertEq(vault.totalAssets(), 1_000 * 1e6, "totalAssets unchanged across rebalance"); + assertEq(vault.totalAssets(), 1000 * 1e6, "totalAssets unchanged across rebalance"); } function test_StrategyTotalAssets_ReadsViaFallback() public { - _depositToVault(alice, 1_000 * 1e6); - _setSingleAllocation(MOCK_ID, 5_000); + _depositToVault(alice, 1000 * 1e6); + _setSingleAllocation(MOCK_ID, 5000); vm.roll(block.number + 1); vm.prank(owner); AllocatorFacet(address(vault)).rebalance(); @@ -207,15 +203,15 @@ contract AllocatorTest is Test { } function test_IdleReserve_FloorEnforcedAfterRebalance() public { - _depositToVault(alice, 1_000 * 1e6); + _depositToVault(alice, 1000 * 1e6); vm.prank(owner); - AllocatorFacet(address(vault)).setIdleReserve(2_500); // 25% floor + AllocatorFacet(address(vault)).setIdleReserve(2500); // 25% floor // Allocation that exactly hits the budget: 75% to strategy, 25% idle. bytes32[] memory ids = new bytes32[](1); uint16[] memory bps = new uint16[](1); ids[0] = MOCK_ID; - bps[0] = 7_500; + bps[0] = 7500; vm.prank(owner); AllocatorFacet(address(vault)).setAllocation(ids, bps); @@ -241,9 +237,7 @@ contract AllocatorTest is Test { IDiamond.FacetCut[] memory cuts = new IDiamond.FacetCut[](6); cuts[0] = IDiamond.FacetCut({ - facetAddress: address(cut), - action: IDiamond.FacetCutAction.Add, - functionSelectors: _diamondCutSelectors() + facetAddress: address(cut), action: IDiamond.FacetCutAction.Add, functionSelectors: _diamondCutSelectors() }); cuts[1] = IDiamond.FacetCut({ facetAddress: address(loupe), @@ -256,9 +250,7 @@ contract AllocatorTest is Test { functionSelectors: _ownershipSelectors() }); cuts[3] = IDiamond.FacetCut({ - facetAddress: address(idle), - action: IDiamond.FacetCutAction.Add, - functionSelectors: _idleSelectors() + facetAddress: address(idle), action: IDiamond.FacetCutAction.Add, functionSelectors: _idleSelectors() }); cuts[4] = IDiamond.FacetCut({ facetAddress: address(allocator), @@ -266,9 +258,7 @@ contract AllocatorTest is Test { functionSelectors: _allocatorSelectors() }); cuts[5] = IDiamond.FacetCut({ - facetAddress: address(mock), - action: IDiamond.FacetCutAction.Add, - functionSelectors: _mockSelectors() + facetAddress: address(mock), action: IDiamond.FacetCutAction.Add, functionSelectors: _mockSelectors() }); return new Vault(IERC20(address(usdc)), "Vault Router", "vUSDC", owner, cuts, address(0), ""); diff --git a/test/unit/Fees.t.sol b/test/unit/Fees.t.sol index aaf5d09..baa5041 100644 --- a/test/unit/Fees.t.sol +++ b/test/unit/Fees.t.sol @@ -1,28 +1,28 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {Test} from "forge-std/Test.sol"; -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {Vault} from "../../src/Vault.sol"; -import {IDiamond} from "../../src/interfaces/IDiamond.sol"; -import {IDiamondCut} from "../../src/interfaces/IDiamondCut.sol"; -import {IDiamondLoupe} from "../../src/interfaces/IDiamondLoupe.sol"; -import {IERC173} from "../../src/interfaces/IERC173.sol"; -import {DiamondCutFacet} from "../../src/facets/DiamondCutFacet.sol"; -import {DiamondLoupeFacet} from "../../src/facets/DiamondLoupeFacet.sol"; -import {OwnershipFacet} from "../../src/facets/OwnershipFacet.sol"; -import {AllocatorFacet} from "../../src/facets/AllocatorFacet.sol"; -import {FeeFacet} from "../../src/facets/FeeFacet.sol"; -import {LibAllocator} from "../../src/libraries/LibAllocator.sol"; -import {LibFees} from "../../src/libraries/LibFees.sol"; - -import {MockProtocol} from "../mocks/MockProtocol.sol"; -import {MockStrategyFacet} from "../mocks/MockStrategyFacet.sol"; +import { Test } from "forge-std/Test.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { Vault } from "../../src/Vault.sol"; +import { IDiamond } from "../../src/interfaces/IDiamond.sol"; +import { IDiamondCut } from "../../src/interfaces/IDiamondCut.sol"; +import { IDiamondLoupe } from "../../src/interfaces/IDiamondLoupe.sol"; +import { IERC173 } from "../../src/interfaces/IERC173.sol"; +import { DiamondCutFacet } from "../../src/facets/DiamondCutFacet.sol"; +import { DiamondLoupeFacet } from "../../src/facets/DiamondLoupeFacet.sol"; +import { OwnershipFacet } from "../../src/facets/OwnershipFacet.sol"; +import { AllocatorFacet } from "../../src/facets/AllocatorFacet.sol"; +import { FeeFacet } from "../../src/facets/FeeFacet.sol"; +import { LibAllocator } from "../../src/libraries/LibAllocator.sol"; +import { LibFees } from "../../src/libraries/LibFees.sol"; + +import { MockProtocol } from "../mocks/MockProtocol.sol"; +import { MockStrategyFacet } from "../mocks/MockStrategyFacet.sol"; contract MockUSDC is ERC20 { - constructor() ERC20("USD Coin", "USDC") {} + constructor() ERC20("USD Coin", "USDC") { } function mint(address to, uint256 amount) external { _mint(to, amount); @@ -62,9 +62,9 @@ contract FeesTest is Test { function test_NoFee_WhenRecipientUnset() public { // Even with non-zero rates set, no recipient → no fee shares. // (Setters reject 0-address recipient, so we just leave it default.) - _depositToVault(alice, 1_000 * 1e6); + _depositToVault(alice, 1000 * 1e6); mockProtocol._testAccrueYield(address(vault), 100 * 1e6); // 10% yield - _depositToVault(alice, 1_000 * 1e6); // triggers _accrueFees + _depositToVault(alice, 1000 * 1e6); // triggers _accrueFees assertEq(vault.balanceOf(feeRx), 0, "no fee minted without recipient"); } @@ -74,8 +74,8 @@ contract FeesTest is Test { // ----------------------------------------------------------------------- function test_PerformanceFee_BootstrapsHwmOnFirstAccrual() public { - _configureFees(2_000, 0); // 20% perf, 0% mgmt - _depositToVault(alice, 1_000 * 1e6); + _configureFees(2000, 0); // 20% perf, 0% mgmt + _depositToVault(alice, 1000 * 1e6); // First deposit just initialised HWM to the current share price. // No fee should have been charged. @@ -84,9 +84,9 @@ contract FeesTest is Test { } function test_PerformanceFee_ChargedOnShareGrowth() public { - _configureFees(2_000, 0); // 20% perf - _depositToVault(alice, 1_000 * 1e6); - _setSingleAllocation(MOCK_ID, 8_000); + _configureFees(2000, 0); // 20% perf + _depositToVault(alice, 1000 * 1e6); + _setSingleAllocation(MOCK_ID, 8000); vm.roll(block.number + 1); vm.prank(owner); AllocatorFacet(address(vault)).rebalance(); @@ -105,9 +105,9 @@ contract FeesTest is Test { } function test_PerformanceFee_NotChargedBelowHWM() public { - _configureFees(2_000, 0); - _depositToVault(alice, 1_000 * 1e6); - _setSingleAllocation(MOCK_ID, 8_000); + _configureFees(2000, 0); + _depositToVault(alice, 1000 * 1e6); + _setSingleAllocation(MOCK_ID, 8000); vm.roll(block.number + 1); vm.prank(owner); AllocatorFacet(address(vault)).rebalance(); @@ -136,7 +136,7 @@ contract FeesTest is Test { function test_ManagementFee_AccruedOverTime() public { _configureFees(0, 200); // 0% perf, 2%/year mgmt - _depositToVault(alice, 1_000 * 1e6); + _depositToVault(alice, 1000 * 1e6); uint256 supplyBefore = vault.totalSupply(); @@ -191,9 +191,7 @@ contract FeesTest is Test { IDiamond.FacetCut[] memory cuts = new IDiamond.FacetCut[](6); cuts[0] = IDiamond.FacetCut({ - facetAddress: address(cut), - action: IDiamond.FacetCutAction.Add, - functionSelectors: _diamondCutSelectors() + facetAddress: address(cut), action: IDiamond.FacetCutAction.Add, functionSelectors: _diamondCutSelectors() }); cuts[1] = IDiamond.FacetCut({ facetAddress: address(loupe), @@ -211,14 +209,10 @@ contract FeesTest is Test { functionSelectors: _allocatorSelectors() }); cuts[4] = IDiamond.FacetCut({ - facetAddress: address(feeFacet), - action: IDiamond.FacetCutAction.Add, - functionSelectors: _feeSelectors() + facetAddress: address(feeFacet), action: IDiamond.FacetCutAction.Add, functionSelectors: _feeSelectors() }); cuts[5] = IDiamond.FacetCut({ - facetAddress: address(mock), - action: IDiamond.FacetCutAction.Add, - functionSelectors: _mockSelectors() + facetAddress: address(mock), action: IDiamond.FacetCutAction.Add, functionSelectors: _mockSelectors() }); return new Vault(IERC20(address(usdc)), "Vault Router", "vUSDC", owner, cuts, address(0), ""); diff --git a/test/unit/Harvest.t.sol b/test/unit/Harvest.t.sol index 028d6ce..f4cbfd4 100644 --- a/test/unit/Harvest.t.sol +++ b/test/unit/Harvest.t.sol @@ -1,27 +1,27 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {Test} from "forge-std/Test.sol"; -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {Vault} from "../../src/Vault.sol"; -import {IDiamond} from "../../src/interfaces/IDiamond.sol"; -import {IDiamondCut} from "../../src/interfaces/IDiamondCut.sol"; -import {IDiamondLoupe} from "../../src/interfaces/IDiamondLoupe.sol"; -import {IERC173} from "../../src/interfaces/IERC173.sol"; -import {DiamondCutFacet} from "../../src/facets/DiamondCutFacet.sol"; -import {DiamondLoupeFacet} from "../../src/facets/DiamondLoupeFacet.sol"; -import {OwnershipFacet} from "../../src/facets/OwnershipFacet.sol"; -import {AllocatorFacet} from "../../src/facets/AllocatorFacet.sol"; -import {HarvestFacet} from "../../src/facets/HarvestFacet.sol"; -import {LibAllocator} from "../../src/libraries/LibAllocator.sol"; - -import {MockProtocol} from "../mocks/MockProtocol.sol"; -import {MockStrategyFacet} from "../mocks/MockStrategyFacet.sol"; +import { Test } from "forge-std/Test.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { Vault } from "../../src/Vault.sol"; +import { IDiamond } from "../../src/interfaces/IDiamond.sol"; +import { IDiamondCut } from "../../src/interfaces/IDiamondCut.sol"; +import { IDiamondLoupe } from "../../src/interfaces/IDiamondLoupe.sol"; +import { IERC173 } from "../../src/interfaces/IERC173.sol"; +import { DiamondCutFacet } from "../../src/facets/DiamondCutFacet.sol"; +import { DiamondLoupeFacet } from "../../src/facets/DiamondLoupeFacet.sol"; +import { OwnershipFacet } from "../../src/facets/OwnershipFacet.sol"; +import { AllocatorFacet } from "../../src/facets/AllocatorFacet.sol"; +import { HarvestFacet } from "../../src/facets/HarvestFacet.sol"; +import { LibAllocator } from "../../src/libraries/LibAllocator.sol"; + +import { MockProtocol } from "../mocks/MockProtocol.sol"; +import { MockStrategyFacet } from "../mocks/MockStrategyFacet.sol"; contract MockUSDC is ERC20 { - constructor() ERC20("USD Coin", "USDC") {} + constructor() ERC20("USD Coin", "USDC") { } function mint(address to, uint256 amount) external { _mint(to, amount); @@ -98,9 +98,7 @@ contract HarvestTest is Test { IDiamond.FacetCut[] memory cuts = new IDiamond.FacetCut[](6); cuts[0] = IDiamond.FacetCut({ - facetAddress: address(cut), - action: IDiamond.FacetCutAction.Add, - functionSelectors: _diamondCutSelectors() + facetAddress: address(cut), action: IDiamond.FacetCutAction.Add, functionSelectors: _diamondCutSelectors() }); cuts[1] = IDiamond.FacetCut({ facetAddress: address(loupe), @@ -123,9 +121,7 @@ contract HarvestTest is Test { functionSelectors: _harvestSelectors() }); cuts[5] = IDiamond.FacetCut({ - facetAddress: address(mock), - action: IDiamond.FacetCutAction.Add, - functionSelectors: _mockSelectors() + facetAddress: address(mock), action: IDiamond.FacetCutAction.Add, functionSelectors: _mockSelectors() }); return new Vault(IERC20(address(usdc)), "Vault Router", "vUSDC", owner, cuts, address(0), ""); diff --git a/test/unit/Vault.t.sol b/test/unit/Vault.t.sol index 91da369..2f31be9 100644 --- a/test/unit/Vault.t.sol +++ b/test/unit/Vault.t.sol @@ -1,22 +1,22 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {Test} from "forge-std/Test.sol"; -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {Vault} from "../../src/Vault.sol"; -import {IDiamond} from "../../src/interfaces/IDiamond.sol"; -import {IDiamondCut} from "../../src/interfaces/IDiamondCut.sol"; -import {IDiamondLoupe} from "../../src/interfaces/IDiamondLoupe.sol"; -import {IERC173} from "../../src/interfaces/IERC173.sol"; -import {DiamondCutFacet} from "../../src/facets/DiamondCutFacet.sol"; -import {DiamondLoupeFacet} from "../../src/facets/DiamondLoupeFacet.sol"; -import {OwnershipFacet} from "../../src/facets/OwnershipFacet.sol"; -import {IdleStrategyFacet} from "../../src/facets/strategies/IdleStrategyFacet.sol"; +import { Test } from "forge-std/Test.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { Vault } from "../../src/Vault.sol"; +import { IDiamond } from "../../src/interfaces/IDiamond.sol"; +import { IDiamondCut } from "../../src/interfaces/IDiamondCut.sol"; +import { IDiamondLoupe } from "../../src/interfaces/IDiamondLoupe.sol"; +import { IERC173 } from "../../src/interfaces/IERC173.sol"; +import { DiamondCutFacet } from "../../src/facets/DiamondCutFacet.sol"; +import { DiamondLoupeFacet } from "../../src/facets/DiamondLoupeFacet.sol"; +import { OwnershipFacet } from "../../src/facets/OwnershipFacet.sol"; +import { IdleStrategyFacet } from "../../src/facets/strategies/IdleStrategyFacet.sol"; contract MockUSDC is ERC20 { - constructor() ERC20("USD Coin", "USDC") {} + constructor() ERC20("USD Coin", "USDC") { } function mint(address to, uint256 amount) external { _mint(to, amount); @@ -39,7 +39,7 @@ contract VaultTest is Test { } function test_DepositMintsSharesAndIncreasesTotalAssets() public { - uint256 amount = 1_000 * 1e6; + uint256 amount = 1000 * 1e6; usdc.mint(alice, amount); vm.startPrank(alice); @@ -54,7 +54,7 @@ contract VaultTest is Test { } function test_RedeemReturnsAssetsToOwner() public { - uint256 amount = 1_000 * 1e6; + uint256 amount = 1000 * 1e6; usdc.mint(alice, amount); vm.startPrank(alice); @@ -117,9 +117,7 @@ contract VaultTest is Test { IDiamond.FacetCut[] memory cuts = new IDiamond.FacetCut[](4); cuts[0] = IDiamond.FacetCut({ - facetAddress: address(cut), - action: IDiamond.FacetCutAction.Add, - functionSelectors: _diamondCutSelectors() + facetAddress: address(cut), action: IDiamond.FacetCutAction.Add, functionSelectors: _diamondCutSelectors() }); cuts[1] = IDiamond.FacetCut({ facetAddress: address(loupe), @@ -132,9 +130,7 @@ contract VaultTest is Test { functionSelectors: _ownershipSelectors() }); cuts[3] = IDiamond.FacetCut({ - facetAddress: address(idle), - action: IDiamond.FacetCutAction.Add, - functionSelectors: _idleSelectors() + facetAddress: address(idle), action: IDiamond.FacetCutAction.Add, functionSelectors: _idleSelectors() }); return new Vault(asset_, "Vault Router", "vUSDC", owner, cuts, address(0), "");