diff --git a/src/facets/strategies/AaveStrategyFacet.sol b/src/facets/strategies/AaveStrategyFacet.sol index 9682c08..c81ae35 100644 --- a/src/facets/strategies/AaveStrategyFacet.sol +++ b/src/facets/strategies/AaveStrategyFacet.sol @@ -24,9 +24,11 @@ contract AaveStrategyFacet { event AaveConfigSet(IAavePool indexed pool, IERC20 indexed aToken); - /// @dev erc7201:vaultrouter.strategy.aave + /// @dev Precomputed erc7201("vaultrouter.strategy.aave"): + /// keccak256(abi.encode(uint256(keccak256(id)) - 1)) & ~bytes32(uint256(0xff)) bytes32 internal constant AAVE_STORAGE_SLOT = 0x340080245a7d3e67835fb5055646777827d09fc7212fda4d8d724367e1215700; + /// @custom:storage-location erc7201:vaultrouter.strategy.aave struct AaveStorage { IAavePool pool; IERC20 aToken; diff --git a/src/facets/strategies/MorphoStrategyFacet.sol b/src/facets/strategies/MorphoStrategyFacet.sol index cadf641..52ac948 100644 --- a/src/facets/strategies/MorphoStrategyFacet.sol +++ b/src/facets/strategies/MorphoStrategyFacet.sol @@ -37,12 +37,13 @@ contract MorphoStrategyFacet { /// @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 + /// @dev Precomputed erc7201("vaultrouter.strategy.morpho"): + /// keccak256(abi.encode(uint256(keccak256(id)) - 1)) & ~bytes32(uint256(0xff)) bytes32 internal constant MORPHO_STORAGE_SLOT = 0xf4b3fd2d8603f5a74e31f8c3250c4c70408eaa33a47a7d4535036bfa6799e900; /// @notice Storage layout for the Morpho strategy facet. /// @dev `vault` is the configured Metamorpho ERC4626 vault. + /// @custom:storage-location erc7201:vaultrouter.strategy.morpho struct MorphoStorage { IMorpho vault; } diff --git a/src/facets/strategies/PendlePtStrategyFacet.sol b/src/facets/strategies/PendlePtStrategyFacet.sol index b89a870..1965017 100644 --- a/src/facets/strategies/PendlePtStrategyFacet.sol +++ b/src/facets/strategies/PendlePtStrategyFacet.sol @@ -88,7 +88,8 @@ contract PendlePtStrategyFacet { // Storage // ----------------------------------------------------------------------- - /// @dev erc7201:vaultrouter.strategy.pendle + /// @dev Precomputed erc7201("vaultrouter.strategy.pendle"): + /// keccak256(abi.encode(uint256(keccak256(id)) - 1)) & ~bytes32(uint256(0xff)) bytes32 internal constant PENDLE_STORAGE_SLOT = 0xb0e016db49ce2cfbe35770c2200cbf5f1a9b502bca57dbaaddf328cb9e0cef00; /// @dev Basis-points denominator. @@ -97,6 +98,7 @@ contract PendlePtStrategyFacet { /// @dev Slippage tolerance (bps) used when none is explicitly configured: 1%. uint16 internal constant DEFAULT_MAX_SLIPPAGE_BPS = 100; + /// @custom:storage-location erc7201:vaultrouter.strategy.pendle struct PendleStorage { /// @notice PendleRouterV4 — handles all swap and redemption paths. IPendleRouter router; diff --git a/src/libraries/LibAllocator.sol b/src/libraries/LibAllocator.sol index 5ff67db..1fa9594 100644 --- a/src/libraries/LibAllocator.sol +++ b/src/libraries/LibAllocator.sol @@ -7,6 +7,8 @@ pragma solidity ^0.8.24; /// LibDiamond's selector table. /// @dev keccak256(abi.encode(uint256(keccak256("vaultrouter.storage.allocator")) - 1)) & ~bytes32(uint256(0xff)) library LibAllocator { + /// @dev Precomputed erc7201("vaultrouter.storage.allocator"): + /// keccak256(abi.encode(uint256(keccak256(id)) - 1)) & ~bytes32(uint256(0xff)) bytes32 internal constant ALLOCATOR_STORAGE_SLOT = 0x2f4e489fd9fdb4c68f60ae0ec4a19ea4d9796e41932a74c08f691957213bd500; diff --git a/src/libraries/LibFees.sol b/src/libraries/LibFees.sol index 99790e7..cae3c42 100644 --- a/src/libraries/LibFees.sol +++ b/src/libraries/LibFees.sol @@ -14,9 +14,11 @@ library LibFees { 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 + /// @dev Precomputed erc7201("vaultrouter.storage.fees"): + /// keccak256(abi.encode(uint256(keccak256(id)) - 1)) & ~bytes32(uint256(0xff)) bytes32 internal constant FEE_STORAGE_SLOT = 0xd8263cd2923de1a73423e53eeb7d7ffc12f7b4ef6a8eadaee1bbca5e38dbe600; + /// @custom:storage-location erc7201:vaultrouter.storage.fees struct FeeStorage { address feeRecipient; uint16 performanceFeeBps; diff --git a/src/libraries/LibGuard.sol b/src/libraries/LibGuard.sol index b3be87b..7687ef3 100644 --- a/src/libraries/LibGuard.sol +++ b/src/libraries/LibGuard.sol @@ -22,6 +22,8 @@ library LibGuard { /// share-price math below never drift apart. uint8 internal constant DECIMALS_OFFSET = 6; + /// @dev Precomputed erc7201("vaultrouter.storage.guard"): + /// keccak256(abi.encode(uint256(keccak256(id)) - 1)) & ~bytes32(uint256(0xff)) bytes32 internal constant GUARD_STORAGE_SLOT = 0x2e670cc2b429ff4c75b2b5ce7b57521bb8c3d00aaafa77116d454e88d382a900; /// @notice Reverted on any deposit/withdraw while the breaker is latched. diff --git a/src/libraries/LibLock.sol b/src/libraries/LibLock.sol index 390482d..459a5da 100644 --- a/src/libraries/LibLock.sol +++ b/src/libraries/LibLock.sol @@ -23,7 +23,8 @@ library LibLock { /// @notice Reverted when shares are moved before their lock window elapses. error SharesLocked(address account, uint256 lockedUntil); - /// @dev erc7201:vaultrouter.storage.lock + /// @dev Precomputed erc7201("vaultrouter.storage.lock"): + /// keccak256(abi.encode(uint256(keccak256(id)) - 1)) & ~bytes32(uint256(0xff)) bytes32 internal constant LOCK_STORAGE_SLOT = 0x98796fb009fa2d66e5ccc76be36c65ba72c5853e7fe1b4f23987457f018b5800; /// @custom:storage-location erc7201:vaultrouter.storage.lock diff --git a/src/libraries/LibRoles.sol b/src/libraries/LibRoles.sol index 2168823..1545012 100644 --- a/src/libraries/LibRoles.sol +++ b/src/libraries/LibRoles.sol @@ -15,6 +15,8 @@ import { LibDiamond } from "./LibDiamond.sol"; /// table, or any other facet's namespace. /// keccak256(abi.encode(uint256(keccak256("vaultrouter.storage.roles")) - 1)) & ~bytes32(uint256(0xff)) library LibRoles { + /// @dev Precomputed erc7201("vaultrouter.storage.roles"): + /// keccak256(abi.encode(uint256(keccak256(id)) - 1)) & ~bytes32(uint256(0xff)) bytes32 internal constant ROLES_STORAGE_SLOT = 0x72812988d549c1f62ecdf8218c688f5047bef5695066f17d8d1060ecc0962300; error NotCurator(address caller); diff --git a/src/libraries/LibWithdrawQueue.sol b/src/libraries/LibWithdrawQueue.sol index 2a5efda..5e613d9 100644 --- a/src/libraries/LibWithdrawQueue.sol +++ b/src/libraries/LibWithdrawQueue.sol @@ -21,7 +21,8 @@ pragma solidity ^0.8.24; /// Storage location: /// keccak256(abi.encode(uint256(keccak256("vaultrouter.storage.withdrawqueue")) - 1)) & ~bytes32(uint256(0xff)) library LibWithdrawQueue { - /// @dev erc7201:vaultrouter.storage.withdrawqueue + /// @dev Precomputed erc7201("vaultrouter.storage.withdrawqueue"): + /// keccak256(abi.encode(uint256(keccak256(id)) - 1)) & ~bytes32(uint256(0xff)) bytes32 internal constant WITHDRAW_QUEUE_STORAGE_SLOT = 0x3d5a7857d3d4e9dbe18f39f41bd8fd54a510e284a7d9a4464e9cc2159e9f9100; diff --git a/test/unit/StorageNamespaces.t.sol b/test/unit/StorageNamespaces.t.sol new file mode 100644 index 0000000..d9bb4a7 --- /dev/null +++ b/test/unit/StorageNamespaces.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import { Test } from "forge-std/Test.sol"; + +import { LibAllocator } from "../../src/libraries/LibAllocator.sol"; +import { LibFees } from "../../src/libraries/LibFees.sol"; +import { LibGuard } from "../../src/libraries/LibGuard.sol"; +import { LibLock } from "../../src/libraries/LibLock.sol"; +import { LibRoles } from "../../src/libraries/LibRoles.sol"; +import { LibWithdrawQueue } from "../../src/libraries/LibWithdrawQueue.sol"; +import { AaveStrategyFacet } from "../../src/facets/strategies/AaveStrategyFacet.sol"; +import { MorphoStrategyFacet } from "../../src/facets/strategies/MorphoStrategyFacet.sol"; +import { PendlePtStrategyFacet } from "../../src/facets/strategies/PendlePtStrategyFacet.sol"; + +// The strategy facets keep their slot constant `internal` at contract scope, which is not +// reachable via qualified access, so a thin heir exposes it for the invariant check. +contract AaveExposer is AaveStrategyFacet { + function exposedSlot() external pure returns (bytes32) { + return AAVE_STORAGE_SLOT; + } +} + +contract MorphoExposer is MorphoStrategyFacet { + function exposedSlot() external pure returns (bytes32) { + return MORPHO_STORAGE_SLOT; + } +} + +contract PendleExposer is PendlePtStrategyFacet { + function exposedSlot() external pure returns (bytes32) { + return PENDLE_STORAGE_SLOT; + } +} + +/// @title Storage namespace invariant +/// @notice Every precomputed storage-slot literal in the protocol must equal the ERC-7201 +/// hash of its declared namespace string. This pins each named literal to its +/// namespace so the two can never silently drift: change a namespace string +/// without recomputing the slot, or paste a wrong literal, and this test fails. +/// @dev The detector proves slots do not collide with each other; this proves each slot +/// is the one its namespace actually resolves to. +contract StorageNamespacesTest is Test { + /// @dev erc7201(id) = keccak256(abi.encode(uint256(keccak256(id)) - 1)) & ~bytes32(uint256(0xff)) + function _erc7201(string memory id) internal pure returns (bytes32) { + return keccak256(abi.encode(uint256(keccak256(bytes(id))) - 1)) & ~bytes32(uint256(0xff)); + } + + function test_storageSlotsMatchTheirNamespaces() public { + assertEq(LibAllocator.ALLOCATOR_STORAGE_SLOT, _erc7201("vaultrouter.storage.allocator"), "allocator"); + assertEq(LibFees.FEE_STORAGE_SLOT, _erc7201("vaultrouter.storage.fees"), "fees"); + assertEq(LibGuard.GUARD_STORAGE_SLOT, _erc7201("vaultrouter.storage.guard"), "guard"); + assertEq(LibLock.LOCK_STORAGE_SLOT, _erc7201("vaultrouter.storage.lock"), "lock"); + assertEq(LibRoles.ROLES_STORAGE_SLOT, _erc7201("vaultrouter.storage.roles"), "roles"); + assertEq( + LibWithdrawQueue.WITHDRAW_QUEUE_STORAGE_SLOT, _erc7201("vaultrouter.storage.withdrawqueue"), "withdrawqueue" + ); + assertEq(new AaveExposer().exposedSlot(), _erc7201("vaultrouter.strategy.aave"), "aave"); + assertEq(new MorphoExposer().exposedSlot(), _erc7201("vaultrouter.strategy.morpho"), "morpho"); + assertEq(new PendleExposer().exposedSlot(), _erc7201("vaultrouter.strategy.pendle"), "pendle"); + } +}