diff --git a/foundry.toml b/foundry.toml index 13aecee3f..26aaaf67d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -38,6 +38,16 @@ paths = "src/imports/MetaMorphoImport.sol" optimizer_runs = 200 evm_version = "cancun" +# The registries are deployed on-chain with verified bytecode compiled in the +# standalone morpho-org/vault-v2-adapter-registries repo (optimizer_runs = 999999). +# vault-v2's default is 100000, so they need the standalone repo's setting to keep +# identical bytecode. All other bytecode-relevant settings already match +# (solc 0.8.28, via_ir, optimizer, cancun, bytecode_hash = "none"). +[[profile.default.compilation_restrictions]] +paths = "src/periphery/registries/**" +optimizer_runs = 999999 +evm_version = "cancun" + # For every restriction above there must be some compatible foundry profile. # The first matching profile will be used. For clarity, all profiles below should be incompatible with each other. @@ -51,4 +61,9 @@ name = "200-cancun" optimizer_runs = 200 evm_version = "cancun" +[[profile.default.additional_compiler_profiles]] +name = "999999-cancun" +optimizer_runs = 999999 +evm_version = "cancun" + # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/src/periphery/README.md b/src/periphery/README.md index 2a9d23d61..7d55d8d3e 100644 --- a/src/periphery/README.md +++ b/src/periphery/README.md @@ -3,6 +3,7 @@ Peripheral, non-core contracts that build on top of Vault V2. Blue public allocator contracts are in [blue-public-allocator](./blue-public-allocator/). Gate contracts are in [gates](./gates/). +Adapter registry contracts are in [registries](./registries/). ## Blue Public Allocator @@ -22,3 +23,10 @@ If an account transfers shares to another account, for example into another prot `WhitelistSendAssetsGate` is an implementation of the vault's `sendAssetsGate`. It restricts who can send assets into the vault on deposits and mints. Whitelisted accounts can still deposit assets that originally belong to non-whitelisted accounts, so this gate does not fully restrict whose funds can enter the vault. + +## Registries + +Adapter registries implement the vault's `IAdapterRegistry` interface (`isInRegistry(address)`) so a vault can validate the adapters it uses. +`RegistryList` is an owner-controlled, append-only list of sub-registries; an adapter is in the registry if it is in any sub-registry. +`MorphoMarketV1RegistryV2` validates adapters created by the `MorphoMarketV1AdapterV2Factory`. +`MorphoVaultV1Registry` validates adapters created by the `MorphoVaultV1AdapterFactory` whose underlying MetaMorpho vault was created by the MetaMorpho factory. diff --git a/src/periphery/registries/MorphoMarketV1RegistryV2.sol b/src/periphery/registries/MorphoMarketV1RegistryV2.sol new file mode 100644 index 000000000..4b792bbec --- /dev/null +++ b/src/periphery/registries/MorphoMarketV1RegistryV2.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2026 Morpho Association +pragma solidity 0.8.28; + +import {IMorphoMarketV1AdapterV2Factory} from "../../adapters/interfaces/IMorphoMarketV1AdapterV2Factory.sol"; +import {IMorphoMarketV1RegistryV2} from "./interfaces/IMorphoMarketV1RegistryV2.sol"; + +contract MorphoMarketV1RegistryV2 is IMorphoMarketV1RegistryV2 { + address public immutable morphoMarketV1AdapterV2Factory; + + constructor(address _morphoMarketV1AdapterV2Factory) { + morphoMarketV1AdapterV2Factory = _morphoMarketV1AdapterV2Factory; + } + + function isInRegistry(address adapter) external view returns (bool) { + return IMorphoMarketV1AdapterV2Factory(morphoMarketV1AdapterV2Factory).isMorphoMarketV1AdapterV2(adapter); + } +} diff --git a/src/periphery/registries/MorphoVaultV1Registry.sol b/src/periphery/registries/MorphoVaultV1Registry.sol new file mode 100644 index 000000000..7c4a249ef --- /dev/null +++ b/src/periphery/registries/MorphoVaultV1Registry.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2026 Morpho Association +pragma solidity 0.8.28; + +import {IMetaMorphoFactory} from "../../../lib/metamorpho/src/interfaces/IMetaMorphoFactory.sol"; +import {IMorphoVaultV1AdapterFactory} from "../../adapters/interfaces/IMorphoVaultV1AdapterFactory.sol"; +import {IMorphoVaultV1Adapter} from "../../adapters/interfaces/IMorphoVaultV1Adapter.sol"; +import {IMorphoVaultV1Registry} from "./interfaces/IMorphoVaultV1Registry.sol"; + +contract MorphoVaultV1Registry is IMorphoVaultV1Registry { + address public immutable morphoVaultV1AdapterFactory; + address public immutable morphoVaultV1Factory; + + constructor(address _morphoVaultV1AdapterFactory, address _morphoVaultV1Factory) { + morphoVaultV1AdapterFactory = _morphoVaultV1AdapterFactory; + morphoVaultV1Factory = _morphoVaultV1Factory; + } + + function isInRegistry(address adapter) external view returns (bool) { + return IMorphoVaultV1AdapterFactory(morphoVaultV1AdapterFactory).isMorphoVaultV1Adapter(adapter) + && IMetaMorphoFactory(morphoVaultV1Factory).isMetaMorpho(IMorphoVaultV1Adapter(adapter).morphoVaultV1()); + } +} diff --git a/src/periphery/registries/RegistryList.sol b/src/periphery/registries/RegistryList.sol new file mode 100644 index 000000000..a4a044001 --- /dev/null +++ b/src/periphery/registries/RegistryList.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2026 Morpho Association +pragma solidity 0.8.28; + +import {IRegistryList} from "./interfaces/IRegistryList.sol"; +import {IAdapterRegistry} from "../../interfaces/IAdapterRegistry.sol"; + +contract RegistryList is IRegistryList { + address public owner; + + /// @dev Owner controlled append-only list of registries. + /// @dev This registry is effectively "add-only" only if all sub-registries are also "add-only". + address[] public subRegistries; + + event Constructor(address indexed owner); + event SetOwner(address indexed newOwner); + event AddSubRegistry(address indexed subRegistry); + + function subRegistriesLength() external view returns (uint256) { + return subRegistries.length; + } + + constructor() { + owner = msg.sender; + emit Constructor(msg.sender); + } + + function setOwner(address newOwner) external { + require(msg.sender == owner, "Not owner"); + owner = newOwner; + emit SetOwner(newOwner); + } + + /// @dev Adding a subRegistry that reverts or makes looping too gas consuming will make new registries uneffective + /// (vaults will not be able to validate adapters that would be validated by registries that have been added after). + function addSubRegistry(address subRegistry) external { + require(msg.sender == owner, "Not owner"); + subRegistries.push(subRegistry); + emit AddSubRegistry(subRegistry); + } + + function isInRegistry(address adapter) public view returns (bool) { + for (uint256 i = 0; i < subRegistries.length; i++) { + if (IAdapterRegistry(subRegistries[i]).isInRegistry(adapter)) return true; + } + return false; + } +} diff --git a/src/periphery/registries/interfaces/IMorphoMarketV1RegistryV2.sol b/src/periphery/registries/interfaces/IMorphoMarketV1RegistryV2.sol new file mode 100644 index 000000000..5aaa36952 --- /dev/null +++ b/src/periphery/registries/interfaces/IMorphoMarketV1RegistryV2.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2026 Morpho Association +pragma solidity >=0.5.0; + +import {IAdapterRegistry} from "../../../interfaces/IAdapterRegistry.sol"; + +interface IMorphoMarketV1RegistryV2 is IAdapterRegistry { + function morphoMarketV1AdapterV2Factory() external view returns (address); +} diff --git a/src/periphery/registries/interfaces/IMorphoVaultV1Registry.sol b/src/periphery/registries/interfaces/IMorphoVaultV1Registry.sol new file mode 100644 index 000000000..b72270732 --- /dev/null +++ b/src/periphery/registries/interfaces/IMorphoVaultV1Registry.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2026 Morpho Association +pragma solidity >=0.5.0; + +import {IAdapterRegistry} from "../../../interfaces/IAdapterRegistry.sol"; + +interface IMorphoVaultV1Registry is IAdapterRegistry { + function morphoVaultV1AdapterFactory() external view returns (address); + function morphoVaultV1Factory() external view returns (address); +} diff --git a/src/periphery/registries/interfaces/IRegistryList.sol b/src/periphery/registries/interfaces/IRegistryList.sol new file mode 100644 index 000000000..db6104af1 --- /dev/null +++ b/src/periphery/registries/interfaces/IRegistryList.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2026 Morpho Association +pragma solidity >=0.5.0; + +import {IAdapterRegistry} from "../../../interfaces/IAdapterRegistry.sol"; + +interface IRegistryList is IAdapterRegistry { + function owner() external view returns (address); + function subRegistries(uint256 index) external view returns (address); + function subRegistriesLength() external view returns (uint256); + function setOwner(address newOwner) external; + function addSubRegistry(address subRegistry) external; +} diff --git a/test/periphery/MorphoMarketV1RegistryV2Test.sol b/test/periphery/MorphoMarketV1RegistryV2Test.sol new file mode 100644 index 000000000..39962f558 --- /dev/null +++ b/test/periphery/MorphoMarketV1RegistryV2Test.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2026 Morpho Association +pragma solidity ^0.8.28; + +import {Test} from "forge-std/Test.sol"; +import {MorphoMarketV1RegistryV2} from "../../src/periphery/registries/MorphoMarketV1RegistryV2.sol"; +import {IMorphoMarketV1RegistryV2} from "../../src/periphery/registries/interfaces/IMorphoMarketV1RegistryV2.sol"; + +contract MorphoMarketV1RegistryV2Test is Test { + IMorphoMarketV1RegistryV2 registry; + address morphoMarketV1AdapterV2Factory = address(0x1001); + + function setUp() public { + registry = IMorphoMarketV1RegistryV2(address(new MorphoMarketV1RegistryV2(morphoMarketV1AdapterV2Factory))); + } + + function testConstructor() public view { + assertEq(registry.morphoMarketV1AdapterV2Factory(), morphoMarketV1AdapterV2Factory); + } + + function testIsInRegistry(address adapter, bool isMorphoMarketV1AdapterV2) public { + vm.assume(adapter != address(vm)); + vm.mockCall( + morphoMarketV1AdapterV2Factory, + abi.encodeWithSignature("isMorphoMarketV1AdapterV2(address)", adapter), + abi.encode(isMorphoMarketV1AdapterV2) + ); + + assertEq(registry.isInRegistry(adapter), isMorphoMarketV1AdapterV2); + } +} diff --git a/test/periphery/MorphoVaultV1RegistryTest.sol b/test/periphery/MorphoVaultV1RegistryTest.sol new file mode 100644 index 000000000..cd4342cf1 --- /dev/null +++ b/test/periphery/MorphoVaultV1RegistryTest.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2026 Morpho Association +pragma solidity ^0.8.28; + +import {Test} from "forge-std/Test.sol"; +import {MorphoVaultV1Registry} from "../../src/periphery/registries/MorphoVaultV1Registry.sol"; +import {IMorphoVaultV1Registry} from "../../src/periphery/registries/interfaces/IMorphoVaultV1Registry.sol"; + +contract MorphoVaultV1RegistryTest is Test { + IMorphoVaultV1Registry registry; + address morphoVaultV1Factory = address(0x1001); + address morphoVaultV1AdapterFactory = address(0x1002); + + function setUp() public { + registry = IMorphoVaultV1Registry( + address(new MorphoVaultV1Registry(morphoVaultV1AdapterFactory, morphoVaultV1Factory)) + ); + } + + function testConstructor() public view { + assertEq(registry.morphoVaultV1AdapterFactory(), morphoVaultV1AdapterFactory); + assertEq(registry.morphoVaultV1Factory(), morphoVaultV1Factory); + } + + function testIsInRegistry(address adapter, address morphoVaultV1, bool isMorphoVaultV1Adapter, bool isMetaMorpho) + public + { + vm.assume(adapter != address(vm)); + vm.mockCall( + morphoVaultV1AdapterFactory, + abi.encodeWithSignature("isMorphoVaultV1Adapter(address)", adapter), + abi.encode(isMorphoVaultV1Adapter) + ); + + if (isMorphoVaultV1Adapter) { + vm.mockCall(adapter, abi.encodeWithSignature("morphoVaultV1()"), abi.encode(morphoVaultV1)); + vm.mockCall( + morphoVaultV1Factory, + abi.encodeWithSignature("isMetaMorpho(address)", morphoVaultV1), + abi.encode(isMetaMorpho) + ); + } + + bool expected = isMorphoVaultV1Adapter && isMetaMorpho; + assertEq(registry.isInRegistry(adapter), expected); + } + + // check that if the adapter isn't a vault adapter, it doesn't revert (basically checks the order of execution of + // solidity). + function testNoObscureRevert(address adapter) public { + vm.mockCall( + morphoVaultV1AdapterFactory, + abi.encodeWithSignature("isMorphoVaultV1Adapter(address)", adapter), + abi.encode(false) + ); + + registry.isInRegistry(adapter); + } +} diff --git a/test/periphery/RegistryListTest.sol b/test/periphery/RegistryListTest.sol new file mode 100644 index 000000000..1a08fdd63 --- /dev/null +++ b/test/periphery/RegistryListTest.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2026 Morpho Association +pragma solidity ^0.8.28; + +import {Test} from "forge-std/Test.sol"; +import {RegistryList} from "../../src/periphery/registries/RegistryList.sol"; +import {IRegistryList} from "../../src/periphery/registries/interfaces/IRegistryList.sol"; + +contract RegistryListTest is Test { + IRegistryList registry; + address user = address(0x1); + + function setUp() public { + registry = IRegistryList(address(new RegistryList())); + } + + function testConstructor() public view { + assertEq(registry.owner(), address(this)); + assertEq(registry.subRegistriesLength(), 0); + } + + function testConstructorEvent() public { + vm.expectEmit(true, false, false, false); + emit RegistryList.Constructor(address(this)); + new RegistryList(); + } + + function testSetOwner(address newOwner) public { + vm.expectEmit(true, false, false, false); + emit RegistryList.SetOwner(newOwner); + + registry.setOwner(newOwner); + assertEq(registry.owner(), newOwner); + } + + function testSetOwnerOnlyOwner() public { + vm.prank(user); + vm.expectRevert("Not owner"); + registry.setOwner(address(0x123)); + } + + function testAddSubRegistry(address subRegistry) public { + vm.expectEmit(true, false, false, false); + emit RegistryList.AddSubRegistry(subRegistry); + + registry.addSubRegistry(subRegistry); + + assertEq(registry.subRegistriesLength(), 1); + assertEq(registry.subRegistries(0), subRegistry); + } + + function testAddRegistrySubRegistryOnlyOwner() public { + vm.prank(user); + vm.expectRevert("Not owner"); + registry.addSubRegistry(address(0x1002)); + } + + function testIsInRegistryWithSubRegistries(address adapter, bool subRegistry1Result, bool subRegistry2Result) + public + { + address subRegistry1 = address(0x1001); + address subRegistry2 = address(0x1002); + + registry.addSubRegistry(subRegistry1); + registry.addSubRegistry(subRegistry2); + + vm.mockCall( + subRegistry1, abi.encodeWithSignature("isInRegistry(address)", adapter), abi.encode(subRegistry1Result) + ); + vm.mockCall( + subRegistry2, abi.encodeWithSignature("isInRegistry(address)", adapter), abi.encode(subRegistry2Result) + ); + + bool expected = subRegistry1Result || subRegistry2Result; + assertEq(registry.isInRegistry(adapter), expected); + } + + function testIsInRegistryNoSubRegistries(address adapter) public view { + assertFalse(registry.isInRegistry(adapter)); + } + + function testAddingRevertingSubRegistry(address adapter, address legitSubRegistry, address revertingSubRegistry) + public + { + assumeAddressIsNot(legitSubRegistry, AddressType.ForgeAddress); + vm.assume(legitSubRegistry != revertingSubRegistry); + vm.assume(legitSubRegistry != address(0)); + + registry.addSubRegistry(legitSubRegistry); + registry.addSubRegistry(revertingSubRegistry); + + vm.mockCall(legitSubRegistry, abi.encodeWithSignature("isInRegistry(address)", adapter), abi.encode(true)); + + assertTrue(registry.isInRegistry(adapter)); + } +}