diff --git a/.env.example b/.env.example index 992829a..39d2157 100644 --- a/.env.example +++ b/.env.example @@ -1,100 +1,60 @@ -# VaultV2 Deployment Environment Variables -# -# For deployed contract addresses, see: -# https://docs.morpho.org/get-started/resources/addresses/ -# -# Morpho V1 Contracts: Morpho, Adaptive Curve IRM, Oracle Factory, MetaMorpho Factory -# Morpho V2 Contracts: VaultV2Factory, MorphoVaultV1AdapterFactory, MorphoMarketV1AdapterV2Factory, MorphoRegistry - -# ============================================================================= -# REQUIRED: Core Protocol Addresses -# ============================================================================= +# ============== +# Core Addresses +# ============== # Morpho Blue core contract -MORPHO_ADDRESS= +MORPHO_ADDRESS=0x99D31FEcc885204b4136ea5D2ef2a37F36E3AeB8 # Adaptive Curve IRM (Interest Rate Model) -IRM_ADDRESS= - -# ============================================================================= -# REQUIRED: Vault V2 Infrastructure -# ============================================================================= +IRM_ADDRESS=0x723E35C4CBb5B91669b62F130f2aC12931DF8049 # VaultV2Factory address -VAULT_V2_FACTORY= +VAULT_V2_FACTORY=0x3137e22F379A1Df004DDb10EBbecF1CfD8CbC0e2 # Adapter registry (MorphoRegistry) -ADAPTER_REGISTRY= - -# MorphoVaultV1AdapterFactory address -MORPHO_VAULT_V1_ADAPTER_FACTORY= +ADAPTER_REGISTRY=0x83dd673Fa2C4DE16cAAF0e6109845EeEbb6d1E00 # MorphoMarketV1AdapterV2Factory address -MORPHO_MARKET_V1_ADAPTER_V2_FACTORY= +MORPHO_MARKET_V1_ADAPTER_V2_FACTORY=0x01Ff31888b1FB51f951273C409f653Dba5Ce8200 -# ============================================================================= -# REQUIRED: Role Addresses -# ============================================================================= +DEPLOYER= + +# ===== +# Roles +# ===== # Owner address (receives ownership of deployed vaults) OWNER= -# ============================================================================= -# OPTIONAL: Additional Role Addresses -# ============================================================================= - # Curator address (defaults to OWNER if not set) CURATOR= # Allocator address (defaults to OWNER if not set) ALLOCATOR= -# Sentinel address (optional guardian role) +# Sentinel address SENTINEL= -# ============================================================================= -# OPTIONAL: Vault V1 Configuration (for migrations) -# ============================================================================= - -# MetaMorpho Factory V1.1 address -VAULT_V1_FACTORY_ADDRESS= - -# Target Vault V1 address (for migration scripts) -VAULT_V1= - -# ============================================================================= -# OPTIONAL: Oracle Configuration -# ============================================================================= +# ============ +# Vault config +# ============ -# MorphoChainlinkOracleV2 Factory address (if not set, MockOracle is deployed) -ORACLE_FACTORY_ADDRESS= +NAME= -# Use existing deployed oracle (skips oracle deployment) -DEPLOYED_ORACLE= +SYMBOL= -# ============================================================================= -# OPTIONAL: Timelock Configuration -# ============================================================================= +LOAN_TOKEN=0x8D82c4E3c936C7B5724A382a9c5a4E6Eb7aB6d5D -# Vault V2 timelock duration in seconds (default: 0) -TIMELOCK_DURATION= +COLLATERAL_TOKEN=0x3100000000000000000000000000000000000006 -# Adapter timelock duration in seconds (default: 259200 = 3 days) -ADAPTER_TIMELOCK_DURATION= +MARKET_ID=0x739380ffb364373a0e47422174b37be743ba39af80c1fb115418396431e7748a -# ============================================================================= -# OPTIONAL: Dead Deposit Configuration -# ============================================================================= +COLLATERAL_TOKEN_CAP= -# Amount for dead deposit to prevent share inflation attacks (default: 1e9) -DEAD_DEPOSIT_AMOUNT= +MARKET_CAP= -# ============================================================================= -# OPTIONAL: Deposit Script Variables -# ============================================================================= +PERFORMANCE_FEE= -# Deployed Vault V2 address (for deposit operations) -VAULT_V2_ADDRESS= +MANAGEMENT_FEE= -# Amount to deposit -DEPOSIT_AMOUNT= +FEE_RECIPIENT= diff --git a/justfile b/justfile new file mode 100644 index 0000000..b2b58e8 --- /dev/null +++ b/justfile @@ -0,0 +1,11 @@ +set shell := ["bash", "-eu", "-o", "pipefail", "-c"] + +script := "script/DeployVaultV2WithMarketAdapter.s.sol:DeployVaultV2WithMarketAdapter" + +# Simulate script execution (no on-chain broadcast). +vault-simulate *args: + forge script {{script}} -vvv --rpc-url https://rpc.mainnet.citrea.xyz {{args}} + +# Deploy on-chain and verify contracts. +vault-deploy *args: + forge script {{script}} -vvv --broadcast --verify --slow --rpc-url https://rpc.mainnet.citrea.xyz {{args}} diff --git a/script/DeployVaultV2.s.sol b/script/DeployVaultV2.s.sol deleted file mode 100644 index a94438e..0000000 --- a/script/DeployVaultV2.s.sol +++ /dev/null @@ -1,354 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.28; - -import {Script, console} from "forge-std/Script.sol"; - -import {IVaultV2} from "vault-v2/interfaces/IVaultV2.sol"; -import {VaultV2} from "vault-v2/VaultV2.sol"; -import {VaultV2Factory} from "vault-v2/VaultV2Factory.sol"; -import {MorphoVaultV1AdapterFactory} from "vault-v2/adapters/MorphoVaultV1AdapterFactory.sol"; -import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import {IERC4626 as IVaultV1} from "openzeppelin-contracts/contracts/interfaces/IERC4626.sol"; - -/** - * @title DeployVaultV2 - * @notice Deployment script for VaultV2 with comprehensive setup including: - * - VaultV2 instance creation - * - MorphoVaultV1 adapter deployment and configuration - * - Role management (owner, curator, allocator, sentinel) - * - Timelock configuration for critical functions - * - Dead deposit functionality for initial vault seeding - * @dev This script handles the complete deployment and configuration of a VaultV2 instance - * with all necessary adapters, roles, and security measures in place. - */ -contract DeployVaultV2 is Script { - /** - * @notice Main deployment function that reads configuration from environment variables - * @return vaultV2Address The address of the deployed VaultV2 instance - */ - function run() external returns (address) { - // Read configuration from environment variables - DeploymentConfig memory config = _readEnvironmentConfig(); - - return deployVaultV2WithConfig(config); - } - - /** - * @notice Deploy VaultV2 with explicit configuration parameters - * @param vaultOwner Address that will own the vault after deployment - * @param vaultCurator Address that can manage vault parameters - * @param vaultAllocator Address that can allocate funds to adapters - * @param vaultSentinel Address that can perform emergency actions (optional) - * @param timelockDurationSeconds Duration in seconds for timelocked functions - * @param sourceVaultV1 The VaultV1 instance to connect to - * @param adapterRegistry Address of the adapter registry - * @param vaultV2FactoryAddress Address of the VaultV2 factory - * @param morphoAdapterFactoryAddress Address of the Morpho adapter factory - * @param initialDeadDepositAmount Amount to deposit as dead deposit (0 to skip) - * @return vaultV2Address The address of the deployed VaultV2 instance - */ - function runWithArguments( - address vaultOwner, - address vaultCurator, - address vaultAllocator, - address vaultSentinel, - uint256 timelockDurationSeconds, - IVaultV1 sourceVaultV1, - address adapterRegistry, - address vaultV2FactoryAddress, - address morphoAdapterFactoryAddress, - uint256 initialDeadDepositAmount - ) public returns (address) { - DeploymentConfig memory config = DeploymentConfig({ - vaultOwner: vaultOwner, - vaultCurator: vaultCurator, - vaultAllocator: vaultAllocator, - vaultSentinel: vaultSentinel, - timelockDurationSeconds: timelockDurationSeconds, - sourceVaultV1: sourceVaultV1, - adapterRegistry: adapterRegistry, - vaultV2FactoryAddress: vaultV2FactoryAddress, - morphoAdapterFactoryAddress: morphoAdapterFactoryAddress, - initialDeadDepositAmount: initialDeadDepositAmount - }); - - return deployVaultV2WithConfig(config); - } - - /** - * @notice Configuration struct for deployment parameters - */ - struct DeploymentConfig { - address vaultOwner; - address vaultCurator; - address vaultAllocator; - address vaultSentinel; - uint256 timelockDurationSeconds; - IVaultV1 sourceVaultV1; - address adapterRegistry; - address vaultV2FactoryAddress; - address morphoAdapterFactoryAddress; - uint256 initialDeadDepositAmount; - } - - /** - * @notice Read configuration from environment variables - * @return config Deployment configuration struct - */ - function _readEnvironmentConfig() internal view returns (DeploymentConfig memory config) { - config.vaultOwner = vm.envAddress("OWNER"); - config.vaultCurator = vm.envAddress("CURATOR"); - config.vaultAllocator = vm.envAddress("ALLOCATOR"); - config.vaultSentinel = vm.envExists("SENTINEL") ? vm.envAddress("SENTINEL") : address(0); - config.timelockDurationSeconds = vm.envExists("TIMELOCK_DURATION") ? vm.envUint("TIMELOCK_DURATION") : 0; - config.sourceVaultV1 = IVaultV1(vm.envAddress("VAULT_V1")); - config.adapterRegistry = vm.envAddress("ADAPTER_REGISTRY"); - config.vaultV2FactoryAddress = vm.envAddress("VAULT_V2_FACTORY"); - config.morphoAdapterFactoryAddress = vm.envAddress("MORPHO_VAULT_V1_ADAPTER_FACTORY"); - config.initialDeadDepositAmount = vm.envExists("DEAD_DEPOSIT_AMOUNT") ? vm.envUint("DEAD_DEPOSIT_AMOUNT") : 0; - } - - /** - * @notice Main deployment logic with comprehensive vault setup - * @param config Deployment configuration - * @return deployedVaultV2Address Address of the deployed VaultV2 - */ - function deployVaultV2WithConfig(DeploymentConfig memory config) internal returns (address) { - address transactionOriginator = tx.origin; - - vm.startBroadcast(); - - // Phase 1: Deploy VaultV2 instance - VaultV2 deployedVaultV2 = - _deployVaultV2Instance(config.vaultV2FactoryAddress, transactionOriginator, config.sourceVaultV1.asset()); - - // Phase 2: Configure temporary permissions - _configureTemporaryPermissions(deployedVaultV2, transactionOriginator); - - // Phase 3: Deploy and configure Morpho adapter - address morphoAdapterAddress = _deployAndConfigureMorphoAdapter( - config.morphoAdapterFactoryAddress, address(deployedVaultV2), address(config.sourceVaultV1) - ); - - // Phase 4: Submit timelocked configuration changes - _submitTimelockedConfigurationChanges( - deployedVaultV2, transactionOriginator, config.vaultAllocator, config.adapterRegistry, morphoAdapterAddress - ); - - // Phase 5: Execute immediate configuration changes - _executeImmediateConfigurationChanges( - deployedVaultV2, transactionOriginator, config.vaultAllocator, config.adapterRegistry, morphoAdapterAddress - ); - - // Phase 6: Configure timelock settings - if (config.timelockDurationSeconds > 0) { - _configureTimelockSettings(deployedVaultV2, config.timelockDurationSeconds); - } - - // Phase 7: Set final role assignments - _setFinalRoleAssignments(deployedVaultV2, config.vaultOwner, config.vaultCurator, config.vaultSentinel); - - // Phase 8: Execute dead deposit if specified - if (config.initialDeadDepositAmount > 0) { - _executeDeadDeposit(deployedVaultV2, config.initialDeadDepositAmount); - } - - vm.stopBroadcast(); - - console.log("VaultV2 deployment completed successfully at:", address(deployedVaultV2)); - return address(deployedVaultV2); - } - - /** - * @notice Deploy the VaultV2 instance using the factory - * @param factoryAddress Address of the VaultV2 factory - * @param temporaryOwner Address that will temporarily own the vault - * @param underlyingAsset Address of the underlying asset token - * @return deployedVault The deployed VaultV2 instance - */ - function _deployVaultV2Instance(address factoryAddress, address temporaryOwner, address underlyingAsset) - internal - returns (VaultV2 deployedVault) - { - // Generate unique salt for deterministic deployment - bytes32 uniqueSalt = keccak256(abi.encodePacked(block.timestamp + gasleft())); - - deployedVault = - VaultV2(VaultV2Factory(factoryAddress).createVaultV2(temporaryOwner, underlyingAsset, uniqueSalt)); - - console.log("VaultV2 deployed at:", address(deployedVault)); - } - - /** - * @notice Configure temporary permissions for deployment process - * @param vault The VaultV2 instance to configure - * @param temporaryCurator Address to grant temporary curator role - */ - function _configureTemporaryPermissions(VaultV2 vault, address temporaryCurator) internal { - vault.setCurator(temporaryCurator); - console.log("Temporary curator role assigned to:", temporaryCurator); - } - - /** - * @notice Deploy and configure the MorphoVaultV1 adapter - * @param factoryAddress Address of the Morpho adapter factory - * @param vaultV2Address Address of the VaultV2 instance - * @param vaultV1Address Address of the source VaultV1 instance - * @return adapterAddress Address of the deployed adapter - */ - function _deployAndConfigureMorphoAdapter(address factoryAddress, address vaultV2Address, address vaultV1Address) - internal - returns (address adapterAddress) - { - adapterAddress = - MorphoVaultV1AdapterFactory(factoryAddress).createMorphoVaultV1Adapter(vaultV2Address, vaultV1Address); - - console.log("MorphoVaultV1Adapter deployed at:", adapterAddress); - } - - /** - * @notice Submit timelocked configuration changes - * @param vault The VaultV2 instance - * @param temporaryAllocator Address with temporary allocator role - * @param finalAllocator Address that will be the final allocator - * @param registry Address of the adapter registry - * @param adapter Address of the Morpho adapter - */ - function _submitTimelockedConfigurationChanges( - VaultV2 vault, - address temporaryAllocator, - address finalAllocator, - address registry, - address adapter - ) internal { - // Submit allocator role changes - vault.submit(abi.encodeCall(vault.setIsAllocator, (temporaryAllocator, true))); - if (temporaryAllocator != finalAllocator) { - vault.submit(abi.encodeCall(vault.setIsAllocator, (temporaryAllocator, false))); - vault.submit(abi.encodeCall(vault.setIsAllocator, (finalAllocator, true))); - } - - // Submit adapter registry configuration - vault.submit(abi.encodeCall(vault.setAdapterRegistry, (registry))); - - // Submit liquidity adapter configuration - vault.submit(abi.encodeCall(vault.setLiquidityAdapterAndData, (adapter, bytes("")))); - - // Submit adapter and cap configurations - bytes memory adapterIdData = abi.encode("this", adapter); - vault.submit(abi.encodeCall(vault.addAdapter, (adapter))); - vault.submit(abi.encodeCall(vault.increaseAbsoluteCap, (adapterIdData, type(uint128).max))); - vault.submit(abi.encodeCall(vault.increaseRelativeCap, (adapterIdData, 1e18))); - - console.log("All timelocked configuration changes submitted"); - } - - /** - * @notice Execute immediate configuration changes - * @param vault The VaultV2 instance - * @param temporaryAllocator Address with temporary allocator role - * @param finalAllocator Address that will be the final allocator - * @param registry Address of the adapter registry - * @param adapter Address of the Morpho adapter - */ - function _executeImmediateConfigurationChanges( - VaultV2 vault, - address temporaryAllocator, - address finalAllocator, - address registry, - address adapter - ) internal { - // Execute adapter registry configuration - vault.setAdapterRegistry(registry); - - // Execute allocator role configuration - vault.setIsAllocator(temporaryAllocator, true); - - // Execute adapter configuration - vault.addAdapter(adapter); - vault.setLiquidityAdapterAndData(adapter, bytes("")); - - // Execute cap configurations - bytes memory adapterIdData = abi.encode("this", adapter); - vault.increaseAbsoluteCap(adapterIdData, type(uint128).max); - vault.increaseRelativeCap(adapterIdData, 1e18); - - // Finalize allocator role changes - if (temporaryAllocator != finalAllocator) { - vault.setIsAllocator(temporaryAllocator, false); - vault.setIsAllocator(finalAllocator, true); - } - - // Abdicate setAdapterRegistry function to prevent future changes - vault.submit(abi.encodeCall(vault.abdicate, (IVaultV2.setAdapterRegistry.selector))); - vault.abdicate(IVaultV2.setAdapterRegistry.selector); - } - - /** - * @notice Configure timelock settings for critical functions - * @param vault The VaultV2 instance - * @param timelockDurationSeconds Duration in seconds for timelocked functions - */ - function _configureTimelockSettings(VaultV2 vault, uint256 timelockDurationSeconds) internal { - // Define function selectors that should be timelocked - bytes4[] memory timelockedSelectors = new bytes4[](10); - timelockedSelectors[0] = IVaultV2.setReceiveSharesGate.selector; - timelockedSelectors[1] = IVaultV2.setSendSharesGate.selector; - timelockedSelectors[2] = IVaultV2.setReceiveAssetsGate.selector; - timelockedSelectors[3] = IVaultV2.addAdapter.selector; - timelockedSelectors[4] = IVaultV2.increaseAbsoluteCap.selector; - timelockedSelectors[5] = IVaultV2.increaseRelativeCap.selector; - timelockedSelectors[6] = IVaultV2.setForceDeallocatePenalty.selector; - timelockedSelectors[7] = IVaultV2.abdicate.selector; - timelockedSelectors[8] = IVaultV2.removeAdapter.selector; - timelockedSelectors[9] = IVaultV2.increaseTimelock.selector; - - // Submit timelock increases for all selectors - for (uint256 i = 0; i < timelockedSelectors.length; i++) { - vault.submit(abi.encodeCall(vault.increaseTimelock, (timelockedSelectors[i], timelockDurationSeconds))); - } - console.log("Timelock increases submitted for", timelockedSelectors.length, "functions"); - - // Execute timelock increases for all selectors - for (uint256 i = 0; i < timelockedSelectors.length; i++) { - vault.increaseTimelock(timelockedSelectors[i], timelockDurationSeconds); - } - console.log("Timelock increases executed for", timelockedSelectors.length, "functions"); - } - - /** - * @notice Set final role assignments for the vault - * @param vault The VaultV2 instance - * @param vaultOwner Address that will own the vault - * @param vaultCurator Address that can manage vault parameters - * @param vaultSentinel Address that can perform emergency actions (optional) - */ - function _setFinalRoleAssignments(VaultV2 vault, address vaultOwner, address vaultCurator, address vaultSentinel) - internal - { - vault.setCurator(vaultCurator); - - if (vaultSentinel != address(0)) { - vault.setIsSentinel(vaultSentinel, true); - } - - vault.setOwner(vaultOwner); - - console.log("Final role assignments completed:"); - console.log(" Owner:", vaultOwner); - console.log(" Curator:", vaultCurator); - if (vaultSentinel != address(0)) { - console.log(" Sentinel:", vaultSentinel); - } - } - - /** - * @notice Execute dead deposit to seed the vault with initial liquidity - * @param vault The VaultV2 instance - * @param depositAmount Amount to deposit as dead deposit - */ - function _executeDeadDeposit(VaultV2 vault, uint256 depositAmount) internal { - IERC20(vault.asset()).approve(address(vault), depositAmount); - vault.deposit(depositAmount, address(0xdead)); // Dead address for burned shares} - } -} diff --git a/script/DeployVaultV2WithMarketAdapter.s.sol b/script/DeployVaultV2WithMarketAdapter.s.sol index e51361c..0e98a5a 100644 --- a/script/DeployVaultV2WithMarketAdapter.s.sol +++ b/script/DeployVaultV2WithMarketAdapter.s.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.28; import {Script, console} from "forge-std/Script.sol"; - import {IVaultV2} from "vault-v2/interfaces/IVaultV2.sol"; import {VaultV2} from "vault-v2/VaultV2.sol"; import {VaultV2Factory} from "vault-v2/VaultV2Factory.sol"; @@ -12,231 +11,280 @@ import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {IERC20Metadata} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {IMorpho, MarketParams, Id, Position} from "morpho-blue/src/interfaces/IMorpho.sol"; -/** - * @title DeployVaultV2WithMarketAdapter - * @notice Deployment script for VaultV2 with MorphoMarketV1AdapterV2 (direct Morpho Blue market access) - * @dev This script deploys a VaultV2 that connects directly to Morpho Blue markets, - * bypassing MetaMorpho (Vault V1). Use this when you want direct market access. - * - * Key differences from DeployVaultV2.s.sol (which uses MorphoVaultV1Adapter): - * - No Vault V1 (MetaMorpho) required - * - Direct access to Morpho Blue markets - * - 3-level cap structure: adapter → collateral token → market params - * - Adapter has its own independent timelock system - * - Markets must have dead deposits (1e9 shares to 0xdead) before use - * - * After deployment, you must configure market caps for each market you want to use. - * See docs/build_own_script.md for market configuration details. - * - * ============================================================================ - * MORPHO LISTING REQUIREMENT: Naming Restriction - * ============================================================================ - * The vault's name and symbol CANNOT contain the word "morpho" (case insensitive). - * - * This is validated during the listing process, not by this script. - * When creating your vault, ensure you choose a name/symbol that does NOT include: - * - "morpho", "Morpho", "MORPHO", etc. - * - * Example valid names: "My USDC Vault", "Blue Chip Yield" - * Example invalid names: "Morpho USDC Vault", "My MorphoVault" - * ============================================================================ - * - * ============================================================================ - * DEPLOYMENT PHASES OVERVIEW - * ============================================================================ - * Phase 1: Deploy VaultV2 instance via factory - * Phase 2: Configure temporary permissions for deployment - * Phase 3: Deploy MorphoMarketV1AdapterV2 via factory - * Phase 4: Submit timelocked configuration changes - * Phase 5: Execute immediate configuration changes + abdications - * Phase 6: Set final role assignments (owner, curator, sentinel) - * Phase 7: Configure market and liquidity adapter (if MARKET_ID provided) - * Phase 8: Execute vault dead deposit (inflation attack protection) - * Phase 9: Configure vault timelocks (LISTING REQUIREMENT) - * Phase 10: Configure adapter timelocks (LISTING REQUIREMENT) - * ============================================================================ - */ +/// @title DeployVaultV2WithMarketAdapter +/// @notice Deployment script for VaultV2 with MorphoMarketV1AdapterV2 (direct Morpho Blue market access) +/// @dev This script deploys a VaultV2 that connects directly to Morpho Blue markets, +/// bypassing MetaMorpho (Vault V1). Use this when you want direct market access. +/// +/// Key differences from DeployVaultV2.s.sol (which uses MorphoVaultV1Adapter): +/// - No Vault V1 (MetaMorpho) required +/// - Direct access to Morpho Blue markets +/// - 3-level cap structure: adapter → collateral token → market params +/// - Adapter has its own independent timelock system +/// - Markets must have dead deposits (1e9 or 1e12 shares to 0xDEAD) before use +/// +/// The script configures one initial market end-to-end (liquidity adapter + caps + dead deposit checks). +/// After deployment, configure additional markets/caps as needed. +/// See docs/build_own_script.md for market configuration details. +/// +/// ============================================================================ +/// MORPHO LISTING REQUIREMENT: Naming Restriction +/// ============================================================================ +/// The vault's name and symbol CANNOT contain the word "morpho" (case insensitive). +/// +/// This is validated during the listing process, not by this script. +/// When creating your vault, ensure you choose a name/symbol that does NOT include: +/// - "morpho", "Morpho", "MORPHO", etc. +/// +/// Example valid names: "My USDC Vault", "Blue Chip Yield" +/// Example invalid names: "Morpho USDC Vault", "My MorphoVault" +/// ============================================================================ +/// +/// ============================================================================ +/// DEPLOYMENT PHASES OVERVIEW +/// ============================================================================ +/// Phase 1: Deploy VaultV2 instance via factory +/// Phase 2: Set vault name and symbol +/// Phase 3: Configure temporary permissions for deployment +/// Phase 4: Deploy MorphoMarketV1AdapterV2 via factory +/// Phase 5: Submit timelocked configuration changes +/// Phase 6: Execute immediate configuration changes + abdications +/// Phase 7: Configure market and liquidity adapter +/// Phase 8: Execute vault dead deposit (inflation attack protection) +/// Phase 9: Configure fee recipients and fee rates +/// Phase 10: Configure VaultV2 timelocks +/// Phase 11: Configure adapter timelocks +/// Phase 12: Set final role assignments (owner, curator, sentinel) +/// Phase 13: Validate final deployment state +/// ============================================================================ contract DeployVaultV2WithMarketAdapter is Script { uint256 constant DEAD_DEPOSIT_HIGH_DECIMALS = 1e9; // For assets with >= 10 decimals uint256 constant DEAD_DEPOSIT_LOW_DECIMALS = 1e12; // For assets with <= 9 decimals uint8 constant DECIMALS_THRESHOLD = 10; - - /** - * @notice Configuration struct for deployment parameters - * @dev Each field serves a specific purpose in the deployment: - * - * ROLE ADDRESSES: - * - vaultOwner: Final owner after deployment, has full administrative control - * - vaultCurator: Can manage vault parameters (caps, markets, etc.) - * - vaultAllocator: Can allocate/deallocate funds between adapters - * - vaultSentinel: Emergency role for protective actions (optional) - * - * INFRASTRUCTURE: - * - asset: The underlying ERC20 token the vault accepts (e.g., USDC, WETH) - * - adapterRegistry: Morpho's registry that validates adapters - * - vaultV2FactoryAddress: Factory contract for creating VaultV2 instances - * - morphoMarketAdapterFactoryAddress: Factory for MorphoMarketV1AdapterV2 - * - * FIRST MARKET (OPTIONAL): - * - marketId: Morpho Blue market ID (bytes32(0) to skip market setup) - * - collateralTokenCap: Maximum allocation to this collateral token - * - marketCap: Maximum allocation to this specific market - * - * TIMELOCKS (MORPHO LISTING REQUIREMENT): - * - vaultTimelockDuration: Timelock for vault functions (minimum 3 days for listing) - * - adapterTimelockDuration: Timelock for adapter functions (minimum 3 days for listing) - */ - struct DeploymentConfig { - address vaultOwner; - address vaultCurator; - address vaultAllocator; - address vaultSentinel; - address asset; + uint256 constant WAD = 1e18; // 1 unit given 18 decimals + uint128 constant MAX_UINT128 = type(uint128).max; + address constant DEAD_ADDRESS = address(0xDEAD); + + /// @notice Configuration struct for deployment parameters + /// @dev Each field serves a specific purpose in the deployment: + /// + /// ROLE ADDRESSES: + /// - owner: Final owner after deployment, has full administrative control + /// - curator: Can manage vault parameters (caps, markets, etc.) + /// - allocator: Address expected to hold allocator permissions at the end + /// - sentinel: Optional emergency role for protective actions + /// + /// INFRASTRUCTURE: + /// - loanToken: Vault asset (underlying ERC20 accepted by the vault) + /// - collateralToken: Expected collateral token of the initial Morpho market + /// - adapterRegistry: Morpho's registry that validates adapters + /// - vaultV2FactoryAddress: Factory contract for creating VaultV2 instances + /// - morphoMarketAdapterFactoryAddress: Factory for MorphoMarketV1AdapterV2 + /// + /// FIRST MARKET: + /// - marketId: Morpho Blue market ID + /// - collateralTokenCap: Maximum allocation to this collateral token + /// - marketCap: Maximum allocation to this specific market + /// + /// FEES: + /// - performanceFee: Vault performance fee value + /// - managementFee: Vault management fee value + /// - feeRecipient: Recipient for both performance and management fees + /// + /// TIMELOCKS: + /// - Vault and adapter timelocks are configured in this script + struct Config { + string name; + string symbol; + address owner; + address curator; + address allocator; + address sentinel; + address loanToken; + address collateralToken; address adapterRegistry; address vaultV2FactoryAddress; address morphoMarketAdapterFactoryAddress; - // First market configuration (optional - set marketId to bytes32(0) to skip) bytes32 marketId; uint128 collateralTokenCap; uint128 marketCap; - // Timelock configuration (MORPHO LISTING REQUIREMENT) - // Set to 0 to skip timelocks (vault will NOT meet listing requirements) - uint256 vaultTimelockDuration; - uint256 adapterTimelockDuration; + uint256 performanceFee; + uint256 managementFee; + address feeRecipient; + } + + struct VaultTimelocks { + uint256 addAdapter; + uint256 increaseAbsoluteCap; + uint256 increaseRelativeCap; + uint256 setForceDeallocatePenalty; + uint256 abdicate; + uint256 removeAdapter; + uint256 increaseTimelock; + } + + struct AdapterTimelocks { + uint256 abdicate; + uint256 setSkimRecipient; + uint256 burnShares; + uint256 increaseTimelock; } - /** - * @notice Main deployment function that reads configuration from environment variables - * @return vaultV2Address The address of the deployed VaultV2 instance - */ + /// @notice Main deployment function that reads configuration from environment variables + /// @return vaultV2Address The address of the deployed VaultV2 instance function run() external returns (address) { - DeploymentConfig memory config = _readEnvironmentConfig(); + Config memory config = readEnvironmentConfig(); return deployVaultV2WithConfig(config); } - /** - * @notice Deploy VaultV2 with explicit configuration parameters - * @param vaultOwner Address that will own the vault after deployment - * @param vaultCurator Address that can manage vault parameters - * @param vaultAllocator Address that can allocate funds to adapters - * @param vaultSentinel Address that can perform emergency actions (optional) - * @param asset The underlying asset token address - * @param adapterRegistry Address of the adapter registry - * @param vaultV2FactoryAddress Address of the VaultV2 factory - * @param morphoMarketAdapterFactoryAddress Address of the MorphoMarketV1AdapterV2 factory - * @param marketId Morpho Blue market ID for first market (bytes32(0) to skip market setup) - * @param collateralTokenCap Absolute cap for collateral token level - * @param marketCap Absolute cap for market level - * @param vaultTimelockDuration Timelock duration for vault functions (0 to skip) - * @param adapterTimelockDuration Timelock duration for adapter functions (0 to skip) - * @return vaultV2Address The address of the deployed VaultV2 instance - */ + /// @notice Deploy VaultV2 with explicit configuration parameters + /// @param name The name of the vault + /// @param symbol The symbol of the vault + /// @param owner Address that will own the vault after deployment + /// @param curator Address that can manage vault parameters + /// @param allocator Address that can allocate funds to adapters + /// @param sentinel Address that can perform emergency actions (optional) + /// @param loanToken The underlying loan token address + /// @param collateralToken The underlying collateral token address + /// @param adapterRegistry Address of the adapter registry + /// @param vaultV2FactoryAddress Address of the VaultV2 factory + /// @param morphoMarketAdapterFactoryAddress Address of the MorphoMarketV1AdapterV2 factory + /// @param marketId Morpho Blue market ID for first market + /// @param collateralTokenCap Absolute cap for collateral token level + /// @param marketCap Absolute cap for market level + /// @param performanceFee The performance fee + /// @param managementFee The management fee + /// @param feeRecipient The fee recipient + /// @return vaultV2Address The address of the deployed VaultV2 instance function runWithArguments( - address vaultOwner, - address vaultCurator, - address vaultAllocator, - address vaultSentinel, - address asset, + string memory name, + string memory symbol, + address owner, + address curator, + address allocator, + address sentinel, + address loanToken, + address collateralToken, address adapterRegistry, address vaultV2FactoryAddress, address morphoMarketAdapterFactoryAddress, bytes32 marketId, uint128 collateralTokenCap, uint128 marketCap, - uint256 vaultTimelockDuration, - uint256 adapterTimelockDuration + uint256 performanceFee, + uint256 managementFee, + address feeRecipient ) public returns (address) { - DeploymentConfig memory config = DeploymentConfig({ - vaultOwner: vaultOwner, - vaultCurator: vaultCurator, - vaultAllocator: vaultAllocator, - vaultSentinel: vaultSentinel, - asset: asset, + Config memory config = Config({ + name: name, + symbol: symbol, + owner: owner, + curator: curator, + allocator: allocator, + sentinel: sentinel, + loanToken: loanToken, + collateralToken: collateralToken, adapterRegistry: adapterRegistry, vaultV2FactoryAddress: vaultV2FactoryAddress, morphoMarketAdapterFactoryAddress: morphoMarketAdapterFactoryAddress, marketId: marketId, collateralTokenCap: collateralTokenCap, marketCap: marketCap, - vaultTimelockDuration: vaultTimelockDuration, - adapterTimelockDuration: adapterTimelockDuration + performanceFee: performanceFee, + managementFee: managementFee, + feeRecipient: feeRecipient }); return deployVaultV2WithConfig(config); } - /** - * @notice Read configuration from environment variables - * @return config Deployment configuration struct - * @dev Environment variables: - * Required: OWNER, ASSET, ADAPTER_REGISTRY, VAULT_V2_FACTORY, MORPHO_MARKET_V1_ADAPTER_V2_FACTORY - * Optional: CURATOR, ALLOCATOR, SENTINEL (defaults to OWNER or address(0)) - * Market: MARKET_ID, COLLATERAL_TOKEN_CAP, MARKET_CAP (set MARKET_ID=0x0 to skip) - * Timelocks: VAULT_TIMELOCK_DURATION, ADAPTER_TIMELOCK_DURATION (in seconds, 0 to skip) - */ - function _readEnvironmentConfig() internal view returns (DeploymentConfig memory config) { - config.vaultOwner = vm.envAddress("OWNER"); - config.vaultCurator = vm.envOr("CURATOR", config.vaultOwner); - config.vaultAllocator = vm.envOr("ALLOCATOR", config.vaultOwner); - config.vaultSentinel = vm.envOr("SENTINEL", address(0)); - config.asset = vm.envAddress("ASSET"); + /// @notice Read configuration from environment variables + /// @return config Deployment configuration struct + /// @dev Environment variables: + /// Required: + /// NAME, SYMBOL, DEPLOYER, OWNER, LOAN_TOKEN, COLLATERAL_TOKEN, + /// ADAPTER_REGISTRY, VAULT_V2_FACTORY, MORPHO_MARKET_V1_ADAPTER_V2_FACTORY, + /// MARKET_ID, COLLATERAL_TOKEN_CAP, MARKET_CAP, + /// PERFORMANCE_FEE, MANAGEMENT_FEE, FEE_RECIPIENT + /// Optional: CURATOR, ALLOCATOR, SENTINEL (defaults to OWNER or address(0)) + function readEnvironmentConfig() internal view returns (Config memory config) { + config.name = vm.envString("NAME"); + config.symbol = vm.envString("SYMBOL"); + config.owner = vm.envAddress("OWNER"); + config.curator = vm.envOr("CURATOR", config.owner); + config.allocator = vm.envOr("ALLOCATOR", config.owner); + config.sentinel = vm.envOr("SENTINEL", address(0)); + config.loanToken = vm.envAddress("LOAN_TOKEN"); + config.collateralToken = vm.envAddress("COLLATERAL_TOKEN"); config.adapterRegistry = vm.envAddress("ADAPTER_REGISTRY"); config.vaultV2FactoryAddress = vm.envAddress("VAULT_V2_FACTORY"); config.morphoMarketAdapterFactoryAddress = vm.envAddress("MORPHO_MARKET_V1_ADAPTER_V2_FACTORY"); - // First market configuration (optional) - config.marketId = vm.envOr("MARKET_ID", bytes32(0)); - config.collateralTokenCap = uint128(vm.envOr("COLLATERAL_TOKEN_CAP", uint256(0))); - config.marketCap = uint128(vm.envOr("MARKET_CAP", uint256(0))); - // Timelock configuration (MORPHO LISTING REQUIREMENT: minimum 3 days = 259200 seconds) - config.vaultTimelockDuration = vm.envOr("VAULT_TIMELOCK_DURATION", uint256(0)); - config.adapterTimelockDuration = vm.envOr("ADAPTER_TIMELOCK_DURATION", uint256(0)); + // First market configuration + config.marketId = vm.envBytes32("MARKET_ID"); + config.collateralTokenCap = uint128(vm.envUint("COLLATERAL_TOKEN_CAP")); + config.marketCap = uint128(vm.envUint("MARKET_CAP")); + config.performanceFee = vm.envUint("PERFORMANCE_FEE"); + config.managementFee = vm.envUint("MANAGEMENT_FEE"); + config.feeRecipient = vm.envAddress("FEE_RECIPIENT"); } - /** - * @notice Main deployment logic with comprehensive vault setup - * @param config Deployment configuration - * @return deployedVaultV2Address Address of the deployed VaultV2 - */ - function deployVaultV2WithConfig(DeploymentConfig memory config) internal returns (address) { - address transactionOriginator = tx.origin; + function validateConfig(Config memory config) internal pure { + require(bytes(config.name).length > 0, "NAME is required"); + require(bytes(config.symbol).length > 0, "SYMBOL is required"); + require(config.owner != address(0), "OWNER is required"); + require(config.curator != address(0), "CURATOR is required"); + require(config.allocator != address(0), "ALLOCATOR is required"); + require(config.loanToken != address(0), "LOAN_TOKEN is required"); + require(config.collateralToken != address(0), "COLLATERAL_TOKEN is required"); + require(config.adapterRegistry != address(0), "ADAPTER_REGISTRY is required"); + require(config.vaultV2FactoryAddress != address(0), "VAULT_V2_FACTORY is required"); + require(config.morphoMarketAdapterFactoryAddress != address(0), "MORPHO_MARKET_V1_ADAPTER_V2_FACTORY is required"); + require(config.marketId != bytes32(0), "MARKET_ID is required"); + require(config.collateralTokenCap > 0, "COLLATERAL_TOKEN_CAP is required"); + require(config.marketCap > 0, "MARKET_CAP is required"); + require(config.feeRecipient != address(0), "FEE_RECIPIENT is required"); + } + + /// @notice Main deployment logic with comprehensive vault setup + function deployVaultV2WithConfig(Config memory config) internal returns (address) { + console.log("Deploying VaultV2..."); + address deployer = resolveDeployer(); + console.log("Deployer:", deployer); + validateConfig(config); vm.startBroadcast(); - // Phase 1: Deploy VaultV2 instance + console.log("1. Deploy VaultV2 instance"); VaultV2 deployedVaultV2 = - _deployVaultV2Instance(config.vaultV2FactoryAddress, transactionOriginator, config.asset); + deployVaultV2Instance(config.vaultV2FactoryAddress, deployer, config.loanToken, config.name, config.symbol); + + console.log("2. Set name and symbol"); + setNameAndSymbol(deployedVaultV2, config.name, config.symbol); - // Phase 2: Configure temporary permissions - _configureTemporaryPermissions(deployedVaultV2, transactionOriginator); + console.log("3. Configure temporary permissions"); + configureTemporaryPermissions(deployedVaultV2, deployer); - // Phase 3: Deploy and configure MorphoMarketV1AdapterV2 - address morphoMarketAdapterAddress = - _deployAndConfigureMorphoMarketAdapter(config.morphoMarketAdapterFactoryAddress, address(deployedVaultV2)); + console.log("4. Deploy and configure MorphoMarketV1AdapterV2"); + IMorphoMarketV1AdapterV2 adapter = + deployAndConfigureMorphoMarketAdapter(config.morphoMarketAdapterFactoryAddress, address(deployedVaultV2)); - // Phase 4: Submit timelocked configuration changes - _submitTimelockedConfigurationChanges( + console.log("5. Submit timelocked configuration changes"); + submitTimelockedConfigurationChanges( deployedVaultV2, - transactionOriginator, - config.vaultAllocator, + deployer, + config.allocator, config.adapterRegistry, - morphoMarketAdapterAddress + address(adapter) ); - // Phase 5: Execute immediate configuration changes (includes critical gates abdication) - _executeImmediateConfigurationChanges( + console.log("6. Execute immediate configuration changes timelocked earlier"); + executeImmediateConfigurationChanges( deployedVaultV2, - transactionOriginator, - config.vaultAllocator, + deployer, config.adapterRegistry, - morphoMarketAdapterAddress + address(adapter) ); - // Phase 6: Set final role assignments - _setFinalRoleAssignments(deployedVaultV2, config.vaultOwner, config.vaultCurator, config.vaultSentinel); - - // ============================================================================ - // Phase 7: Configure market and liquidity adapter if specified - // ============================================================================ // MORPHO LISTING REQUIREMENT: No Idle Liquidity // ============================================================================ // The listing requirement states: "Vault asset balance must be zero" @@ -245,12 +293,6 @@ contract DeployVaultV2WithMarketAdapter is Script { // idle in the vault contract. The liquidityAdapter automatically allocates // incoming deposits to the configured market. // - // WITHOUT a market configured (MARKET_ID = 0): - // - liquidityAdapter is NOT set - // - Deposits stay idle in the vault - // - This violates the "No Idle Liquidity" listing requirement - // - The vault will NOT be eligible for Morpho app listing - // // WITH a market configured: // - liquidityAdapter is set with encoded MarketParams // - Deposits automatically allocate to the market @@ -261,116 +303,61 @@ contract DeployVaultV2WithMarketAdapter is Script { // - MorphoMarketV1AdapterV2 requires encoded MarketParams in liquidityData // - If liquidityAdapter is set with empty data, allocate() will revert on abi.decode // - Without a market, we leave liquidityAdapter unset so deposits stay idle in vault - if (config.marketId != bytes32(0)) { - _configureMarketAndLiquidityAdapter( - deployedVaultV2, IMorphoMarketV1AdapterV2(morphoMarketAdapterAddress), config - ); - } else { - console.log("Phase 7: Skipped - no MARKET_ID provided"); - console.log(" liquidityAdapter NOT set - deposits will stay idle in vault"); - console.log(" WARNING: Vault will NOT meet 'No Idle Liquidity' listing requirement"); - console.log(" Configure a market before accepting external deposits"); - } + console.log("7. Configure market and liquidity adapter"); + configureMarketAndLiquidityAdapter(deployedVaultV2, adapter, config); - // Phase 8: Execute vault dead deposit (always required for inflation attack protection) - // Now works correctly because liquidityAdapter is either: - // - Set with encoded MarketParams (allocates to market) - // - Not set (deposits stays idle in vault) - _executeVaultDeadDeposit(deployedVaultV2, config.asset); + console.log("8. Execute dead deposit"); + executeDeadDeposit(deployedVaultV2, IMorpho(adapter.morpho()), config); - // ============================================================================ - // Phase 9: Configure vault timelocks (BEFORE transferring ownership) - // ============================================================================ - // MORPHO LISTING REQUIREMENT: Timelocks must be configured - // - 7 days minimum: increaseTimelock, removeAdapter, abdicate - // - 3 days minimum: addAdapter, increaseRelativeCap, setForceDeallocatePenalty, increaseAbsoluteCap - // - // NOTE: We configure all functions with the same duration for simplicity. - // The minimum required is 3 days (259200 seconds) for listing eligibility. - // ============================================================================ - if (config.vaultTimelockDuration > 0) { - _configureVaultTimelocks(deployedVaultV2, config.vaultTimelockDuration); - } else { - console.log("Phase 9: Skipped - VAULT_TIMELOCK_DURATION is 0"); - console.log(" WARNING: Vault will NOT meet Morpho listing requirements without timelocks"); - } + console.log("9. Set the fees for the vault"); + setFees(deployedVaultV2, config.performanceFee, config.managementFee, config.feeRecipient); - // ============================================================================ - // Phase 10: Configure adapter timelocks - // ============================================================================ - // MORPHO LISTING REQUIREMENT: burnShares timelock must be >= 3 days - // - // The adapter has its own independent timelock system separate from the vault. - // Key functions that need timelocks: - // - burnShares: Prevents immediate share burning (protects against manipulation) - // - setSkimRecipient: Controls who receives skimmed tokens - // - abdicate: Permanently disables functions - // ============================================================================ - if (config.adapterTimelockDuration > 0) { - _configureAdapterTimelocks( - IMorphoMarketV1AdapterV2(morphoMarketAdapterAddress), config.adapterTimelockDuration - ); - } else { - console.log("Phase 10: Skipped - ADAPTER_TIMELOCK_DURATION is 0"); - console.log(" WARNING: Adapter will NOT meet Morpho listing requirements without timelocks"); - } + console.log("10. Configure vault timelocks"); + configureVaultTimelocks(deployedVaultV2); + + console.log("11. Configure adapter timelocks"); + configureAdapterTimelocks(adapter); + + console.log("12. Set final role assignments"); + setFinalRoleAssignments(deployedVaultV2, config.owner, config.curator, config.allocator, config.sentinel); vm.stopBroadcast(); + console.log("13. Validate final state"); + validateFinalState(deployedVaultV2, adapter, config); + console.log(""); - console.log("=== DEPLOYMENT COMPLETE ==="); + console.log("======== DEPLOYMENT COMPLETE ========"); console.log("VaultV2:", address(deployedVaultV2)); - console.log("MorphoMarketV1AdapterV2:", morphoMarketAdapterAddress); - console.log(""); - if (config.marketId == bytes32(0)) { - console.log("NEXT STEPS:"); - console.log("1. Ensure target markets have dead deposits (1e9 shares to 0xdead)"); - console.log("2. Configure collateral token caps for each collateral you want to use"); - console.log("3. Configure market caps for each market you want to allocate to"); - console.log("See docs/build_own_script.md for detailed instructions."); - } else { - console.log("First market configured. To add more markets:"); - console.log("1. Ensure target markets have dead deposits (1e9 shares to 0xdead)"); - console.log("2. Configure collateral token caps and market caps via vault functions"); - } - console.log("==========================="); + console.log("MorphoMarketV1AdapterV2:", address(adapter)); return address(deployedVaultV2); } - /** - * @notice Deploy the VaultV2 instance using the factory - */ - function _deployVaultV2Instance(address factoryAddress, address temporaryOwner, address underlyingAsset) + ////////////////// + /// Deployment /// + ////////////////// + + /// @notice Deploy the VaultV2 instance using the factory + function deployVaultV2Instance(address factoryAddress, address temporaryOwner, address underlyingAsset, string memory name, string memory symbol) internal - returns (VaultV2 deployedVault) + returns (VaultV2) { - bytes32 uniqueSalt = keccak256(abi.encodePacked(block.timestamp + gasleft())); + bytes32 salt = keccak256(abi.encodePacked(name, symbol)); + VaultV2Factory factory = VaultV2Factory(factoryAddress); + address vaultAddress = factory.createVaultV2(temporaryOwner, underlyingAsset, salt); - deployedVault = - VaultV2(VaultV2Factory(factoryAddress).createVaultV2(temporaryOwner, underlyingAsset, uniqueSalt)); - - console.log("Phase 1: VaultV2 deployed at:", address(deployedVault)); - } - - /** - * @notice Configure temporary permissions for deployment process - */ - function _configureTemporaryPermissions(VaultV2 vault, address temporaryCurator) internal { - vault.setCurator(temporaryCurator); - console.log("Phase 2: Temporary curator assigned:", temporaryCurator); + console.log("- VaultV2 deployed at:", vaultAddress); + return VaultV2(vaultAddress); } - /** - * @notice Deploy and verify the MorphoMarketV1AdapterV2 - */ - function _deployAndConfigureMorphoMarketAdapter(address factoryAddress, address vaultV2Address) + /// @notice Deploy and verify the MorphoMarketV1AdapterV2 + function deployAndConfigureMorphoMarketAdapter(address factoryAddress, address vaultV2Address) internal - returns (address adapterAddress) + returns (IMorphoMarketV1AdapterV2) { IMorphoMarketV1AdapterV2Factory factory = IMorphoMarketV1AdapterV2Factory(factoryAddress); - - adapterAddress = factory.createMorphoMarketV1AdapterV2(vaultV2Address); + address adapterAddress = factory.createMorphoMarketV1AdapterV2(vaultV2Address); // Verify factory registration require(factory.isMorphoMarketV1AdapterV2(adapterAddress), "Adapter not registered in factory"); @@ -379,15 +366,36 @@ contract DeployVaultV2WithMarketAdapter is Script { IMorphoMarketV1AdapterV2 adapter = IMorphoMarketV1AdapterV2(adapterAddress); require(adapter.parentVault() == vaultV2Address, "Parent vault mismatch"); - console.log("Phase 3: MorphoMarketV1AdapterV2 deployed at:", adapterAddress); - console.log(" Morpho:", adapter.morpho()); - console.log(" Adaptive Curve IRM:", adapter.adaptiveCurveIrm()); + console.log("- MorphoMarketV1AdapterV2 deployed at:", adapterAddress); + console.log("- Morpho:", adapter.morpho()); + console.log("- Adaptive Curve IRM:", adapter.adaptiveCurveIrm()); + + return adapter; } - /** - * @notice Submit timelocked configuration changes (includes critical gates abdication) - */ - function _submitTimelockedConfigurationChanges( + ///////////////////// + /// Configuration /// + ///////////////////// + + /// @notice Set the name and symbol for the vault + function setNameAndSymbol(VaultV2 vault, string memory name, string memory symbol) internal { + vault.setName(name); + vault.setSymbol(symbol); + console.log("- Name and symbol set:"); + console.log("- name:", vault.name()); + console.log("- symbol:", vault.symbol()); + } + + /// @notice Configure temporary permissions for deployment process + function configureTemporaryPermissions(VaultV2 vault, address temporaryCurator) internal { + vault.setCurator(temporaryCurator); + console.log("- Temporary curator assigned:", temporaryCurator); + } + + /// @notice Submit configuration actions while timelocks are still zero + /// @dev Every state-changing call executed later in this script is submitted first so the + /// queued operation is present before the corresponding direct call. + function submitTimelockedConfigurationChanges( VaultV2 vault, address temporaryAllocator, address finalAllocator, @@ -405,39 +413,29 @@ contract DeployVaultV2WithMarketAdapter is Script { vault.submit(abi.encodeCall(vault.setAdapterRegistry, (registry))); // NOTE: liquidityAdapterAndData is NOT set here - it requires encoded MarketParams - // It will be set in _configureMarketAndLiquidityAdapter when a market is specified + // It will be set in configureMarketAndLiquidityAdapter when a market is specified // Submit adapter and cap configurations bytes memory adapterIdData = abi.encode("this", adapter); vault.submit(abi.encodeCall(vault.addAdapter, (adapter))); - vault.submit(abi.encodeCall(vault.increaseAbsoluteCap, (adapterIdData, type(uint128).max))); - vault.submit(abi.encodeCall(vault.increaseRelativeCap, (adapterIdData, 1e18))); + vault.submit(abi.encodeCall(vault.increaseAbsoluteCap, (adapterIdData, MAX_UINT128))); + vault.submit(abi.encodeCall(vault.increaseRelativeCap, (adapterIdData, WAD))); // Submit abdication for setAdapterRegistry vault.submit(abi.encodeCall(vault.abdicate, (IVaultV2.setAdapterRegistry.selector))); - // Verify gates are set to address(0) before submitting abdication - require(vault.receiveSharesGate() == address(0), "receiveSharesGate must be address(0) before abdication"); - require(vault.sendSharesGate() == address(0), "sendSharesGate must be address(0) before abdication"); - require(vault.receiveAssetsGate() == address(0), "receiveAssetsGate must be address(0) before abdication"); - // Submit abdication for critical gates (preserves non-custodial properties) vault.submit(abi.encodeCall(vault.abdicate, (IVaultV2.setReceiveSharesGate.selector))); vault.submit(abi.encodeCall(vault.abdicate, (IVaultV2.setSendSharesGate.selector))); - vault.submit(abi.encodeCall(vault.abdicate, (IVaultV2.setReceiveAssetsGate.selector))); - - console.log("Phase 4: Timelocked configuration changes submitted"); - console.log(" Includes critical gates abdication requests"); + vault.submit(abi.encodeCall(vault.abdicate, (IVaultV2.setReceiveAssetsGate.selector))); + console.log("- Timelocked configuration changes submitted successfully"); } - /** - * @notice Execute immediate configuration changes (includes critical gates abdication) - * @dev Executes all configuration and abdications while timelock is still 0 - */ - function _executeImmediateConfigurationChanges( + /// @notice Execute initial configuration immediately (while timelock == 0) + /// @dev This applies adapter registry, allocator, adapter caps, and required abdications. + function executeImmediateConfigurationChanges( VaultV2 vault, address temporaryAllocator, - address finalAllocator, address registry, address adapter ) internal { @@ -451,22 +449,24 @@ contract DeployVaultV2WithMarketAdapter is Script { vault.addAdapter(adapter); // NOTE: liquidityAdapterAndData is NOT set here - it requires encoded MarketParams - // It will be set in _configureMarketAndLiquidityAdapter when a market is specified + // It will be set in configureMarketAndLiquidityAdapter when a market is specified // Execute adapter-level cap configurations bytes memory adapterIdData = abi.encode("this", adapter); - vault.increaseAbsoluteCap(adapterIdData, type(uint128).max); - vault.increaseRelativeCap(adapterIdData, 1e18); - - // Finalize allocator role changes - if (temporaryAllocator != finalAllocator) { - vault.setIsAllocator(temporaryAllocator, false); - vault.setIsAllocator(finalAllocator, true); - } + vault.increaseAbsoluteCap(adapterIdData, MAX_UINT128); + vault.increaseRelativeCap(adapterIdData, WAD); + // Verify adapterRegistry is set before abdication + require(vault.adapterRegistry() == registry, "adapterRegistry must be set to registry before abdication"); + // Execute abdication for setAdapterRegistry vault.abdicate(IVaultV2.setAdapterRegistry.selector); + // Verify gates are set to address(0) before abdication + require(vault.receiveSharesGate() == address(0), "receiveSharesGate must be address(0) before abdication"); + require(vault.sendSharesGate() == address(0), "sendSharesGate must be address(0) before abdication"); + require(vault.receiveAssetsGate() == address(0), "receiveAssetsGate must be address(0) before abdication"); + // Execute abdication for critical gates (preserves non-custodial properties) vault.abdicate(IVaultV2.setReceiveSharesGate.selector); vault.abdicate(IVaultV2.setSendSharesGate.selector); @@ -478,89 +478,84 @@ contract DeployVaultV2WithMarketAdapter is Script { require(vault.abdicated(IVaultV2.setSendSharesGate.selector), "setSendSharesGate abdication failed"); require(vault.abdicated(IVaultV2.setReceiveAssetsGate.selector), "setReceiveAssetsGate abdication failed"); - console.log("Phase 5: Immediate configuration changes executed"); - console.log(" Adapter registry set and abdicated"); - console.log(" Adapter-level caps configured (unlimited)"); - console.log(" Critical gates abdicated (non-custodial properties preserved)"); + console.log("- Immediate configuration changes executed successfully:"); + console.log("- Adapter registry set and abdicated"); + console.log("- Adapter-level caps configured (unlimited)"); + console.log("- Critical gates abdicated (non-custodial properties preserved)"); } - /** - * @notice Set final role assignments for the vault - */ - function _setFinalRoleAssignments(VaultV2 vault, address vaultOwner, address vaultCurator, address vaultSentinel) + /// @notice Set final role assignments for the vault + function setFinalRoleAssignments(VaultV2 vault, address owner, address curator, address allocator, address sentinel) internal { - vault.setCurator(vaultCurator); - - if (vaultSentinel != address(0)) { - vault.setIsSentinel(vaultSentinel, true); + address deployer = resolveDeployer(); + if (deployer != allocator) { + vault.setIsAllocator(deployer, false); + console.log("- Allocator role renounced by deployer"); + vault.setIsAllocator(allocator, true); + console.log("- Allocator role granted to", allocator); } - - vault.setOwner(vaultOwner); - - console.log("Phase 6: Final role assignments completed"); - console.log(" Owner:", vaultOwner); - console.log(" Curator:", vaultCurator); - if (vaultSentinel != address(0)) { - console.log(" Sentinel:", vaultSentinel); + if (sentinel != address(0)) { + vault.setIsSentinel(sentinel, true); + console.log("- Sentinel role granted to", sentinel); } + vault.setCurator(curator); + console.log("- Curator role granted to", curator); + vault.setOwner(owner); + console.log("- Owner role granted to", owner); + + console.log("- Final role assignments completed"); } - /** - * @notice Get the required dead deposit amount based on asset decimals - * @dev Returns 1e9 for assets with >= 10 decimals, 1e12 for assets with <= 9 decimals - * This ensures sufficient protection against inflation attacks regardless of token decimals - */ - function _getDeadDepositAmount(address asset) internal view returns (uint256) { + /// @notice Get the required dead deposit amount based on asset decimals + /// @dev Returns 1e9 for assets with >= 10 decimals, 1e12 for assets with <= 9 decimals + /// This ensures sufficient protection against inflation attacks regardless of token decimals + function getDeadDepositAmount(address asset) internal view returns (uint256) { uint8 decimals = IERC20Metadata(asset).decimals(); return decimals >= DECIMALS_THRESHOLD ? DEAD_DEPOSIT_HIGH_DECIMALS : DEAD_DEPOSIT_LOW_DECIMALS; } - /** - * @notice Execute dead deposit to seed the vault with initial liquidity - * @dev Always required for inflation attack protection. Amount is determined by asset decimals. - * In simulation mode (without --broadcast), the deployer may not have tokens, - * so we use a try/catch to handle this gracefully. - */ - function _executeVaultDeadDeposit(VaultV2 vault, address asset) internal { - uint256 depositAmount = _getDeadDepositAmount(asset); - address depositor = tx.origin; + /// @notice Execute dead deposit to seed the vault with initial liquidity + /// @dev Mints shares to 0xDEAD for inflation attack protection. The required share amount + /// depends on loan token decimals and is verified against market dead supply shares. + function executeDeadDeposit(VaultV2 vault, IMorpho morpho, Config memory config) internal { + IERC20 asset = IERC20(config.loanToken); + uint256 sharesAmount = getDeadDepositAmount(address(asset)); + console.log("- Dead deposit amount:", sharesAmount); + address depositor = resolveDeployer(); + + uint256 assetsRequired = vault.previewMint(sharesAmount); + console.log("- Assets required:", assetsRequired); // Check deployer's balance - uint256 balance = IERC20(asset).balanceOf(depositor); - if (balance < depositAmount) { - console.log("Phase 8: SKIPPED - Insufficient token balance for dead deposit"); - console.log(" Required:", depositAmount); - console.log(" Available:", balance); - console.log(" NOTE: In production, ensure deployer has sufficient tokens"); - console.log(" The dead deposit is REQUIRED before the vault can accept external deposits"); - return; - } + uint256 balance = asset.balanceOf(depositor); + require(balance >= assetsRequired, "Insufficient token balance for dead deposit"); + + asset.approve(address(vault), assetsRequired); + vault.mint(sharesAmount, DEAD_ADDRESS); + console.log("- Vault dead deposit executed:", sharesAmount, "wei of shares to 0xDEAD"); - IERC20(asset).approve(address(vault), depositAmount); - vault.deposit(depositAmount, address(0xdead)); - console.log("Phase 8: Vault dead deposit executed:", depositAmount, "wei to 0xdead"); + // Check if market has sufficient dead deposit + Position memory deadPosition = IMorpho(morpho).position(Id.wrap(config.marketId), DEAD_ADDRESS); + uint256 deadSupplyShares = deadPosition.supplyShares; + require(deadSupplyShares >= sharesAmount, "Market does not have sufficient dead deposit"); } - /** - * @notice Configure the first market with liquidity adapter setup and cap configuration - * @dev This function: - * 1. Looks up MarketParams from Morpho using idToMarketParams - * 2. Validates market params (loanToken matches asset, irm matches adaptiveCurveIrm) - * 3. Sets liquidityAdapterAndData with encoded MarketParams (required for allocate) - * 4. Checks if market has dead deposit (>= required shares at 0xdead) - * 5. If not, supplies to create dead deposit - * 6. Configures collateral token cap - * 7. Configures market cap - * - * IMPORTANT: liquidityAdapterAndData must be set with encoded MarketParams because - * MorphoMarketV1AdapterV2.allocate() does: `abi.decode(data, (MarketParams))` - * Setting it with empty data causes allocate to revert. - */ - function _configureMarketAndLiquidityAdapter( + /// @notice Configure the first market with liquidity adapter setup and cap configuration + /// @dev This function: + /// 1. Looks up MarketParams from Morpho using idToMarketParams + /// 2. Validates market params (loanToken matches asset, irm matches adaptiveCurveIrm) + /// 3. Sets liquidityAdapterAndData with encoded MarketParams (required for allocate) + /// 4. Configures collateral token cap (absolute + 100% relative) + /// 5. Configures market cap (absolute + 100% relative) + /// + /// IMPORTANT: liquidityAdapterAndData must be set with encoded MarketParams because + /// MorphoMarketV1AdapterV2.allocate() does: `abi.decode(data, (MarketParams))` + /// Setting it with empty data causes allocate to revert. + function configureMarketAndLiquidityAdapter( VaultV2 vault, IMorphoMarketV1AdapterV2 adapter, - DeploymentConfig memory config + Config memory config ) internal { address morpho = adapter.morpho(); @@ -568,149 +563,251 @@ contract DeployVaultV2WithMarketAdapter is Script { MarketParams memory marketParams = IMorpho(morpho).idToMarketParams(Id.wrap(config.marketId)); // Validate market params - _validateMarketParams(marketParams, config.asset, adapter.adaptiveCurveIrm()); - - console.log("Phase 7: Configuring market and liquidity adapter"); - console.log(" Market ID:", vm.toString(config.marketId)); - console.log(" Collateral Token:", marketParams.collateralToken); - console.log(" Oracle:", marketParams.oracle); - console.log(" LLTV:", marketParams.lltv); - - // KEY FIX: Set liquidityAdapterAndData with encoded MarketParams - // This is required because MorphoMarketV1AdapterV2.allocate() decodes the data as MarketParams + validateMarketParams(marketParams, config.loanToken, config.collateralToken, adapter.adaptiveCurveIrm()); + + console.log("- Configuring market and liquidity adapter"); + console.log("- Market ID: ", vm.toString(config.marketId)); + console.log("- Loan Token: ", marketParams.loanToken); + console.log("- Collateral Token:", marketParams.collateralToken); + console.log("- IRM: ", marketParams.irm); + console.log("- Oracle: ", marketParams.oracle); + console.log("- LLTV: ", marketParams.lltv); + + // Set liquidityAdapterAndData with encoded MarketParams. + // MorphoMarketV1AdapterV2.allocate() decodes liquidityData as MarketParams. bytes memory liquidityData = abi.encode(marketParams); vault.submit(abi.encodeCall(vault.setLiquidityAdapterAndData, (address(adapter), liquidityData))); vault.setLiquidityAdapterAndData(address(adapter), liquidityData); - console.log(" Liquidity adapter set with encoded MarketParams"); - - // Determine required dead deposit amount based on asset decimals - uint256 requiredDeadDeposit = _getDeadDepositAmount(config.asset); - - // Check if market has sufficient dead deposit - Position memory deadPosition = IMorpho(morpho).position(Id.wrap(config.marketId), address(0xdead)); - uint256 deadSupplyShares = deadPosition.supplyShares; - - if (deadSupplyShares < requiredDeadDeposit) { - console.log(" Market needs dead deposit, current shares:", deadSupplyShares); - console.log(" Required shares:", requiredDeadDeposit); - - // Check deployer's balance for market dead deposit - uint256 balance = IERC20(config.asset).balanceOf(tx.origin); - if (balance < requiredDeadDeposit) { - console.log(" SKIPPED - Insufficient token balance for market dead deposit"); - console.log(" Required:", requiredDeadDeposit); - console.log(" Available:", balance); - console.log(" NOTE: In production, ensure market has dead deposit before use"); - } else { - // Approve and supply to create dead deposit - IERC20(config.asset).approve(morpho, requiredDeadDeposit); - IMorpho(morpho).supply(marketParams, requiredDeadDeposit, 0, address(0xdead), hex""); - console.log(" Market dead deposit created:", requiredDeadDeposit, "assets to 0xdead"); - } - } else { - console.log(" Market already has sufficient dead deposit:", deadSupplyShares, "shares"); - } + console.log("- Liquidity adapter set with encoded MarketParams"); // Configure collateral token caps (absolute + 100% relative) bytes memory collateralTokenIdData = abi.encode("collateralToken", marketParams.collateralToken); vault.submit(abi.encodeCall(vault.increaseAbsoluteCap, (collateralTokenIdData, config.collateralTokenCap))); - vault.submit(abi.encodeCall(vault.increaseRelativeCap, (collateralTokenIdData, 1e18))); + vault.submit(abi.encodeCall(vault.increaseRelativeCap, (collateralTokenIdData, WAD))); vault.increaseAbsoluteCap(collateralTokenIdData, config.collateralTokenCap); - vault.increaseRelativeCap(collateralTokenIdData, 1e18); + vault.increaseRelativeCap(collateralTokenIdData, WAD); - console.log(" Collateral token cap set:", config.collateralTokenCap, "(absolute), 100% (relative)"); + console.log("- Collateral token cap set:", config.collateralTokenCap, "(absolute), 100% (relative)"); // Configure market caps (absolute + 100% relative) bytes memory marketIdData = abi.encode("this/marketParams", address(adapter), marketParams); vault.submit(abi.encodeCall(vault.increaseAbsoluteCap, (marketIdData, config.marketCap))); - vault.submit(abi.encodeCall(vault.increaseRelativeCap, (marketIdData, 1e18))); + vault.submit(abi.encodeCall(vault.increaseRelativeCap, (marketIdData, WAD))); vault.increaseAbsoluteCap(marketIdData, config.marketCap); - vault.increaseRelativeCap(marketIdData, 1e18); + vault.increaseRelativeCap(marketIdData, WAD); + + console.log("- Market cap set:", config.marketCap, "(absolute), 100% (relative)"); + } + + /// @notice Set the fees for the vault + function setFees(VaultV2 vault, uint256 performanceFee, uint256 managementFee, address feeRecipient) internal { + require(feeRecipient != address(0), "feeRecipient cannot be zero"); + + vault.submit(abi.encodeCall(vault.setPerformanceFeeRecipient, (feeRecipient))); + vault.submit(abi.encodeCall(vault.setManagementFeeRecipient, (feeRecipient))); + vault.submit(abi.encodeCall(vault.setPerformanceFee, (performanceFee))); + vault.submit(abi.encodeCall(vault.setManagementFee, (managementFee))); + + vault.setPerformanceFeeRecipient(feeRecipient); + vault.setManagementFeeRecipient(feeRecipient); + vault.setPerformanceFee(performanceFee); + vault.setManagementFee(managementFee); + + console.log("- Fees set"); + console.log("- Performance Fee:", vault.performanceFee()); + console.log("- Management Fee:", vault.managementFee()); + console.log("- Performance Fee Recipient:", vault.performanceFeeRecipient()); + console.log("- Management Fee Recipient:", vault.managementFeeRecipient()); + } + + function getVaultTimelocks() internal pure returns (VaultTimelocks memory) { + return VaultTimelocks({ + // 3-day minimum functions + addAdapter: 3 days, + increaseAbsoluteCap: 3 days, + increaseRelativeCap: 3 days, + setForceDeallocatePenalty: 3 days, + // 7-day minimum functions + abdicate: 7 days, + removeAdapter: 7 days, + increaseTimelock: 7 days + }); + } + + function getAdapterTimelocks() internal pure returns (AdapterTimelocks memory) { + return AdapterTimelocks({ + abdicate: 3 days, + setSkimRecipient: 3 days, + burnShares: 3 days, + increaseTimelock: 3 days + }); + } - console.log(" Market cap set:", config.marketCap, "(absolute), 100% (relative)"); + /// @notice Configure timelocks for VaultV2 functions + /// @dev MORPHO LISTING REQUIREMENT: Timelocks must be 3-7 days depending on function + /// - 7 days minimum: increaseTimelock, removeAdapter, abdicate + /// - 3 days minimum: addAdapter, increaseRelativeCap, setForceDeallocatePenalty, increaseAbsoluteCap + /// + /// IMPORTANT: increaseTimelock.selector must be configured LAST because once timelocked, + /// subsequent calls require waiting for the timelock to expire. + function configureVaultTimelocks(IVaultV2 vault) internal { + VaultTimelocks memory timelocks = getVaultTimelocks(); + // addAdapter + increaseVaultTimelock(vault, IVaultV2.addAdapter.selector, timelocks.addAdapter); + console.log("- Did set 'addAdapter' timelock to", timelocks.addAdapter, "seconds"); + // increaseAbsoluteCap + increaseVaultTimelock(vault, IVaultV2.increaseAbsoluteCap.selector, timelocks.increaseAbsoluteCap); + console.log("- Did set 'increaseAbsoluteCap' timelock to", timelocks.increaseAbsoluteCap, "seconds"); + // increaseRelativeCap + increaseVaultTimelock(vault, IVaultV2.increaseRelativeCap.selector, timelocks.increaseRelativeCap); + console.log("- Did set 'increaseRelativeCap' timelock to", timelocks.increaseRelativeCap, "seconds"); + // setForceDeallocatePenalty + increaseVaultTimelock(vault, IVaultV2.setForceDeallocatePenalty.selector, timelocks.setForceDeallocatePenalty); + console.log("- Did set 'setForceDeallocatePenalty' timelock to", timelocks.setForceDeallocatePenalty, "seconds"); + // abdicate + increaseVaultTimelock(vault, IVaultV2.abdicate.selector, timelocks.abdicate); + console.log("- Did set 'abdicate' timelock to", timelocks.abdicate, "seconds"); + // removeAdapter + increaseVaultTimelock(vault, IVaultV2.removeAdapter.selector, timelocks.removeAdapter); + console.log("- Did set 'removeAdapter' timelock to", timelocks.removeAdapter, "seconds"); + // increaseTimelock (MUST BE LAST!) + increaseVaultTimelock(vault, IVaultV2.increaseTimelock.selector, timelocks.increaseTimelock); + console.log("- Did set 'increaseTimelock' timelock to", timelocks.increaseTimelock, "seconds"); } - /** - * @notice Validate that MarketParams match expected values - * @param marketParams The market parameters to validate - * @param expectedAsset The expected loan token address (vault's underlying asset) - * @param expectedIrm The expected IRM address (adapter's adaptiveCurveIrm) - */ - function _validateMarketParams(MarketParams memory marketParams, address expectedAsset, address expectedIrm) + function increaseVaultTimelock(IVaultV2 vault, bytes4 selector, uint256 duration) internal { + vault.submit(abi.encodeCall(vault.increaseTimelock, (selector, duration))); + vault.increaseTimelock(selector, duration); + } + + /// @notice Configure timelocks for MorphoMarketV1AdapterV2 functions + /// @dev MORPHO LISTING REQUIREMENT: burnShares timelock must be >= 3 days + /// + /// The adapter has its own independent timelock system separate from the vault. + /// Key functions that need timelocks: + /// - burnShares: Prevents immediate share burning (protects against manipulation) + /// - setSkimRecipient: Controls who receives skimmed tokens + /// - abdicate: Permanently disables functions + /// + /// IMPORTANT: increaseTimelock.selector must be configured LAST because once timelocked, + /// subsequent calls require waiting for the timelock to expire. + function configureAdapterTimelocks(IMorphoMarketV1AdapterV2 adapter) internal { + AdapterTimelocks memory timelocks = getAdapterTimelocks(); + // abdicate + increaseAdapterTimelock(adapter, IMorphoMarketV1AdapterV2.abdicate.selector, timelocks.abdicate); + console.log("- Did set 'abdicate' timelock to", timelocks.abdicate, "seconds"); + // setSkimRecipient + increaseAdapterTimelock(adapter, IMorphoMarketV1AdapterV2.setSkimRecipient.selector, timelocks.setSkimRecipient); + console.log("- Did set 'setSkimRecipient' timelock to", timelocks.setSkimRecipient, "seconds"); + // burnShares + increaseAdapterTimelock(adapter, IMorphoMarketV1AdapterV2.burnShares.selector, timelocks.burnShares); + console.log("- Did set 'burnShares' timelock to", timelocks.burnShares, "seconds"); + // increaseTimelock (MUST BE LAST!) + increaseAdapterTimelock(adapter, IMorphoMarketV1AdapterV2.increaseTimelock.selector, timelocks.increaseTimelock); + console.log("- Did set 'increaseTimelock' timelock to", timelocks.increaseTimelock, "seconds"); + } + + function increaseAdapterTimelock( + IMorphoMarketV1AdapterV2 adapter, + bytes4 selector, + uint256 timelockDuration + ) internal { + adapter.submit(abi.encodeCall(adapter.increaseTimelock, (selector, timelockDuration))); + adapter.increaseTimelock(selector, timelockDuration); + } + + /// @notice Validate that MarketParams match expected values + function validateMarketParams( + MarketParams memory marketParams, + address expLoanToken, + address expCollateralToken, + address expIrm + ) internal pure { - require(marketParams.loanToken == expectedAsset, "Market loanToken does not match vault asset"); - require(marketParams.irm == expectedIrm, "Market IRM does not match adaptiveCurveIrm"); - require(marketParams.collateralToken != address(0), "Market not found (collateralToken is zero)"); + require(marketParams.loanToken == expLoanToken, "Market loanToken mismatch"); + require(marketParams.irm == expIrm, "Market IRM mismatch"); + require(marketParams.collateralToken == expCollateralToken, "Market collateralToken mismatch"); } - /** - * @notice Configure timelocks for VaultV2 functions - * @param vault The VaultV2 instance to configure - * @param timelockDuration The timelock duration in seconds - * @dev MORPHO LISTING REQUIREMENT: Timelocks must be 3-7 days depending on function - * - 7 days minimum: increaseTimelock, removeAdapter, abdicate - * - 3 days minimum: addAdapter, increaseRelativeCap, setForceDeallocatePenalty, increaseAbsoluteCap - * - * IMPORTANT: increaseTimelock.selector must be configured LAST because once timelocked, - * subsequent calls require waiting for the timelock to expire. - * - * This function configures all timelocked functions with the same duration for simplicity. - * For production deployments, you may want to configure different durations per function. - */ - function _configureVaultTimelocks(VaultV2 vault, uint256 timelockDuration) internal { - // Configure timelocks for key vault functions - // Order matters: increaseTimelock.selector MUST be last! - bytes4[] memory selectors = new bytes4[](7); - // 3-day minimum functions - selectors[0] = IVaultV2.addAdapter.selector; - selectors[1] = IVaultV2.increaseAbsoluteCap.selector; - selectors[2] = IVaultV2.increaseRelativeCap.selector; - selectors[3] = IVaultV2.setForceDeallocatePenalty.selector; - // 7-day minimum functions - selectors[4] = IVaultV2.abdicate.selector; - selectors[5] = IVaultV2.removeAdapter.selector; - selectors[6] = IVaultV2.increaseTimelock.selector; // MUST BE LAST! - - for (uint256 i = 0; i < selectors.length; i++) { - vault.submit(abi.encodeCall(vault.increaseTimelock, (selectors[i], timelockDuration))); - vault.increaseTimelock(selectors[i], timelockDuration); + function validateFinalState(VaultV2 vault, IMorphoMarketV1AdapterV2 adapter, Config memory config) internal view { + // Core params + require(stringsEqual(vault.name(), config.name), "Vault name mismatch"); + require(stringsEqual(vault.symbol(), config.symbol), "Vault symbol mismatch"); + require(vault.owner() == config.owner, "Vault owner mismatch"); + require(vault.curator() == config.curator, "Vault curator mismatch"); + console.log("- Vault core params valid"); + // Adapter-related params + require(vault.adapterRegistry() == config.adapterRegistry, "Vault adapterRegistry mismatch"); + require(vault.liquidityAdapter() == address(adapter), "Vault liquidityAdapter mismatch"); + require(bytes(vault.liquidityData()).length > 0, "Vault liquidityData is empty"); + console.log("- Vault adapter-related params valid"); + // Market-related params + bytes32 collateralTokenId = keccak256(abi.encode("collateralToken", config.collateralToken)); + require(vault.absoluteCap(collateralTokenId) == config.collateralTokenCap, "Vault collateralToken cap mismatch"); + require(vault.relativeCap(collateralTokenId) == WAD, "Vault collateralToken relative cap mismatch"); + bytes32 adapterId = keccak256(abi.encode("this", address(adapter))); + require(vault.absoluteCap(adapterId) == MAX_UINT128, "Vault adapter cap mismatch"); + require(vault.relativeCap(adapterId) == WAD, "Vault adapter relative cap mismatch"); + MarketParams memory marketParams = IMorpho(adapter.morpho()).idToMarketParams(Id.wrap(config.marketId)); + bytes32 marketParamsId = keccak256(abi.encode("this/marketParams", address(adapter), marketParams)); + require(vault.absoluteCap(marketParamsId) == config.marketCap, "Vault market cap mismatch"); + require(vault.relativeCap(marketParamsId) == WAD, "Vault market relative cap mismatch"); + console.log("- Vault market-related params valid"); + // Dead deposit + require(vault.balanceOf(DEAD_ADDRESS) > 0, "No dead deposit made"); + console.log("- Vault dead deposit made"); + // Abdications + require(vault.abdicated(IVaultV2.setAdapterRegistry.selector), "Vault adapterRegistry not abdicated"); + require(vault.abdicated(IVaultV2.setReceiveSharesGate.selector), "Vault receiveSharesGate not abdicated"); + require(vault.abdicated(IVaultV2.setSendSharesGate.selector), "Vault sendSharesGate not abdicated"); + require(vault.abdicated(IVaultV2.setReceiveAssetsGate.selector), "Vault receiveAssetsGate not abdicated"); + console.log("- Necessary vault abdications performed"); + // Vault timelocks + VaultTimelocks memory vaultTimelocks = getVaultTimelocks(); + require(vault.timelock(IVaultV2.addAdapter.selector) == vaultTimelocks.addAdapter, "Vault addAdapter timelock mismatch"); + require(vault.timelock(IVaultV2.increaseAbsoluteCap.selector) == vaultTimelocks.increaseAbsoluteCap, "Vault increaseAbsoluteCap timelock mismatch"); + require(vault.timelock(IVaultV2.increaseRelativeCap.selector) == vaultTimelocks.increaseRelativeCap, "Vault increaseRelativeCap timelock mismatch"); + require(vault.timelock(IVaultV2.setForceDeallocatePenalty.selector) == vaultTimelocks.setForceDeallocatePenalty, "Vault setForceDeallocatePenalty timelock mismatch"); + require(vault.timelock(IVaultV2.abdicate.selector) == vaultTimelocks.abdicate, "Vault abdication timelock mismatch"); + require(vault.timelock(IVaultV2.removeAdapter.selector) == vaultTimelocks.removeAdapter, "Vault removeAdapter timelock mismatch"); + require(vault.timelock(IVaultV2.increaseTimelock.selector) == vaultTimelocks.increaseTimelock, "Vault increaseTimelock timelock mismatch"); + console.log("- Vault timelocks configured correctly"); + // Adapter timelocks + AdapterTimelocks memory adapterTimelocks = getAdapterTimelocks(); + require(adapter.timelock(IMorphoMarketV1AdapterV2.abdicate.selector) == adapterTimelocks.abdicate, "Adapter abdication timelock mismatch"); + require(adapter.timelock(IMorphoMarketV1AdapterV2.setSkimRecipient.selector) == adapterTimelocks.setSkimRecipient, "Adapter setSkimRecipient timelock mismatch"); + require(adapter.timelock(IMorphoMarketV1AdapterV2.burnShares.selector) == adapterTimelocks.burnShares, "Adapter burnShares timelock mismatch"); + require(adapter.timelock(IMorphoMarketV1AdapterV2.increaseTimelock.selector) == adapterTimelocks.increaseTimelock, "Adapter increaseTimelock timelock mismatch"); + console.log("- Adapter timelocks configured correctly"); + // Fees + require(vault.performanceFee() == config.performanceFee, "Vault performanceFee mismatch"); + require(vault.managementFee() == config.managementFee, "Vault managementFee mismatch"); + require(vault.performanceFeeRecipient() == config.feeRecipient, "Vault performanceFeeRecipient mismatch"); + require(vault.managementFeeRecipient() == config.feeRecipient, "Vault managementFeeRecipient mismatch"); + console.log("- Vault fees settings valid"); + // Permissions + require(vault.owner() == config.owner, "Vault owner mismatch"); + require(vault.curator() == config.curator, "Vault curator mismatch"); + require(vault.isAllocator(config.allocator), "Vault allocator not set"); + if (config.sentinel != address(0)) { + require(vault.isSentinel(config.sentinel), "Vault sentinel not set"); } - - console.log("Phase 9: Vault timelocks configured:", timelockDuration, "seconds"); + console.log("- Vault permissions configured correctly"); + address deployer = resolveDeployer(); + if (deployer != config.allocator) { + require(!vault.isAllocator(deployer), "Deployer didn't renounce allocator role"); + } + console.log("- Temporary permissions renounced by deployer"); } - /** - * @notice Configure timelocks for MorphoMarketV1AdapterV2 functions - * @param adapter The adapter instance to configure - * @param timelockDuration The timelock duration in seconds - * @dev MORPHO LISTING REQUIREMENT: burnShares timelock must be >= 3 days - * - * The adapter has its own independent timelock system separate from the vault. - * Key functions that need timelocks: - * - burnShares: Prevents immediate share burning (protects against manipulation) - * - setSkimRecipient: Controls who receives skimmed tokens - * - abdicate: Permanently disables functions - * - * IMPORTANT: increaseTimelock.selector must be configured LAST because once timelocked, - * subsequent calls require waiting for the timelock to expire. - */ - function _configureAdapterTimelocks(IMorphoMarketV1AdapterV2 adapter, uint256 timelockDuration) internal { - // Configure timelocks for key adapter functions - // Order matters: increaseTimelock.selector MUST be last! - bytes4[] memory selectors = new bytes4[](4); - selectors[0] = IMorphoMarketV1AdapterV2.abdicate.selector; - selectors[1] = IMorphoMarketV1AdapterV2.setSkimRecipient.selector; - selectors[2] = IMorphoMarketV1AdapterV2.burnShares.selector; - selectors[3] = IMorphoMarketV1AdapterV2.increaseTimelock.selector; // MUST BE LAST! - - for (uint256 i = 0; i < selectors.length; i++) { - adapter.submit(abi.encodeCall(adapter.increaseTimelock, (selectors[i], timelockDuration))); - adapter.increaseTimelock(selectors[i], timelockDuration); - } + function resolveDeployer() internal view returns (address) { + return vm.envAddress("DEPLOYER"); + } - console.log("Phase 10: Adapter timelocks configured:", timelockDuration, "seconds"); + function stringsEqual(string memory a, string memory b) internal pure returns (bool) { + return keccak256(bytes(a)) == keccak256(bytes(b)); } }