diff --git a/contracts/script/Deploy.s.sol b/contracts/script/Deploy.s.sol index 97826e2..f4e2b0e 100644 --- a/contracts/script/Deploy.s.sol +++ b/contracts/script/Deploy.s.sol @@ -13,9 +13,12 @@ import {MarginAccount} from "../src/MarginAccount.sol"; import {Pool} from "../src/Pool.sol"; import {RiskConfigurator} from "../src/RiskConfigurator.sol"; import {WhitelistRegistry} from "../src/WhitelistRegistry.sol"; +import {AdapterRegistry} from "../src/AdapterRegistry.sol"; import {ChainlinkPriceOracle} from "../src/ChainlinkPriceOracle.sol"; import {IChainlinkAggregator} from "../src/interfaces/IChainlinkAggregator.sol"; import {UniswapV3Adapter} from "../src/adapters/UniswapV3Adapter.sol"; +import {CurveAdapter} from "../src/adapters/CurveAdapter.sol"; +import {LstAdapter} from "../src/adapters/LstAdapter.sol"; import {Guardian} from "../src/governance/Guardian.sol"; import {RiskParams} from "../src/libraries/RiskParams.sol"; import {IGuardian} from "../src/interfaces/IGuardian.sol"; @@ -26,9 +29,13 @@ import {IPriceOracle} from "../src/interfaces/IPriceOracle.sol"; import {IRiskConfigurator} from "../src/interfaces/IRiskConfigurator.sol"; import {IUniswapV3SwapRouter} from "../src/interfaces/IUniswapV3SwapRouter.sol"; import {IWhitelistRegistry} from "../src/interfaces/IWhitelistRegistry.sol"; +import {IAdapterRegistry} from "../src/interfaces/IAdapterRegistry.sol"; +import {IWstETH} from "../src/interfaces/IWstETH.sol"; import {MockERC20} from "../test/mocks/MockERC20.sol"; import {MockPriceOracle} from "../test/mocks/MockPriceOracle.sol"; import {MockSwapRouter} from "../test/mocks/MockSwapRouter.sol"; +import {MockCurvePool} from "../test/mocks/MockCurvePool.sol"; +import {MockWstETH} from "../test/mocks/MockWstETH.sol"; /// @title DeployScript /// @notice Deploys the full Meridian system and applies every wiring step. The local profile @@ -87,7 +94,16 @@ contract DeployScript is Script { address accountImplementation; address guardian; address whitelistRegistry; + address adapterRegistry; address accessController; + // Shared periphery adapters (local: wrapped over mock venues), registered in the adapter + // registry and whitelisted so any market's account can route these ops through both gates. + address curveAdapter; + address curvePool; + address curveLp; + address lstAdapter; + address steth; + address wsteth; // Primary (WETH) market; the flat fields are also what the off-chain services read today. address weth; address creditManager; @@ -172,6 +188,7 @@ contract DeployScript is Script { // --- Safety and access (shared) --- d.guardian = address(new Guardian(deployer, deployer)); d.whitelistRegistry = address(new WhitelistRegistry(deployer)); + d.adapterRegistry = address(new AdapterRegistry(deployer)); d.accessController = address(new AccessController(deployer)); Pool(d.pool).setGuardian(IGuardian(d.guardian)); AccessController(d.accessController).grantRole(AccessController.Role.Keeper, config.keeper); @@ -204,6 +221,72 @@ contract DeployScript is Script { d.basketLiquidationModule = basket.liquidationModule; d.basketSwapRouter = basket.swapRouter; d.basketSwapAdapter = basket.swapAdapter; + + // --- Shared periphery adapters (Curve liquidity + LST wrap, registered + gated like swaps) --- + (d.curveAdapter, d.curvePool, d.curveLp) = _deployCurve(d); + (d.lstAdapter, d.steth, d.wsteth) = _deployLst(d); + } + + /// @notice Deploys the shared Curve adapter over a local mock two-coin (USDC/WETH) pool, whitelists + /// its liquidity selectors and the LP-token approve leg, and registers the adapter so any + /// market's margin account can route Curve liquidity through the same whitelist + adapter + /// gates as the swap adapter. The pool is a mock locally; a real Curve pool address is + /// supplied by config on public networks (a follow-up, like the other live venues). + /// @dev This wires the adapter as a sanctioned action surface. Valuing a Curve LP position as + /// account collateral (oracle price + risk haircut for the LP) is a separate strategy and is + /// not configured here. + function _deployCurve(Deployment memory d) internal returns (address adapter, address pool, address lp) { + MockERC20 lpToken = new MockERC20("Curve USDC/WETH LP", "crvUSDCWETH", 18); + MockCurvePool curvePool = new MockCurvePool(d.usdc, d.weth, lpToken); + // Seed both coins so single-coin withdrawals can pay out locally. + MockERC20(d.usdc).mint(address(curvePool), 1_000_000e6); + MockERC20(d.weth).mint(address(curvePool), 1_000_000e18); + CurveAdapter curveAdapter = new CurveAdapter(); + + WhitelistRegistry whitelist = WhitelistRegistry(d.whitelistRegistry); + whitelist.setTarget(address(curveAdapter), true); + whitelist.setSelector(address(curveAdapter), CurveAdapter.addLiquidity.selector, true); + whitelist.setSelector(address(curveAdapter), CurveAdapter.removeLiquidityOneCoin.selector, true); + // LP-token approve leg (account -> adapter) for the withdraw path; mirrors the global USDC approve. + whitelist.setTarget(address(lpToken), true); + whitelist.setSelector(address(lpToken), IERC20.approve.selector, true); + + AdapterRegistry(d.adapterRegistry).registerAdapter(address(curveAdapter), address(curvePool)); + + adapter = address(curveAdapter); + pool = address(curvePool); + lp = address(lpToken); + } + + /// @notice Deploys the shared LST adapter over a local mock staked token (stETH) and its wrapper + /// (wstETH), whitelists wrap/unwrap and both approve legs, and registers the adapter so any + /// margin account can route wrap/unwrap through the whitelist + adapter gates. Mock tokens + /// locally; real stETH/wstETH addresses are config-supplied on public networks (a follow-up). + /// @dev Like Curve, this wires the adapter as a sanctioned action surface; pricing wstETH as account + /// collateral (oracle + haircut) is a separate strategy and is not configured here. + function _deployLst(Deployment memory d) internal returns (address adapter, address staked, address wrapped) { + MockERC20 steth = new MockERC20("Staked Ether", "stETH", 18); + MockWstETH wsteth = new MockWstETH(IERC20(address(steth))); + // Seed the wrapper with stETH so unwraps can pay out locally. + steth.mint(address(wsteth), 1_000_000e18); + LstAdapter lstAdapter = new LstAdapter(IERC20(address(steth)), IWstETH(address(wsteth))); + + WhitelistRegistry whitelist = WhitelistRegistry(d.whitelistRegistry); + whitelist.setTarget(address(lstAdapter), true); + whitelist.setSelector(address(lstAdapter), LstAdapter.wrap.selector, true); + whitelist.setSelector(address(lstAdapter), LstAdapter.unwrap.selector, true); + // Approve legs (account -> adapter): stETH for wrap, wstETH for unwrap. Mirror the global USDC approve. + whitelist.setTarget(address(steth), true); + whitelist.setSelector(address(steth), IERC20.approve.selector, true); + whitelist.setTarget(address(wsteth), true); + whitelist.setSelector(address(wsteth), IERC20.approve.selector, true); + + // The wrapped token is the external protocol the adapter wraps. + AdapterRegistry(d.adapterRegistry).registerAdapter(address(lstAdapter), address(wsteth)); + + adapter = address(lstAdapter); + staked = address(steth); + wrapped = address(wsteth); } /// @notice Reads a Chainlink feed, registers it on the oracle for `token`, and returns the live @@ -258,6 +341,7 @@ contract DeployScript is Script { cm.setFacade(address(facade)); cm.setGuardian(IGuardian(d.guardian)); cm.setWhitelistRegistry(IWhitelistRegistry(d.whitelistRegistry)); + cm.setAdapterRegistry(IAdapterRegistry(d.adapterRegistry)); cm.setLiquidationModule(address(liquidation)); // Whitelist this market's lever leg (USDC approve is whitelisted once, globally). @@ -265,6 +349,10 @@ contract DeployScript is Script { whitelist.setTarget(address(adapter), true); whitelist.setSelector(address(adapter), UniswapV3Adapter.swapExactInputSingle.selector, true); + // Register the adapter so it passes the credit manager's adapter gate: the adapter wraps the + // (mock) swap router, which is the external protocol it routes into. + AdapterRegistry(d.adapterRegistry).registerAdapter(address(adapter), address(router)); + m = Market({ symbol: symbol, collateralToken: collateralToken, @@ -315,6 +403,13 @@ contract DeployScript is Script { vm.serializeAddress(obj, "creditFacade", d.creditFacade); vm.serializeAddress(obj, "guardian", d.guardian); vm.serializeAddress(obj, "whitelistRegistry", d.whitelistRegistry); + vm.serializeAddress(obj, "adapterRegistry", d.adapterRegistry); + vm.serializeAddress(obj, "curveAdapter", d.curveAdapter); + vm.serializeAddress(obj, "curvePool", d.curvePool); + vm.serializeAddress(obj, "curveLp", d.curveLp); + vm.serializeAddress(obj, "lstAdapter", d.lstAdapter); + vm.serializeAddress(obj, "steth", d.steth); + vm.serializeAddress(obj, "wsteth", d.wsteth); vm.serializeAddress(obj, "accessController", d.accessController); vm.serializeAddress(obj, "liquidationModule", d.liquidationModule); vm.serializeAddress(obj, "swapRouter", d.swapRouter); @@ -423,6 +518,11 @@ contract DeployScript is Script { console2.log(" MarginAccountImpl ", d.accountImplementation); console2.log(" Guardian ", d.guardian); console2.log(" WhitelistRegistry ", d.whitelistRegistry); + console2.log(" AdapterRegistry ", d.adapterRegistry); + console2.log(" CurveAdapter ", d.curveAdapter); + console2.log(" CurvePool (mock) ", d.curvePool); + console2.log(" LstAdapter ", d.lstAdapter); + console2.log(" wstETH (mock) ", d.wsteth); console2.log(" AccessController ", d.accessController); Market[] memory markets = _markets(d); for (uint256 i = 0; i < markets.length; i++) { diff --git a/contracts/src/AdapterRegistry.sol b/contracts/src/AdapterRegistry.sol index 9c43591..6984f88 100644 --- a/contracts/src/AdapterRegistry.sol +++ b/contracts/src/AdapterRegistry.sol @@ -2,13 +2,14 @@ pragma solidity ^0.8.24; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IAdapterRegistry} from "./interfaces/IAdapterRegistry.sol"; /// @title AdapterRegistry /// @notice Registry of approved adapters and the external protocol each one wraps. The credit /// system only routes margin-account calls through adapters listed here. -contract AdapterRegistry is Ownable { - mapping(address adapter => bool registered) public isAdapter; - mapping(address adapter => address target) public adapterTarget; +contract AdapterRegistry is Ownable, IAdapterRegistry { + mapping(address adapter => bool registered) public override isAdapter; + mapping(address adapter => address target) public override adapterTarget; event AdapterRegistered(address indexed adapter, address indexed target); event AdapterUnregistered(address indexed adapter); diff --git a/contracts/src/CreditManager.sol b/contracts/src/CreditManager.sol index b19f20a..d71632b 100644 --- a/contracts/src/CreditManager.sol +++ b/contracts/src/CreditManager.sol @@ -15,6 +15,7 @@ import {IPriceOracle} from "./interfaces/IPriceOracle.sol"; import {IRiskConfigurator} from "./interfaces/IRiskConfigurator.sol"; import {IGuardian} from "./interfaces/IGuardian.sol"; import {IWhitelistRegistry} from "./interfaces/IWhitelistRegistry.sol"; +import {IAdapterRegistry} from "./interfaces/IAdapterRegistry.sol"; import {ILiquidationTarget} from "./interfaces/ILiquidationTarget.sol"; import {MarginAccount} from "./MarginAccount.sol"; @@ -55,6 +56,7 @@ contract CreditManager is Ownable, ReentrancyGuard, ILiquidationTarget { IRiskConfigurator public riskConfigurator; IGuardian public guardian; IWhitelistRegistry public whitelistRegistry; + IAdapterRegistry public adapterRegistry; address public liquidationModule; address public facade; @@ -88,6 +90,7 @@ contract CreditManager is Ownable, ReentrancyGuard, ILiquidationTarget { event RiskConfiguratorSet(address indexed riskConfigurator); event GuardianSet(address indexed guardian); event WhitelistRegistrySet(address indexed whitelistRegistry); + event AdapterRegistrySet(address indexed adapterRegistry); event LiquidationModuleSet(address indexed liquidationModule); event FacadeSet(address indexed facade); event CollateralTokenAdded(address indexed token); @@ -101,6 +104,7 @@ contract CreditManager is Ownable, ReentrancyGuard, ILiquidationTarget { error AccountNotEmpty(); error Undercollateralized(); error CallNotWhitelisted(address target, bytes4 selector); + error TargetNotAdapter(address target); error NotCollateral(address token); error AlreadyCollateral(address token); error CannotRemovePrimary(); @@ -170,6 +174,14 @@ contract CreditManager is Ownable, ReentrancyGuard, ILiquidationTarget { emit WhitelistRegistrySet(address(whitelistRegistry_)); } + /// @notice Sets (or clears) the adapter allowlist. When unset, the adapter gate is off; once set, + /// every routed call other than a token approve must target a registered adapter. This is + /// an independent second gate alongside the whitelist: a call must satisfy both. + function setAdapterRegistry(IAdapterRegistry adapterRegistry_) external onlyOwner { + adapterRegistry = adapterRegistry_; + emit AdapterRegistrySet(address(adapterRegistry_)); + } + /// @notice Sets the module permitted to trigger liquidations. Until set, liquidation is /// disabled, since no caller can satisfy the liquidation-module check. function setLiquidationModule(address liquidationModule_) external onlyOwner { @@ -436,14 +448,20 @@ contract CreditManager is Ownable, ReentrancyGuard, ILiquidationTarget { _accrueIndex(); IWhitelistRegistry registry = whitelistRegistry; + IAdapterRegistry adapters = adapterRegistry; uint256 len = calls.length; for (uint256 i = 0; i < len; i++) { address target = calls[i].target; bytes calldata callData = calls[i].callData; + bytes4 selector = callData.length >= 4 ? bytes4(callData[:4]) : bytes4(0); if (address(registry) != address(0)) { - bytes4 selector = callData.length >= 4 ? bytes4(callData[:4]) : bytes4(0); if (!registry.isAllowed(target, selector)) revert CallNotWhitelisted(target, selector); } + // Independent second gate: every routed call must hit a registered adapter, except the + // token-approve leg that funds an adapter (its target is the token, not the adapter). + if (address(adapters) != address(0) && selector != IERC20.approve.selector) { + if (!adapters.isAdapter(target)) revert TargetNotAdapter(target); + } MarginAccount(account).execute(target, callData); } diff --git a/contracts/src/interfaces/IAdapterRegistry.sol b/contracts/src/interfaces/IAdapterRegistry.sol new file mode 100644 index 0000000..9ec59a2 --- /dev/null +++ b/contracts/src/interfaces/IAdapterRegistry.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +/// @title IAdapterRegistry +/// @notice View surface of the adapter allowlist consumed by the credit manager. Every routed +/// margin-account call other than a token approve must target an adapter registered here, +/// so an account can only ever invoke vetted adapter contracts. +interface IAdapterRegistry { + /// @notice True when `adapter` is a registered, approved adapter. + function isAdapter(address adapter) external view returns (bool); + + /// @notice The external protocol the adapter wraps, or the zero address when not registered. + function adapterTarget(address adapter) external view returns (address); +} diff --git a/contracts/test/Deploy.t.sol b/contracts/test/Deploy.t.sol index a4f7b29..584bc7f 100644 --- a/contracts/test/Deploy.t.sol +++ b/contracts/test/Deploy.t.sol @@ -6,6 +6,10 @@ import {DeployScript} from "../script/Deploy.s.sol"; import {Pool} from "../src/Pool.sol"; import {CreditManager} from "../src/CreditManager.sol"; import {AccessController} from "../src/AccessController.sol"; +import {AdapterRegistry} from "../src/AdapterRegistry.sol"; +import {WhitelistRegistry} from "../src/WhitelistRegistry.sol"; +import {CurveAdapter} from "../src/adapters/CurveAdapter.sol"; +import {LstAdapter} from "../src/adapters/LstAdapter.sol"; import {MockPriceOracle} from "./mocks/MockPriceOracle.sol"; /// @notice Runs the deployment script and asserts every wiring step was applied, so deployment @@ -87,6 +91,50 @@ contract DeployScriptTest is Test { assertEq(set.length, 2); } + function test_AdapterRegistryWiredAsGate() public view { + assertTrue(d.adapterRegistry != address(0)); + // Every credit manager consults the shared adapter registry. + assertEq(address(CreditManager(d.creditManager).adapterRegistry()), d.adapterRegistry); + assertEq(address(CreditManager(d.linkCreditManager).adapterRegistry()), d.adapterRegistry); + // Each market's swap adapter is registered against the router it wraps. + AdapterRegistry registry = AdapterRegistry(d.adapterRegistry); + assertTrue(registry.isAdapter(d.swapAdapter)); + assertEq(registry.adapterTarget(d.swapAdapter), d.swapRouter); + assertTrue(registry.isAdapter(d.linkSwapAdapter)); + assertEq(registry.adapterTarget(d.linkSwapAdapter), d.linkSwapRouter); + } + + function test_CurveAdapterDeployedWiredAndRegistered() public view { + assertTrue(d.curveAdapter != address(0) && d.curvePool != address(0) && d.curveLp != address(0)); + + // Registered against the (mock) pool it wraps, so it clears the adapter gate. + AdapterRegistry registry = AdapterRegistry(d.adapterRegistry); + assertTrue(registry.isAdapter(d.curveAdapter)); + assertEq(registry.adapterTarget(d.curveAdapter), d.curvePool); + + // Its liquidity selectors and the LP-token approve leg are whitelisted. + WhitelistRegistry whitelist = WhitelistRegistry(d.whitelistRegistry); + assertTrue(whitelist.isAllowed(d.curveAdapter, CurveAdapter.addLiquidity.selector)); + assertTrue(whitelist.isAllowed(d.curveAdapter, CurveAdapter.removeLiquidityOneCoin.selector)); + assertTrue(whitelist.allowedTarget(d.curveLp)); + } + + function test_LstAdapterDeployedWiredAndRegistered() public view { + assertTrue(d.lstAdapter != address(0) && d.steth != address(0) && d.wsteth != address(0)); + + // Registered against the wrapper it wraps into, so it clears the adapter gate. + AdapterRegistry registry = AdapterRegistry(d.adapterRegistry); + assertTrue(registry.isAdapter(d.lstAdapter)); + assertEq(registry.adapterTarget(d.lstAdapter), d.wsteth); + + // wrap/unwrap and both approve legs are whitelisted. + WhitelistRegistry whitelist = WhitelistRegistry(d.whitelistRegistry); + assertTrue(whitelist.isAllowed(d.lstAdapter, LstAdapter.wrap.selector)); + assertTrue(whitelist.isAllowed(d.lstAdapter, LstAdapter.unwrap.selector)); + assertTrue(whitelist.allowedTarget(d.steth)); + assertTrue(whitelist.allowedTarget(d.wsteth)); + } + /// @notice The manifest round-trips: writing it and parsing it back yields the same addresses and /// chain metadata the services need to start with no manual address entry. function test_ManifestRoundTrips() public { @@ -118,6 +166,11 @@ contract DeployScriptTest is Test { assertEq(vm.parseJsonString(json, ".basketMarket.collaterals[1].symbol"), "LINK"); assertEq(vm.parseJsonAddress(json, ".basketMarket.collaterals[1].collateralToken"), d.link); + // Shared periphery adapters are carried so the services can read them. + assertEq(vm.parseJsonAddress(json, ".adapterRegistry"), d.adapterRegistry); + assertEq(vm.parseJsonAddress(json, ".curveAdapter"), d.curveAdapter); + assertEq(vm.parseJsonAddress(json, ".lstAdapter"), d.lstAdapter); + vm.removeFile(path); } } diff --git a/contracts/test/MulticallWhitelist.t.sol b/contracts/test/MulticallWhitelist.t.sol index d49982e..5f91a5a 100644 --- a/contracts/test/MulticallWhitelist.t.sol +++ b/contracts/test/MulticallWhitelist.t.sol @@ -12,6 +12,8 @@ import {MarginAccount} from "../src/MarginAccount.sol"; import {RiskConfigurator} from "../src/RiskConfigurator.sol"; import {WhitelistRegistry} from "../src/WhitelistRegistry.sol"; import {IWhitelistRegistry} from "../src/interfaces/IWhitelistRegistry.sol"; +import {AdapterRegistry} from "../src/AdapterRegistry.sol"; +import {IAdapterRegistry} from "../src/interfaces/IAdapterRegistry.sol"; import {MockERC20} from "./mocks/MockERC20.sol"; import {MockPriceOracle} from "./mocks/MockPriceOracle.sol"; @@ -26,6 +28,7 @@ contract MulticallWhitelistTest is Test { MarginAccount internal accountImpl; RiskConfigurator internal riskConfigurator; WhitelistRegistry internal whitelist; + AdapterRegistry internal adapters; CreditManager internal cm; CreditFacade internal facade; @@ -51,6 +54,7 @@ contract MulticallWhitelistTest is Test { whitelist = new WhitelistRegistry(address(this)); cm.setWhitelistRegistry(IWhitelistRegistry(address(whitelist))); + adapters = new AdapterRegistry(address(this)); usd.mint(lp, 1000e18); vm.startPrank(lp); @@ -121,4 +125,84 @@ contract MulticallWhitelistTest is Test { vm.expectRevert(); cm.setWhitelistRegistry(IWhitelistRegistry(address(whitelist))); } + + // ----------------------------------------------------------------------- // + // Adapter-registry gate // + // ----------------------------------------------------------------------- // + + /// @dev A non-approve call whose target is not a token approve. `usd.totalSupply()` is harmless + /// (view, no state) so it executes cleanly once it clears the gates. + function _totalSupplyCall() internal view returns (CreditManager.MultiCall[] memory calls) { + calls = new CreditManager.MultiCall[](1); + calls[0] = CreditManager.MultiCall({ + target: address(usd), callData: abi.encodeWithSelector(IERC20.totalSupply.selector) + }); + } + + function test_AdapterGate_UnregisteredNonApproveReverts() public { + address account = _open(); + cm.setWhitelistRegistry(IWhitelistRegistry(address(0))); // isolate the adapter gate + cm.setAdapterRegistry(IAdapterRegistry(address(adapters))); + + vm.prank(borrower); + vm.expectRevert(abi.encodeWithSelector(CreditManager.TargetNotAdapter.selector, address(usd))); + facade.multicall(account, _totalSupplyCall()); + } + + function test_AdapterGate_RegisteredTargetPasses() public { + address account = _open(); + cm.setWhitelistRegistry(IWhitelistRegistry(address(0))); + cm.setAdapterRegistry(IAdapterRegistry(address(adapters))); + adapters.registerAdapter(address(usd), address(this)); // treat usd as a registered adapter + + // No revert: the call clears the adapter gate and executes. + vm.prank(borrower); + facade.multicall(account, _totalSupplyCall()); + } + + function test_AdapterGate_ApproveLegAllowedToNonAdapter() public { + address account = _open(); + cm.setWhitelistRegistry(IWhitelistRegistry(address(0))); + cm.setAdapterRegistry(IAdapterRegistry(address(adapters))); + + // The approve leg is carved out: it funds an adapter, so its target is the token, not an adapter. + vm.prank(borrower); + facade.multicall(account, _approveCall()); + assertEq(usd.allowance(account, address(this)), 1e18); + } + + function test_AdapterGate_ComposesWithWhitelist() public { + address account = _open(); + // Both gates on. A non-approve call must satisfy both: whitelisted AND a registered adapter. + cm.setAdapterRegistry(IAdapterRegistry(address(adapters))); + whitelist.setTarget(address(usd), true); + whitelist.setSelector(address(usd), IERC20.totalSupply.selector, true); + + // Whitelisted but not yet registered: the adapter gate still blocks it. + vm.prank(borrower); + vm.expectRevert(abi.encodeWithSelector(CreditManager.TargetNotAdapter.selector, address(usd))); + facade.multicall(account, _totalSupplyCall()); + + // Once registered, the call clears both gates. + adapters.registerAdapter(address(usd), address(this)); + vm.prank(borrower); + facade.multicall(account, _totalSupplyCall()); + } + + function test_AdapterGate_ClearingRemovesGate() public { + address account = _open(); + cm.setWhitelistRegistry(IWhitelistRegistry(address(0))); + cm.setAdapterRegistry(IAdapterRegistry(address(adapters))); + cm.setAdapterRegistry(IAdapterRegistry(address(0))); // clear it again + + // Gate off: an unregistered, non-approve target routes freely. + vm.prank(borrower); + facade.multicall(account, _totalSupplyCall()); + } + + function test_OnlyOwnerSetsAdapterRegistry() public { + vm.prank(makeAddr("intruder")); + vm.expectRevert(); + cm.setAdapterRegistry(IAdapterRegistry(address(adapters))); + } }