From fabef7a5f0015121356e2c177eb8ae5aa85fac5a Mon Sep 17 00:00:00 2001 From: Jayesh Yadav Date: Fri, 29 May 2026 18:07:52 +0530 Subject: [PATCH] refactor(diamond): split proxy mechanics into Diamond base --- src/Diamond.sol | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ src/Vault.sol | 39 +++++++++---------------------------- 2 files changed, 61 insertions(+), 30 deletions(-) create mode 100644 src/Diamond.sol diff --git a/src/Diamond.sol b/src/Diamond.sol new file mode 100644 index 0000000..5cd31ea --- /dev/null +++ b/src/Diamond.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import { IDiamond } from "./interfaces/IDiamond.sol"; +import { LibDiamond } from "./libraries/LibDiamond.sol"; + +/// @title Diamond — EIP-2535 proxy mechanics. +/// @notice The pure plumbing of the diamond: it wires the initial facet set and +/// ownership at construction, then routes every unknown selector to its +/// facet via a delegatecall fallback. It carries no application logic. +/// @dev Vault inherits this for its diamond surface and layers the ERC-4626 vault +/// surface on top. ERC-4626's own functions are native (compiled into Vault), +/// so Solidity dispatches them directly; only selectors with no native match +/// reach this fallback and are delegated to facets. Keeping the proxy +/// mechanics here leaves Vault.sol focused on the vault economics. +abstract contract Diamond { + error UnknownSelector(bytes4 selector); + + /// @param initialOwner Diamond owner (facet upgrades, risk bounds, fees). + /// @param diamondCut_ Initial facet cuts applied at deployment. + /// @param init_ Optional initializer delegatecall target (0 to skip). + /// @param initCalldata_ Calldata for the initializer. + constructor( + address initialOwner, + IDiamond.FacetCut[] memory diamondCut_, + address init_, + bytes memory initCalldata_ + ) { + LibDiamond.setContractOwner(initialOwner); + LibDiamond.diamondCut(diamondCut_, init_, initCalldata_); + } + + fallback() external payable { + LibDiamond.DiamondStorage storage ds; + bytes32 position = LibDiamond.DIAMOND_STORAGE_POSITION; + assembly { + ds.slot := position + } + address facet = ds.selectorToFacetAndPosition[msg.sig].facetAddress; + if (facet == address(0)) revert UnknownSelector(msg.sig); + assembly { + calldatacopy(0, 0, calldatasize()) + let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0) + returndatacopy(0, 0, returndatasize()) + switch result + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } + } + } + + receive() external payable { } +} diff --git a/src/Vault.sol b/src/Vault.sol index 05d23f8..5e5cd59 100644 --- a/src/Vault.sol +++ b/src/Vault.sol @@ -6,19 +6,20 @@ import { ERC4626 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626. import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IDiamond } from "./interfaces/IDiamond.sol"; -import { LibDiamond } from "./libraries/LibDiamond.sol"; +import { Diamond } from "./Diamond.sol"; import { LibAllocator } from "./libraries/LibAllocator.sol"; import { LibFees } from "./libraries/LibFees.sol"; import { LibGuard } from "./libraries/LibGuard.sol"; /// @title Vault Router is a modular ERC-4626 vault on the EIP-2535 Diamond pattern. -/// @notice Vault.sol owns the ERC-4626 surface (deposit/withdraw/totalAssets) and -/// acts as the Diamond proxy. Strategy logic, allocation policy, and +/// @notice Vault owns the ERC-4626 surface (deposit/withdraw/totalAssets) plus the +/// fee and circuit-breaker hooks. The diamond proxy mechanics live in the +/// inherited Diamond base; strategy logic, allocation policy, and /// harvesting live in facets attached via diamondCut. /// @dev Inflation attack mitigation comes from OZ ERC-4626's `_decimalsOffset` -/// virtual shares. -contract Vault is ERC4626 { - error UnknownSelector(bytes4 selector); +/// virtual shares. The ERC-4626 surface is native (non-facet) and therefore +/// non-upgradeable, so it cannot be altered by a later diamondCut. +contract Vault is Diamond, ERC4626 { error StrategyTotalAssetsCallFailed(bytes32 strategyId); constructor( @@ -30,12 +31,10 @@ contract Vault is ERC4626 { address init_, bytes memory initCalldata_ ) + Diamond(initialOwner, diamondCut_, init_, initCalldata_) ERC20(name_, symbol_) ERC4626(asset_) - { - LibDiamond.setContractOwner(initialOwner); - LibDiamond.diamondCut(diamondCut_, init_, initCalldata_); - } + { } /// @dev 6 decimals of virtual shares, OZ's recommended inflation-attack /// mitigation for ERC-4626 vaults. Sourced from LibGuard so the breaker's @@ -156,24 +155,4 @@ contract Vault is ERC4626 { f.lastFeeAccrual = nowTs; if (feeShares > 0) _mint(f.feeRecipient, feeShares); } - - fallback() external payable { - LibDiamond.DiamondStorage storage ds; - bytes32 position = LibDiamond.DIAMOND_STORAGE_POSITION; - assembly { - ds.slot := position - } - address facet = ds.selectorToFacetAndPosition[msg.sig].facetAddress; - if (facet == address(0)) revert UnknownSelector(msg.sig); - assembly { - calldatacopy(0, 0, calldatasize()) - let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0) - returndatacopy(0, 0, returndatasize()) - switch result - case 0 { revert(0, returndatasize()) } - default { return(0, returndatasize()) } - } - } - - receive() external payable { } }