Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions src/Vault.sol
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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_);
}
Expand All @@ -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));
}
Expand All @@ -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).
Expand All @@ -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();
Expand Down Expand Up @@ -157,5 +157,5 @@ contract Vault is ERC4626 {
}
}

receive() external payable {}
receive() external payable { }
}
22 changes: 10 additions & 12 deletions src/facets/AllocatorFacet.sol
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/facets/DiamondCutFacet.sol
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/facets/DiamondLoupeFacet.sol
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/facets/FeeFacet.sol
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
10 changes: 4 additions & 6 deletions src/facets/HarvestFacet.sol
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions src/facets/OwnershipFacet.sol
Original file line number Diff line number Diff line change
@@ -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
Expand Down
15 changes: 7 additions & 8 deletions src/facets/strategies/AaveStrategyFacet.sol
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/facets/strategies/IdleStrategyFacet.sol
Original file line number Diff line number Diff line change
@@ -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
Expand Down
129 changes: 129 additions & 0 deletions src/facets/strategies/MorphoStrategyFacet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// 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";

/// @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);

/// @notice EIP-7201 namespaced storage slot for the Morpho strategy state.
/// @dev erc7201:vaultrouter.strategy.morpho
bytes32 internal constant MORPHO_STORAGE_SLOT = 0xf4b3fd2d8603f5a74e31f8c3250c4c70408eaa33a47a7d4535036bfa6799e900;

/// @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 {
s.slot := slot
}
}

/// @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
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"

/// @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)));
}

/// @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());
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);
}

/// @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));
}

//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;
}
}
2 changes: 1 addition & 1 deletion src/interfaces/IDiamondCut.sol
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/interfaces/external/IMorpho.sol
Original file line number Diff line number Diff line change
@@ -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 { }
Loading