Skip to content
Open
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
15 changes: 15 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
8 changes: 8 additions & 0 deletions src/periphery/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
18 changes: 18 additions & 0 deletions src/periphery/registries/MorphoMarketV1RegistryV2.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
23 changes: 23 additions & 0 deletions src/periphery/registries/MorphoVaultV1Registry.sol
Original file line number Diff line number Diff line change
@@ -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());
}
}
48 changes: 48 additions & 0 deletions src/periphery/registries/RegistryList.sol
Comment thread
MathisGD marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
10 changes: 10 additions & 0 deletions src/periphery/registries/interfaces/IMorphoVaultV1Registry.sol
Original file line number Diff line number Diff line change
@@ -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);
}
13 changes: 13 additions & 0 deletions src/periphery/registries/interfaces/IRegistryList.sol
Original file line number Diff line number Diff line change
@@ -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;
}
31 changes: 31 additions & 0 deletions test/periphery/MorphoMarketV1RegistryV2Test.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
59 changes: 59 additions & 0 deletions test/periphery/MorphoVaultV1RegistryTest.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
96 changes: 96 additions & 0 deletions test/periphery/RegistryListTest.sol
Original file line number Diff line number Diff line change
@@ -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));
}
}
Loading