VaultV2Helper

Description:

Multi-signature wallet contract requiring multiple confirmations for transaction execution.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "src/periphery/VaultV2Helper.sol": {
      "content": "// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (c) 2025 Steakhouse Financial
pragma solidity ^0.8.28;

import {MorphoVaultV1AdapterFactory} from "@vault-v2/src/adapters/MorphoVaultV1AdapterFactory.sol";
import {IVaultV2, IERC20} from "@vault-v2/src/interfaces/IVaultV2.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@vault-v2/src/libraries/ConstantsLib.sol";
import {VaultV2} from "@vault-v2/src/VaultV2.sol";
import {VaultV2Factory} from "@vault-v2/src/VaultV2Factory.sol";
import {Revoker} from "./Revoker.sol";
import {VaultV2Lib} from "./VaultV2Lib.sol";
import "../interfaces/Aragon.sol";

/**
 * @title VaultV2Helper
 * @notice Helper contract for VaultV2 configuration
 * @dev This contract handles vault configuration, adapter management, and other utility functions.
 *
 * Chain Support:
 * - Ethereum Mainnet (chainId: 1)
 * - Base (chainId: 8453)
 * - Addresses configured per chain in constructor
 */
contract VaultV2Helper {
    using VaultV2Lib for IVaultV2;

    /* ======== EVENTS ======== */
    event VaultCreated(address indexed vault, address indexed asset, string name, string symbol);
    event RevokerDeployed(address indexed vault, address indexed sentinel, address indexed revoker);
    event VaultConfigured(address indexed vault);

    /* ======== VARIABLES ======== */
    address public owner;
    address public curator;
    address[] public allocators;

    address public morphoRegistry;
    VaultV2Factory public vaultV2Factory;
    MorphoVaultV1AdapterFactory public mv1AdapterFactory;
    IDAOFactory public daoFactory;
    address public lockToVoteRepo;
    address public multisigRepo;


    /* ======== CONSTRUCTOR ======== */

    constructor() {
        // Base configuration (chain ID 8453) or local testing (31337)
        if (block.chainid == 8453 || block.chainid == 31337) {
            morphoRegistry = 0x5C2531Cbd2cf112Cf687da3Cd536708aDd7DB10a;
            owner = 0x0A0e559bc3b0950a7e448F0d4894db195b9cf8DD;
            curator = 0x827e86072B06674a077f592A531dcE4590aDeCdB;
            allocators.push(0x0000aeB716a0DF7A9A1AAd119b772644Bc089dA8);
            allocators.push(0xfeed46c11F57B7126a773EeC6ae9cA7aE1C03C9a);
            vaultV2Factory = VaultV2Factory(0x4501125508079A99ebBebCE205DeC9593C2b5857);
            mv1AdapterFactory = MorphoVaultV1AdapterFactory(0xF42D9c36b34c9c2CF3Bc30eD2a52a90eEB604642);
            daoFactory = IDAOFactory(0xcc602EA573a42eBeC290f33F49D4A87177ebB8d2);
            lockToVoteRepo = 0x05ECA5ab78493Bf812052B0211a206BCBA03471B;
            multisigRepo = 0xcDC4b0BC63AEfFf3a7826A19D101406C6322A585;
        }
        // Ethereum Mainnet configuration (chain ID 1)
        else if (block.chainid == 1) {
            morphoRegistry = address(0); // TODO: Set mainnet Morpho registry
            owner = 0x0A0e559bc3b0950a7e448F0d4894db195b9cf8DD;
            curator = 0x827e86072B06674a077f592A531dcE4590aDeCdB;
            allocators.push(0x0000aeB716a0DF7A9A1AAd119b772644Bc089dA8);
            allocators.push(0xfeed46c11F57B7126a773EeC6ae9cA7aE1C03C9a);
            vaultV2Factory = VaultV2Factory(address(0xA1D94F746dEfa1928926b84fB2596c06926C0405));
            mv1AdapterFactory = MorphoVaultV1AdapterFactory(address(0xD1B8E2dee25c2b89DCD2f98448a7ce87d6F63394));
            daoFactory = IDAOFactory(address(0x246503df057A9a85E0144b6867a828c99676128B));
            lockToVoteRepo = address(0x0f4FBD2951Db08B45dE16e7519699159aE1b4bb7);
            multisigRepo = address(0x8c278e37D0817210E18A7958524b7D0a1fAA6F7b);
        } else {
            revert("Unsupported chain");
        }
    }

    /* ======== VAULT CREATION TEMPLATES ======== */
    function createV1WrapperCompliant(
        address asset,
        bytes32 salt,
        string calldata name,
        string calldata symbol,
        address v1Vault) external returns (IVaultV2 vault) {
        // Create vault via factory
        vault = create(asset, salt, name, symbol);

        addVaultV1(vault, v1Vault, true, 1_000_000_000 * 10 ** IERC20Metadata(v1Vault).decimals(), 1 ether);
        conformMorphoRegistry(vault);

        address guardian = createGuardian(vault);
        
        finalize(vault, 3 days, guardian);
    }


    /* ======== VAULT CREATION ======== */




    /**
     * @notice Create a new VaultV2 instance
     * @dev Helper becomes initial owner and curator, which should be transferred later
     * @param asset The underlying asset (e.g., USDC)
     * @param salt Salt for deterministic deployment
     * @param name Vault name
     * @param symbol Vault symbol
     * @return vault The created vault
     */
    function create(
        address asset,
        bytes32 salt,
        string calldata name,
        string calldata symbol
    ) public returns (IVaultV2 vault) {
        // Create vault via factory
        vault = VaultV2(vaultV2Factory.createVaultV2(address(this), asset, salt));

        // Set helper as curator
        vault.setCurator(address(this));

        // Add helper as allocator (needed for initial config)
        vault.submit(abi.encodeWithSelector(vault.setIsAllocator.selector, address(this), true));
        vault.setIsAllocator(address(this), true);

        // Add configured allocators
        for (uint i = 0; i < allocators.length; i++) {
            vault.submit(abi.encodeWithSelector(vault.setIsAllocator.selector, address(allocators[i]), true));
            vault.setIsAllocator(address(allocators[i]), true);
        }

        // Set vault metadata
        vault.setName(name);
        vault.setSymbol(symbol);

        // Set maximum rate
        vault.setMaxRate(MAX_MAX_RATE);

        emit VaultCreated(address(vault), asset, name, symbol);
    }

    /**
     * @notice Add a Morpho MetaMorpho vault as an adapter
     * @dev Creates adapter, adds it to vault, and sets caps
     * @param vault The VaultV2 to configure
     * @param vaultV1 The MetaMorpho vault address
     * @param liquidity Whether to set as liquidity adapter
     * @param capAbs Absolute cap in asset units
     * @param capRel Relative cap as a fraction (1 ether = 100%)
     */
    function addVaultV1(
        IVaultV2 vault,
        address vaultV1,
        bool liquidity,
        uint256 capAbs,
        uint256 capRel
    ) public {
        // Create adapter
        address adapterMV1 = mv1AdapterFactory.createMorphoVaultV1Adapter(address(vault), vaultV1);
        bytes memory idData = abi.encode("this", adapterMV1);

        // Add adapter and set caps using submit/accept pattern
        vault.submit(abi.encodeWithSelector(vault.addAdapter.selector, adapterMV1));
        vault.addAdapter(adapterMV1);

        vault.submit(abi.encodeWithSelector(vault.increaseAbsoluteCap.selector, idData, capAbs));
        vault.increaseAbsoluteCap(idData, capAbs);

        vault.submit(abi.encodeWithSelector(vault.increaseRelativeCap.selector, idData, capRel));
        vault.increaseRelativeCap(idData, capRel);

        // Optionally set as liquidity adapter
        if (liquidity) {
            vault.setLiquidityAdapterAndData(adapterMV1, "");
        }
    }

    /**
     * @notice Configure vault to use Morpho's adapter registry
     * @dev Sets registry and abdicates permission to change it
     * @param vault The vault to configure
     */
    function conformMorphoRegistry(IVaultV2 vault) public {
        // Set the correct adapter registry and abdicate
        vault.submit(abi.encodeWithSelector(vault.setAdapterRegistry.selector, morphoRegistry));
        vault.setAdapterRegistry(morphoRegistry);
        vault.submit(abi.encodeWithSelector(vault.abdicate.selector, vault.setAdapterRegistry.selector));
        vault.abdicate(vault.setAdapterRegistry.selector);
    }

    /**
     * @notice Create the Guardian DAO using LockToVote plugin and set the sentinel
     */
    function createGuardian(IVaultV2 vault) public returns (address guardian) {
        guardian = createGuardianDAO(vault);
        setRevoker(vault, guardian);
    }

    /**
     * @notice Perform timelocks and ACLs setup, then transfer ownership to Owner DAO
     */
    function finalize(IVaultV2 vault, uint256 timelocks, address guardian) public returns (address) {
        
        removeHelperAsAllocator(vault);
        setVaultTimelocks(vault, timelocks);

        setProductionCurator(vault);

        if(guardian != address(0)) {
            address ownerDAO = createOwnerDAO(guardian, owner, "");
            transferOwnership(vault, ownerDAO);
            return ownerDAO;
        }
        else {
            transferOwnership(vault, owner);
            return owner;
        }
    }

    /* ======== VAULT FINALIZATION ======== */

    /**
     * @notice Set all required timelocks on the vault
     * @dev Sets timelocks according to Morpho requirements (7 days for critical functions)
     * @param vault The vault to configure
     * @param capsDays Timelock for caps changes (typically 3 days)
     */
    function setVaultTimelocks(IVaultV2 vault, uint256 capsDays) public {
        // Morpho requires these to be 7 days
        vault.submit(abi.encodeWithSelector(vault.increaseTimelock.selector, vault.setReceiveAssetsGate.selector, 7 days));
        vault.increaseTimelock(vault.setReceiveAssetsGate.selector, 7 days);

        vault.submit(abi.encodeWithSelector(vault.increaseTimelock.selector, vault.setReceiveSharesGate.selector, 7 days));
        vault.increaseTimelock(vault.setReceiveSharesGate.selector, 7 days);

        vault.submit(abi.encodeWithSelector(vault.increaseTimelock.selector, vault.setSendSharesGate.selector, 7 days));
        vault.increaseTimelock(vault.setSendSharesGate.selector, 7 days);

        vault.submit(abi.encodeWithSelector(vault.increaseTimelock.selector, vault.setSendAssetsGate.selector, 7 days));
        vault.increaseTimelock(vault.setSendAssetsGate.selector, 7 days);

        vault.submit(abi.encodeWithSelector(vault.increaseTimelock.selector, vault.abdicate.selector, 7 days));
        vault.increaseTimelock(vault.abdicate.selector, 7 days);

        vault.submit(abi.encodeWithSelector(vault.increaseTimelock.selector, vault.setAdapterRegistry.selector, 7 days));
        vault.increaseTimelock(vault.setAdapterRegistry.selector, 7 days);

        vault.submit(abi.encodeWithSelector(vault.increaseTimelock.selector, vault.removeAdapter.selector, 7 days));
        vault.increaseTimelock(vault.removeAdapter.selector, 7 days);

        vault.submit(abi.encodeWithSelector(vault.increaseTimelock.selector, vault.setForceDeallocatePenalty.selector, 7 days));
        vault.increaseTimelock(vault.setForceDeallocatePenalty.selector, 7 days);

        // These can be 3 days for Morpho UI acceptance
        vault.submit(abi.encodeWithSelector(vault.increaseTimelock.selector, vault.addAdapter.selector, capsDays));
        vault.increaseTimelock(vault.addAdapter.selector, capsDays);

        vault.submit(abi.encodeWithSelector(vault.increaseTimelock.selector, vault.increaseRelativeCap.selector, capsDays));
        vault.increaseTimelock(vault.increaseRelativeCap.selector, capsDays);

        vault.submit(abi.encodeWithSelector(vault.increaseTimelock.selector, vault.increaseAbsoluteCap.selector, capsDays));
        vault.increaseTimelock(vault.increaseAbsoluteCap.selector, capsDays);

        // This must be last - 7 days minimum
        vault.submit(abi.encodeWithSelector(vault.increaseTimelock.selector, vault.increaseTimelock.selector, 7 days));
        vault.increaseTimelock(vault.increaseTimelock.selector, 7 days);
    }

    /**
     * @notice Set production curator on the vault
     * @dev Should be called after vault is configured but before ownership transfer
     * @param vault The vault to configure
     */
    function setProductionCurator(IVaultV2 vault) public {
        vault.setCurator(curator);
    }

    /**
     * @notice Remove helper from vault allocator list
     * @dev Should be called before transferring curator/ownership
     * @param vault The vault to clean up
     */
    function removeHelperAsAllocator(IVaultV2 vault) public {
        vault.submit(abi.encodeWithSelector(vault.setIsAllocator.selector, address(this), false));
        vault.setIsAllocator(address(this), false);
    }

    /* ======== GUARDIAN SETUP ======== */

    /**
     * @notice Deploy Revoker and set as vault sentinel
     * @dev Revoker connects guardian DAO to vault for emergency actions
     * @param vault The vault to configure
     * @param guardian Address of Guardian DAO (LockToVote)
     * @return revoker The deployed Revoker contract
     */
    function setRevoker(IVaultV2 vault, address guardian) public returns (address revoker) {
        // Deploy Revoker
        Revoker revokerContract = new Revoker(vault, guardian);
        revoker = address(revokerContract);

        // Set as sentinel
        vault.setIsSentinel(revoker, true);

        emit RevokerDeployed(address(vault), guardian, revoker);
    }

    /**
     * @notice create a Guardian DAO using LockToVote plugin using the vault shares
     */
    function createGuardianDAO(IVaultV2 vault) public returns (address) {
        DAOSettings memory daoSettings = DAOSettings({
            trustedForwarder: address(0),
            daoURI: "",
            subdomain: "",
            metadata: "ipfs://QmTke6dGx54zCEqiqok8W3YViBqz1LTt7yX7zAABvRVtvF"
        });

        PluginSettings[] memory pluginSettings = new PluginSettings[](0);

        (address dao, InstalledPlugin[] memory installedPlugins) = IDAOFactory(daoFactory).createDao(
            daoSettings,
            pluginSettings
        );

        address plugin = addLockedVotePlugin(IDAO(dao), address(vault));

        // Create the action array
        IDAO.Action[] memory actions = new IDAO.Action[](3);

        bytes32 ROOT = keccak256("ROOT_PERMISSION");
        bytes32 UPGRADE = keccak256("UPGRADE_DAO_PERMISSION");
        bytes32 UPDATE_VOTING = keccak256("UPDATE_VOTING_SETTINGS_PERMISSION");

        // 1. Revoke UPDATE_VOTING_SETTINGS_PERMISSION from plugin
        actions[0] = IDAO.Action({
            to: dao,
            value: 0,
            data: abi.encodeWithSelector(IDAO.revoke.selector, plugin, dao, UPDATE_VOTING)
        });

        // 2. Revoke UPGRADE_DAO_PERMISSION from DAO
        actions[1] = IDAO.Action({
            to: dao,
            value: 0,
            data: abi.encodeWithSelector(IDAO.revoke.selector, dao, dao, UPGRADE)
        });

        // 3. Revoke ROOT_PERMISSION from DAO (makes it fully immutable)
        actions[2] = IDAO.Action({
            to: dao,
            value: 0,
            data: abi.encodeWithSelector(IDAO.revoke.selector, dao, dao, ROOT)
        });

        // Execute
        IDAO(dao).execute({_callId: "", _actions: actions, _allowFailureMap: 0});

        return dao;
    }

    function createOwnerDAO(
        address guardian,
        address owner,
        string memory metadataURI
    ) public returns (address) {

        DAOSettings memory daoSettings = DAOSettings({
            trustedForwarder: address(0),
            daoURI: "",
            subdomain: "",
            metadata: "ipfs://QmP7dhYX2HdVPQbhcu6a1oLjsWoqmwtWB5Bwk7Gajvehrb"
        });

        PluginSettings[] memory pluginSettings = new PluginSettings[](1);
        pluginSettings[0] = PluginSettings({
            pluginSetupRef: PluginSetupRef({
                versionTag: PluginSettingsTag({release: 1, build: 3}),
                pluginSetupRepo: multisigRepo
            }),
            data: _getMultisigData(guardian, owner)
        });

        (address dao, InstalledPlugin[] memory installedPlugins) = IDAOFactory(daoFactory).createDao(
            daoSettings,
            pluginSettings
        );

        return dao;
    }

    function _revoke(address dao, address where, address who, bytes32 permission) internal {
        (bool ok, ) = dao.call(
            abi.encodeWithSignature("revoke(address,address,bytes32)", where, who, permission)
        );
        require(ok, "Revoke failed");
    }
    /* ======== OWNERSHIP TRANSFER ======== */

    /**
     * @notice Transfer vault ownership to Owner DAO
     * @dev Final step in vault setup - transfers control to governance
     * @param vault The vault to transfer
     * @param newOwner Address of Owner DAO (2/2 multisig)
     */
    function transferOwnership(IVaultV2 vault, address newOwner) public {
        require(newOwner != address(0), "Invalid owner address");
        vault.setOwner(newOwner);
        emit VaultConfigured(address(vault));
    }

    /* ======== VIEW FUNCTIONS ======== */

    /**
     * @notice Get configured addresses for this chain
     */
    function getConfig() external view returns (
        address _owner,
        address _curator,
        address[] memory _allocators,
        address _morphoRegistry,
        address _vaultV2Factory,
        address _mv1AdapterFactory
    ) {
        return (owner, curator, allocators, morphoRegistry, address(vaultV2Factory), address(mv1AdapterFactory));
    }


    /* ======== INTERNAL FUNCTIONS ======== */

    function _getLockToVoteData(address votingToken) internal pure returns (bytes memory) {
        // Create the exact struct that prepareInstallation expects
        InstallationParameters memory params = InstallationParameters({
            token: votingToken,
            votingSettings: VotingSettings({
                votingMode: 0,
                supportThreshold: 500000, // 50%
                minParticipation: 1, // 0.0001%
                minApprovalRatio: 0,
                proposalDuration: 1 days,
                minProposerVotingPower: 1
            }),
            pluginMetadata: "",
            createProposalCaller: address(type(uint160).max),
            executeCaller: address(type(uint160).max),
            targetConfig: TargetConfig({
                target: address(0),
                operation: 0
            })
        });

        // Now encode it properly - Solidity will handle the nested struct encoding correctly
        return abi.encode(params);
    }

    function _getMultisigData(address guardian, address owner) internal pure returns (bytes memory) {
        address[] memory members = new address[](2);
        members[0] = guardian;
        members[1] = owner;

        return abi.encode(
            members,
            false,      // onlyListed
            uint16(2),  // minApprovals (2/2)
            address(0), // target address
            0,          // operation type
            "ipfs://QmeCvj5xo55cHHqmRQhKzTMX6AYACeYG8z2DgD6g16tJ2x" // metadata
        );
    }

    function addLockedVotePlugin(IDAO dao, address vault) internal returns (address) {
        address daoAddress = address(dao);
        
        PluginSettings memory pluginSettings = PluginSettings({
            pluginSetupRef: PluginSetupRef({
                versionTag: PluginSettingsTag({release: 1, build: 1}),
                pluginSetupRepo: lockToVoteRepo
            }),
            data: _getLockToVoteData(vault)
        });
     
        IPluginSetupProcessor pluginSetupProcessor = IPluginSetupProcessor(address(daoFactory.pluginSetupProcessor()));

        // Create the action array
        IDAO.Action[] memory actions = new IDAO.Action[](2);

        // Grant Temporarily `ROOT_PERMISSION` to `pluginSetupProcessor`.
        actions[0] = IDAO.Action({
            to: daoAddress,
            value: 0,
            data: abi.encodeWithSelector(IDAO.grant.selector, daoAddress, address(pluginSetupProcessor), dao.ROOT_PERMISSION_ID())
        });

        // Grant Temporarily `APPLY_INSTALLATION_PERMISSION` on `pluginSetupProcessor` to this `DAOFactory`.
        actions[1] = IDAO.Action({
            to: daoAddress,
            value: 0,
            data: abi.encodeWithSelector(IDAO.grant.selector, 
                address(pluginSetupProcessor),
                address(this), 
                keccak256("APPLY_INSTALLATION_PERMISSION"))
        });

        // Execute
        IDAO(dao).execute({_callId: "", _actions: actions, _allowFailureMap: 0});

        (
            address plugin,
            IPluginSetup.PreparedSetupData memory preparedSetupData
        ) = pluginSetupProcessor.prepareInstallation(
                daoAddress,
                IPluginSetupProcessor.PrepareInstallationParams(
                    pluginSettings.pluginSetupRef,
                    pluginSettings.data
            )
        );

        // Apply plugin.
        pluginSetupProcessor.applyInstallation(
            daoAddress,
            IPluginSetupProcessor.ApplyInstallationParams(
                pluginSettings.pluginSetupRef,
                plugin,
                preparedSetupData.permissions,
                    keccak256(abi.encode((preparedSetupData.helpers)))
            )
        );
        
        // Revoke Temporarily `ROOT_PERMISSION` to `pluginSetupProcessor`.
        actions[0] = IDAO.Action({
            to: daoAddress,
            value: 0,
            data: abi.encodeWithSelector(IDAO.revoke.selector, daoAddress, address(pluginSetupProcessor), dao.ROOT_PERMISSION_ID())
        });

        // Revoke Temporarily `APPLY_INSTALLATION_PERMISSION` on `pluginSetupProcessor` to this `DAOFactory`.
        actions[1] = IDAO.Action({
            to: daoAddress,
            value: 0,
            data: abi.encodeWithSelector(IDAO.revoke.selector, 
                address(pluginSetupProcessor),
                address(this), 
                keccak256("APPLY_INSTALLATION_PERMISSION"))
        });

        // Execute
        IDAO(dao).execute({_callId: "", _actions: actions, _allowFailureMap: 0});

        return plugin;   
    }

}
"
    },
    "lib/vault-v2/src/adapters/MorphoVaultV1AdapterFactory.sol": {
      "content": "// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (c) 2025 Morpho Association
pragma solidity 0.8.28;

import {MorphoVaultV1Adapter} from "./MorphoVaultV1Adapter.sol";
import {IMorphoVaultV1AdapterFactory} from "./interfaces/IMorphoVaultV1AdapterFactory.sol";

contract MorphoVaultV1AdapterFactory is IMorphoVaultV1AdapterFactory {
    /* STORAGE */

    mapping(address parentVault => mapping(address morphoVaultV1 => address)) public morphoVaultV1Adapter;
    mapping(address account => bool) public isMorphoVaultV1Adapter;

    /* FUNCTIONS */

    /// @dev Returns the address of the deployed MorphoVaultV1Adapter.
    function createMorphoVaultV1Adapter(address parentVault, address morphoVaultV1) external returns (address) {
        address _morphoVaultV1Adapter = address(new MorphoVaultV1Adapter{salt: bytes32(0)}(parentVault, morphoVaultV1));
        morphoVaultV1Adapter[parentVault][morphoVaultV1] = _morphoVaultV1Adapter;
        isMorphoVaultV1Adapter[_morphoVaultV1Adapter] = true;
        emit CreateMorphoVaultV1Adapter(parentVault, morphoVaultV1, _morphoVaultV1Adapter);
        return _morphoVaultV1Adapter;
    }
}
"
    },
    "lib/vault-v2/src/interfaces/IVaultV2.sol": {
      "content": "// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (c) 2025 Morpho Association
pragma solidity >=0.5.0;

import {IERC20} from "./IERC20.sol";
import {IERC4626} from "./IERC4626.sol";
import {IERC2612} from "./IERC2612.sol";

struct Caps {
    uint256 allocation;
    uint128 absoluteCap;
    uint128 relativeCap;
}

interface IVaultV2 is IERC4626, IERC2612 {
    // State variables
    function virtualShares() external view returns (uint256);
    function owner() external view returns (address);
    function curator() external view returns (address);
    function receiveSharesGate() external view returns (address);
    function sendSharesGate() external view returns (address);
    function receiveAssetsGate() external view returns (address);
    function sendAssetsGate() external view returns (address);
    function adapterRegistry() external view returns (address);
    function isSentinel(address account) external view returns (bool);
    function isAllocator(address account) external view returns (bool);
    function firstTotalAssets() external view returns (uint256);
    function _totalAssets() external view returns (uint128);
    function lastUpdate() external view returns (uint64);
    function maxRate() external view returns (uint64);
    function adapters(uint256 index) external view returns (address);
    function adaptersLength() external view returns (uint256);
    function isAdapter(address account) external view returns (bool);
    function allocation(bytes32 id) external view returns (uint256);
    function absoluteCap(bytes32 id) external view returns (uint256);
    function relativeCap(bytes32 id) external view returns (uint256);
    function forceDeallocatePenalty(address adapter) external view returns (uint256);
    function liquidityAdapter() external view returns (address);
    function liquidityData() external view returns (bytes memory);
    function timelock(bytes4 selector) external view returns (uint256);
    function abdicated(bytes4 selector) external view returns (bool);
    function executableAt(bytes memory data) external view returns (uint256);
    function performanceFee() external view returns (uint96);
    function performanceFeeRecipient() external view returns (address);
    function managementFee() external view returns (uint96);
    function managementFeeRecipient() external view returns (address);

    // Gating
    function canSendShares(address account) external view returns (bool);
    function canReceiveShares(address account) external view returns (bool);
    function canSendAssets(address account) external view returns (bool);
    function canReceiveAssets(address account) external view returns (bool);

    // Multicall
    function multicall(bytes[] memory data) external;

    // Owner functions
    function setOwner(address newOwner) external;
    function setCurator(address newCurator) external;
    function setIsSentinel(address account, bool isSentinel) external;
    function setName(string memory newName) external;
    function setSymbol(string memory newSymbol) external;

    // Timelocks for curator functions
    function submit(bytes memory data) external;
    function revoke(bytes memory data) external;

    // Curator functions
    function setIsAllocator(address account, bool newIsAllocator) external;
    function setReceiveSharesGate(address newReceiveSharesGate) external;
    function setSendSharesGate(address newSendSharesGate) external;
    function setReceiveAssetsGate(address newReceiveAssetsGate) external;
    function setSendAssetsGate(address newSendAssetsGate) external;
    function setAdapterRegistry(address newAdapterRegistry) external;
    function addAdapter(address account) external;
    function removeAdapter(address account) external;
    function increaseTimelock(bytes4 selector, uint256 newDuration) external;
    function decreaseTimelock(bytes4 selector, uint256 newDuration) external;
    function abdicate(bytes4 selector) external;
    function setPerformanceFee(uint256 newPerformanceFee) external;
    function setManagementFee(uint256 newManagementFee) external;
    function setPerformanceFeeRecipient(address newPerformanceFeeRecipient) external;
    function setManagementFeeRecipient(address newManagementFeeRecipient) external;
    function increaseAbsoluteCap(bytes memory idData, uint256 newAbsoluteCap) external;
    function decreaseAbsoluteCap(bytes memory idData, uint256 newAbsoluteCap) external;
    function increaseRelativeCap(bytes memory idData, uint256 newRelativeCap) external;
    function decreaseRelativeCap(bytes memory idData, uint256 newRelativeCap) external;
    function setMaxRate(uint256 newMaxRate) external;
    function setForceDeallocatePenalty(address adapter, uint256 newForceDeallocatePenalty) external;

    // Allocator functions
    function allocate(address adapter, bytes memory data, uint256 assets) external;
    function deallocate(address adapter, bytes memory data, uint256 assets) external;
    function setLiquidityAdapterAndData(address newLiquidityAdapter, bytes memory newLiquidityData) external;

    // Exchange rate
    function accrueInterest() external;
    function accrueInterestView()
        external
        view
        returns (uint256 newTotalAssets, uint256 performanceFeeShares, uint256 managementFeeShares);

    // Force deallocate
    function forceDeallocate(address adapter, bytes memory data, uint256 assets, address onBehalf)
        external
        returns (uint256 penaltyShares);
}
"
    },
    "lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/extensions/IERC20Metadata.sol)

pragma solidity >=0.6.2;

import {IERC20} from "../IERC20.sol";

/**
 * @dev Interface for the optional metadata functions from the ERC-20 standard.
 */
interface IERC20Metadata is IERC20 {
    /**
     * @dev Returns the name of the token.
     */
    function name() external view returns (string memory);

    /**
     * @dev Returns the symbol of the token.
     */
    function symbol() external view returns (string memory);

    /**
     * @dev Returns the decimals places of the token.
     */
    function decimals() external view returns (uint8);
}
"
    },
    "lib/vault-v2/src/libraries/ConstantsLib.sol": {
      "content": "// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (c) 2025 Morpho Association
pragma solidity ^0.8.0;

uint256 constant WAD = 1e18;
bytes32 constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(uint256 chainId,address verifyingContract)");
bytes32 constant PERMIT_TYPEHASH =
    keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
uint256 constant MAX_MAX_RATE = 200e16 / uint256(365 days); // 200% APR
uint256 constant MAX_PERFORMANCE_FEE = 0.5e18; // 50%
uint256 constant MAX_MANAGEMENT_FEE = 0.05e18 / uint256(365 days); // 5%
uint256 constant MAX_FORCE_DEALLOCATE_PENALTY = 0.02e18; // 2%
"
    },
    "lib/vault-v2/src/VaultV2.sol": {
      "content": "// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (c) 2025 Morpho Association
pragma solidity 0.8.28;

import {IVaultV2, IERC20, Caps} from "./interfaces/IVaultV2.sol";
import {IAdapter} from "./interfaces/IAdapter.sol";
import {IAdapterRegistry} from "./interfaces/IAdapterRegistry.sol";

import {ErrorsLib} from "./libraries/ErrorsLib.sol";
import {EventsLib} from "./libraries/EventsLib.sol";
import "./libraries/ConstantsLib.sol";
import {MathLib} from "./libraries/MathLib.sol";
import {SafeERC20Lib} from "./libraries/SafeERC20Lib.sol";
import {IReceiveSharesGate, ISendSharesGate, IReceiveAssetsGate, ISendAssetsGate} from "./interfaces/IGate.sol";

/// ERC4626
/// @dev The vault is compliant with ERC-4626 and with ERC-2612 (permit extension). Though the vault has a
/// non-conventional behaviour on max functions: they always return zero.
/// @dev totalSupply is not updated to include shares minted to fee recipients. One can call accrueInterestView to
/// compute the updated totalSupply.
///
/// TOTAL ASSETS
/// @dev Adapters are responsible for reporting to the vault how much their investments are worth at any time, so that
/// the vault can accrue interest or realize losses.
/// @dev _totalAssets stores the last recorded total assets. Use totalAssets() for the updated total assets.
/// @dev Upon interest accrual, the vault loops through adapters' realAssets(). If there are too many adapters and/or
/// they consume too much gas on realAssets(), it could cause issues such as expensive interactions, even DOS.
///
/// LOSS REALIZATION
/// @dev Loss realization occurs in accrueInterest and decreases the total assets, causing shares to lose value.
/// @dev Vault shares should not be loanable to prevent shares shorting on loss realization. Shares can be flashloanable
/// because flashloan-based shorting is prevented as interests and losses are only accounted once per transaction.
///
/// SHARE PRICE
/// @dev The share price can go down if the vault incurs some losses. Users might want to perform slippage checks upon
/// withdraw/redeem via an other contract.
/// @dev Interest/loss are accounted only once per transaction (at the first interaction with the vault).
/// @dev Donations increase the share price but not faster than the maxRate.
/// @dev The vault has 1 virtual asset and a decimal offset of max(0, 18 - assetDecimals). In order to protect against
/// inflation attacks, the vault might need to be seeded with an initial deposit. See
/// https://docs.openzeppelin.com/contracts/5.x/erc4626#inflation-attack
/// @dev Donations and forceDeallocate penalties increase the rate, which can attract opportunistic depositors which
/// will dilute interest. This fact can be mitigated by reducing the maxRate.
///
/// CAPS
/// @dev Ids have an asset allocation, and can be absolutely capped and/or relatively capped.
/// @dev The allocation is not always up to date, because interest and losses are accounted only when (de)allocating in
/// the corresponding markets.
/// @dev The caps are checked on allocate (where allocations can increase) for the ids returned by the adapter.
/// @dev Relative caps are "soft" in the sense that they are not checked on exit.
/// @dev Caps can be exceeded because of interest.
/// @dev The relative cap is relative to firstTotalAssets, not realAssets.
/// @dev The relative cap unit is WAD.
/// @dev To track allocations using events, use the Allocate and Deallocate events only.
///
/// FIRST TOTAL ASSETS
/// @dev The variable firstTotalAssets tracks the total assets after the first interest accrual of the transaction.
/// @dev Used to implement a mechanism that prevents bypassing relative caps with flashloans. This mechanism makes the
/// caps conservative and can generate false positives, notably for big deposits that go through the liquidity adapter.
/// @dev Also used to accrue interest only once per transaction (see the "share price" section).
/// @dev Relative caps can still be manipulated by allocators (with short-term deposits), but it requires capital.
/// @dev The behavior of firstTotalAssets is different when the vault has totalAssets=0, but it does not matter
/// internally because in this case there are no investments to cap.
///
/// ADAPTERS
/// @dev Loose specification of adapters:
/// - They must enforce that only the vault can call allocate/deallocate.
/// - They must enter/exit markets only in allocate/deallocate.
/// - They must return the right ids on allocate/deallocate. Returned ids must not repeat.
/// - After a call to deallocate, the vault must have an approval to transfer at least `assets` from the adapter.
/// - They must make it possible to make deallocate possible (for in-kind redemptions).
/// - The totalAssets() calculation ignores markets for which the vault has no allocation.
/// - They must not re-enter (directly or indirectly) the vault. They might not statically prevent it, but the curator
/// must not interact with markets that can re-enter the vault.
/// - After an update, the sum of the changes returned after interactions with a given market must be exactly the
/// current estimated position.
/// @dev Ids being reused are useful to cap multiple investments that have a common property.
/// @dev Allocating is prevented if one of the ids' absolute cap is zero and deallocating is prevented if the id's
/// allocation is zero. This prevents interactions with zero assets with unknown markets. For markets that share all
/// their ids, it will be impossible to "disable" them (preventing any interaction) without disabling the others using
/// the same ids.
/// @dev On allocate or deallocate, the adapters might lose some assets (total realAssets decreases), for instance due
/// to roundings or entry/exit fees. This loss should stay negligible compared to gas. Adapters might not statically
/// ensure this, but the curators should not interact with markets that can create big entry/exit losses.
/// @dev Except particular scenarios, adapters should be removed only if they have no assets. In order to ensure no
/// allocator can allocate some assets in an adapter being removed, there should be an id exclusive to the adapter with
/// its cap set to zero.
///
/// ADAPTER REGISTRY
/// @dev An adapter registry can be added to restrict the adapters. This is useful to commit to using only a certain
/// type of adapters for example.
/// @dev If adapterRegistry is set to address(0), the vault can have any adapters.
/// @dev When an adapterRegistry is set, it retroactively checks already added adapters.
/// @dev If the adapterRegistry now returns false for an already added adapter, it doesn't impact the vault's
/// functioning.
/// @dev The invariant that adapters of the vault are all in the registry holds only if the registry cannot remove
/// adapters (is "add only").
///
/// LIQUIDITY ADAPTER
/// @dev Liquidity is allocated to the liquidityAdapter on deposit/mint, and deallocated from the liquidityAdapter on
/// withdraw/redeem if idle assets don't cover the withdrawal.
/// @dev The liquidity adapter is useful on exit, so that exit liquidity is available in addition to the idle assets. But
/// the same adapter/data is used for both entry and exit to have the property that in the general case looping
/// supply-withdraw or withdraw-supply should not change the allocation.
/// @dev If a cap (absolute or relative) associated with the ids returned by the liquidity adapter on the liquidity data
/// is reached, deposit/mint will revert. In particular, when the vault is empty or almost empty, the relative cap check
/// is likely to make deposits revert.
///
/// TOKEN REQUIREMENTS
/// @dev List of assumptions on the token that guarantees that the vault behaves as expected:
/// - It should be ERC-20 compliant, except that it can omit return values on transfer and transferFrom.
/// - The balance of the vault should only decrease on transfer and transferFrom.
/// - It should not re-enter the vault on transfer or transferFrom.
/// - The balance of the sender (resp. receiver) should decrease (resp. increase) by exactly the given amount on
/// transfer and transferFrom. In particular, tokens with fees on transfer are not supported.
///
/// LIVENESS REQUIREMENTS
/// @dev List of assumptions that guarantees the vault's liveness properties:
/// - Adapters should not revert on realAssets.
/// - The token should not revert on transfer and transferFrom if balances and approvals are right.
/// - The token should not revert on transfer to self.
/// - totalAssets and totalSupply must stay below ~10^35. Initially there are min(1, 10^(18-decimals)) shares per asset.
/// - The vault is pinged at least every 10 years.
/// - Adapters must not revert on deallocate if the underlying markets are liquid.
///
/// TIMELOCKS
/// @dev The timelock duration of decreaseTimelock is the timelock duration of the function whose timelock is being
/// decreased (e.g. the timelock of decreaseTimelock(addAdapter, ...) is timelock[addAdapter]).
/// @dev It is still possible to submit changes of the timelock duration of decreaseTimelock, but it won't have any
/// effect (and trying to execute this change will revert).
/// @dev Multiple clashing data can be pending, for example increaseCap and decreaseCap, which can make so accepted
/// timelocked data can potentially be changed shortly afterwards.
/// @dev If a function is abdicated, it cannot be called no matter its timelock and what executableAt[data] contains.
/// Otherwise, the minimum time in which a function can be called is the following:
/// min(
///     timelock[selector],
///     executableAt[selector::_],
///     executableAt[decreaseTimelock::selector::newTimelock] + newTimelock
/// ).
/// @dev Nothing is checked on the timelocked data, so it could be not executable (function does not exist, argument
/// encoding is wrong, function' conditions are not met, etc.).
///
/// ABDICATION
/// @dev When a timelocked function is abdicated, it can't be called anymore.
/// @dev It is still possible to submit data for it or change its timelock, but it will not be executable / effective.
///
/// GATES
/// @dev Set to 0 to disable a gate.
/// @dev Gates must never revert, nor consume too much gas.
/// @dev receiveSharesGate:
///     - Gates receiving shares.
///     - Can lock users out of getting back their shares deposited on an other contract.
/// @dev sendSharesGate:
///     - Gates sending shares.
///     - Can lock users out of exiting the vault.
/// @dev receiveAssetsGate:
///     - Gates withdrawing assets from the vault.
///     - The vault itself (address(this)) is always allowed to receive assets, regardless of the gate configuration.
///     - Can lock users out of exiting the vault.
/// @dev sendAssetsGate:
///     - Gates depositing assets to the vault.
///     - This gate is not critical (cannot block users' funds), while still being able to gate supplies.
///
/// FEES
/// @dev Fees unit is WAD.
/// @dev This invariant holds for both fees: fee != 0 => recipient != address(0).
///
/// ROLES
/// @dev The owner cannot do actions that can directly hurt depositors. Though it can set the curator and sentinels.
/// @dev The curator cannot do actions that can directly hurt depositors without going through a timelock.
/// @dev Allocators can move funds between markets in the boundaries set by caps without going through timelocks. They
/// can also set the liquidity adapter and data, which can prevent deposits and/or withdrawals (it cannot prevent
/// "in-kind redemptions" with forceDeallocate though). Allocators also set the maxRate.
/// @dev Warning: if setIsAllocator is timelocked, removing an allocator will take time.
/// @dev Roles are not "two-step", so anyone can give a role to anyone, but it does not mean that they will exercise it.
///
/// MISC
/// @dev Zero checks are not systematically performed.
/// @dev No-ops are allowed.
/// @dev NatSpec comments are included only when they bring clarity.
/// @dev The contract uses transient storage.
/// @dev At creation, all settings are set to their default values. Notably, timelocks are zero which is useful to set
/// up the vault quickly. Also, there are no gates so anybody can interact with the vault. To prevent that, the gates
/// configuration can be batched with the vault creation.
contract VaultV2 is IVaultV2 {
    using MathLib for uint256;
    using MathLib for uint128;
    using MathLib for int256;

    /* IMMUTABLE */

    address public immutable asset;
    uint8 public immutable decimals;
    uint256 public immutable virtualShares;

    /* ROLES STORAGE */

    address public owner;
    address public curator;
    address public receiveSharesGate;
    address public sendSharesGate;
    address public receiveAssetsGate;
    address public sendAssetsGate;
    address public adapterRegistry;
    mapping(address account => bool) public isSentinel;
    mapping(address account => bool) public isAllocator;

    /* TOKEN STORAGE */

    string public name;
    string public symbol;
    uint256 public totalSupply;
    mapping(address account => uint256) public balanceOf;
    mapping(address owner => mapping(address spender => uint256)) public allowance;
    mapping(address account => uint256) public nonces;

    /* INTEREST STORAGE */

    uint256 public transient firstTotalAssets;
    uint128 public _totalAssets;
    uint64 public lastUpdate;
    uint64 public maxRate;

    /* CURATION STORAGE */

    mapping(address account => bool) public isAdapter;
    address[] public adapters;
    mapping(bytes32 id => Caps) internal caps;
    mapping(address adapter => uint256) public forceDeallocatePenalty;

    /* LIQUIDITY ADAPTER STORAGE */

    address public liquidityAdapter;
    bytes public liquidityData;

    /* TIMELOCKS STORAGE */

    mapping(bytes4 selector => uint256) public timelock;
    mapping(bytes4 selector => bool) public abdicated;
    mapping(bytes data => uint256) public executableAt;

    /* FEES STORAGE */

    uint96 public performanceFee;
    address public performanceFeeRecipient;
    uint96 public managementFee;
    address public managementFeeRecipient;

    /* GETTERS */

    function adaptersLength() external view returns (uint256) {
        return adapters.length;
    }

    function totalAssets() external view returns (uint256) {
        (uint256 newTotalAssets,,) = accrueInterestView();
        return newTotalAssets;
    }

    function DOMAIN_SEPARATOR() public view returns (bytes32) {
        return keccak256(abi.encode(DOMAIN_TYPEHASH, block.chainid, address(this)));
    }

    function absoluteCap(bytes32 id) external view returns (uint256) {
        return caps[id].absoluteCap;
    }

    function relativeCap(bytes32 id) external view returns (uint256) {
        return caps[id].relativeCap;
    }

    function allocation(bytes32 id) external view returns (uint256) {
        return caps[id].allocation;
    }

    /* MULTICALL */

    /// @dev Useful for EOAs to batch admin calls.
    /// @dev Does not return anything, because accounts who would use the return data would be contracts, which can do
    /// the multicall themselves.
    function multicall(bytes[] calldata data) external {
        for (uint256 i = 0; i < data.length; i++) {
            (bool success, bytes memory returnData) = address(this).delegatecall(data[i]);
            if (!success) {
                assembly ("memory-safe") {
                    revert(add(32, returnData), mload(returnData))
                }
            }
        }
    }

    /* CONSTRUCTOR */

    constructor(address _owner, address _asset) {
        asset = _asset;
        owner = _owner;
        lastUpdate = uint64(block.timestamp);
        uint256 assetDecimals = IERC20(_asset).decimals();
        uint256 decimalOffset = uint256(18).zeroFloorSub(assetDecimals);
        decimals = uint8(assetDecimals + decimalOffset);
        virtualShares = 10 ** decimalOffset;
        emit EventsLib.Constructor(_owner, _asset);
    }

    /* OWNER FUNCTIONS */

    function setOwner(address newOwner) external {
        require(msg.sender == owner, ErrorsLib.Unauthorized());
        owner = newOwner;
        emit EventsLib.SetOwner(newOwner);
    }

    function setCurator(address newCurator) external {
        require(msg.sender == owner, ErrorsLib.Unauthorized());
        curator = newCurator;
        emit EventsLib.SetCurator(newCurator);
    }

    function setIsSentinel(address account, bool newIsSentinel) external {
        require(msg.sender == owner, ErrorsLib.Unauthorized());
        isSentinel[account] = newIsSentinel;
        emit EventsLib.SetIsSentinel(account, newIsSentinel);
    }

    function setName(string memory newName) external {
        require(msg.sender == owner, ErrorsLib.Unauthorized());
        name = newName;
        emit EventsLib.SetName(newName);
    }

    function setSymbol(string memory newSymbol) external {
        require(msg.sender == owner, ErrorsLib.Unauthorized());
        symbol = newSymbol;
        emit EventsLib.SetSymbol(newSymbol);
    }

    /* TIMELOCKS FOR CURATOR FUNCTIONS */

    /// @dev Will revert if the timelock value is type(uint256).max or any value that overflows when added to the block
    /// timestamp.
    function submit(bytes calldata data) external {
        require(msg.sender == curator, ErrorsLib.Unauthorized());
        require(executableAt[data] == 0, ErrorsLib.DataAlreadyPending());

        bytes4 selector = bytes4(data);
        uint256 _timelock =
            selector == IVaultV2.decreaseTimelock.selector ? timelock[bytes4(data[4:8])] : timelock[selector];
        executableAt[data] = block.timestamp + _timelock;
        emit EventsLib.Submit(selector, data, executableAt[data]);
    }

    function timelocked() internal {
        bytes4 selector = bytes4(msg.data);
        require(executableAt[msg.data] != 0, ErrorsLib.DataNotTimelocked());
        require(block.timestamp >= executableAt[msg.data], ErrorsLib.TimelockNotExpired());
        require(!abdicated[selector], ErrorsLib.Abdicated());
        executableAt[msg.data] = 0;
        emit EventsLib.Accept(selector, msg.data);
    }

    function revoke(bytes calldata data) external {
        require(msg.sender == curator || isSentinel[msg.sender], ErrorsLib.Unauthorized());
        require(executableAt[data] != 0, ErrorsLib.DataNotTimelocked());
        executableAt[data] = 0;
        bytes4 selector = bytes4(data);
        emit EventsLib.Revoke(msg.sender, selector, data);
    }

    /* CURATOR FUNCTIONS */

    function setIsAllocator(address account, bool newIsAllocator) external {
        timelocked();
        isAllocator[account] = newIsAllocator;
        emit EventsLib.SetIsAllocator(account, newIsAllocator);
    }

    function setReceiveSharesGate(address newReceiveSharesGate) external {
        timelocked();
        receiveSharesGate = newReceiveSharesGate;
        emit EventsLib.SetReceiveSharesGate(newReceiveSharesGate);
    }

    function setSendSharesGate(address newSendSharesGate) external {
        timelocked();
        sendSharesGate = newSendSharesGate;
        emit EventsLib.SetSendSharesGate(newSendSharesGate);
    }

    function setReceiveAssetsGate(address newReceiveAssetsGate) external {
        timelocked();
        receiveAssetsGate = newReceiveAssetsGate;
        emit EventsLib.SetReceiveAssetsGate(newReceiveAssetsGate);
    }

    function setSendAssetsGate(address newSendAssetsGate) external {
        timelocked();
        sendAssetsGate = newSendAssetsGate;
        emit EventsLib.SetSendAssetsGate(newSendAssetsGate);
    }

    /// @dev The no-op will revert if the registry now returns false for an already added adapter.
    function setAdapterRegistry(address newAdapterRegistry) external {
        timelocked();

        if (newAdapterRegistry != address(0)) {
            for (uint256 i = 0; i < adapters.length; i++) {
                require(
                    IAdapterRegistry(newAdapterRegistry).isInRegistry(adapters[i]), ErrorsLib.NotInAdapterRegistry()
                );
            }
        }

        adapterRegistry = newAdapterRegistry;
        emit EventsLib.SetAdapterRegistry(newAdapterRegistry);
    }

    function addAdapter(address account) external {
        timelocked();
        require(
            adapterRegistry == address(0) || IAdapterRegistry(adapterRegistry).isInRegistry(account),
            ErrorsLib.NotInAdapterRegistry()
        );
        if (!isAdapter[account]) {
            adapters.push(account);
            isAdapter[account] = true;
        }
        emit EventsLib.AddAdapter(account);
    }

    function removeAdapter(address account) external {
        timelocked();
        if (isAdapter[account]) {
            for (uint256 i = 0; i < adapters.length; i++) {
                if (adapters[i] == account) {
                    adapters[i] = adapters[adapters.length - 1];
                    adapters.pop();
                    break;
                }
            }
            isAdapter[account] = false;
        }
        emit EventsLib.RemoveAdapter(account);
    }

    /// @dev This function requires great caution because it can irreversibly disable submit for a selector.
    /// @dev Existing pending operations submitted before increasing a timelock can still be executed at the initial
    /// executableAt.
    function increaseTimelock(bytes4 selector, uint256 newDuration) external {
        timelocked();
        require(selector != IVaultV2.decreaseTimelock.selector, ErrorsLib.AutomaticallyTimelocked());
        require(newDuration >= timelock[selector], ErrorsLib.TimelockNotIncreasing());

        timelock[selector] = newDuration;
        emit EventsLib.IncreaseTimelock(selector, newDuration);
    }

    function decreaseTimelock(bytes4 selector, uint256 newDuration) external {
        timelocked();
        require(selector != IVaultV2.decreaseTimelock.selector, ErrorsLib.AutomaticallyTimelocked());
        require(newDuration <= timelock[selector], ErrorsLib.TimelockNotDecreasing());

        timelock[selector] = newDuration;
        emit EventsLib.DecreaseTimelock(selector, newDuration);
    }

    function abdicate(bytes4 selector) external {
        timelocked();
        abdicated[selector] = true;
        emit EventsLib.Abdicate(selector);
    }

    function setPerformanceFee(uint256 newPerformanceFee) external {
        timelocked();
        require(newPerformanceFee <= MAX_PERFORMANCE_FEE, ErrorsLib.FeeTooHigh());
        require(performanceFeeRecipient != address(0) || newPerformanceFee == 0, ErrorsLib.FeeInvariantBroken());

        accrueInterest();

        // Safe because 2**96 > MAX_PERFORMANCE_FEE.
        performanceFee = uint96(newPerformanceFee);
        emit EventsLib.SetPerformanceFee(newPerformanceFee);
    }

    function setManagementFee(uint256 newManagementFee) external {
        timelocked();
        require(newManagementFee <= MAX_MANAGEMENT_FEE, ErrorsLib.FeeTooHigh());
        require(managementFeeRecipient != address(0) || newManagementFee == 0, ErrorsLib.FeeInvariantBroken());

        accrueInterest();

        // Safe because 2**96 > MAX_MANAGEMENT_FEE.
        managementFee = uint96(newManagementFee);
        emit EventsLib.SetManagementFee(newManagementFee);
    }

    function setPerformanceFeeRecipient(address newPerformanceFeeRecipient) external {
        timelocked();
        require(newPerformanceFeeRecipient != address(0) || performanceFee == 0, ErrorsLib.FeeInvariantBroken());

        accrueInterest();

        performanceFeeRecipient = newPerformanceFeeRecipient;
        emit EventsLib.SetPerformanceFeeRecipient(newPerformanceFeeRecipient);
    }

    function setManagementFeeRecipient(address newManagementFeeRecipient) external {
        timelocked();
        require(newManagementFeeRecipient != address(0) || managementFee == 0, ErrorsLib.FeeInvariantBroken());

        accrueInterest();

        managementFeeRecipient = newManagementFeeRecipient;
        emit EventsLib.SetManagementFeeRecipient(newManagementFeeRecipient);
    }

    function increaseAbsoluteCap(bytes memory idData, uint256 newAbsoluteCap) external {
        timelocked();
        bytes32 id = keccak256(idData);
        require(newAbsoluteCap >= caps[id].absoluteCap, ErrorsLib.AbsoluteCapNotIncreasing());

        caps[id].absoluteCap = newAbsoluteCap.toUint128();
        emit EventsLib.IncreaseAbsoluteCap(id, idData, newAbsoluteCap);
    }

    function decreaseAbsoluteCap(bytes memory idData, uint256 newAbsoluteCap) external {
        bytes32 id = keccak256(idData);
        require(msg.sender == curator || isSentinel[msg.sender], ErrorsLib.Unauthorized());
        require(newAbsoluteCap <= caps[id].absoluteCap, ErrorsLib.AbsoluteCapNotDecreasing());

        // Safe because newAbsoluteCap <= absoluteCap < 2**128.
        caps[id].absoluteCap = uint128(newAbsoluteCap);
        emit EventsLib.DecreaseAbsoluteCap(msg.sender, id, idData, newAbsoluteCap);
    }

    function increaseRelativeCap(bytes memory idData, uint256 newRelativeCap) external {
        timelocked();
        bytes32 id = keccak256(idData);
        require(newRelativeCap <= WAD, ErrorsLib.RelativeCapAboveOne());
        require(newRelativeCap >= caps[id].relativeCap, ErrorsLib.RelativeCapNotIncreasing());

        // Safe because WAD < 2**128.
        caps[id].relativeCap = uint128(newRelativeCap);
        emit EventsLib.IncreaseRelativeCap(id, idData, newRelativeCap);
    }

    function decreaseRelativeCap(bytes memory idData, uint256 newRelativeCap) external {
        bytes32 id = keccak256(idData);
        require(msg.sender == curator || isSentinel[msg.sender], ErrorsLib.Unauthorized());
        require(newRelativeCap <= caps[id].relativeCap, ErrorsLib.RelativeCapNotDecreasing());

        // Safe because WAD < 2**128.
        caps[id].relativeCap = uint128(newRelativeCap);
        emit EventsLib.DecreaseRelativeCap(msg.sender, id, idData, newRelativeCap);
    }

    function setForceDeallocatePenalty(address adapter, uint256 newForceDeallocatePenalty) external {
        timelocked();
        require(newForceDeallocatePenalty <= MAX_FORCE_DEALLOCATE_PENALTY, ErrorsLib.PenaltyTooHigh());
        forceDeallocatePenalty[adapter] = newForceDeallocatePenalty;
        emit EventsLib.SetForceDeallocatePenalty(adapter, newForceDeallocatePenalty);
    }

    /* ALLOCATOR FUNCTIONS */

    function allocate(address adapter, bytes memory data, uint256 assets) external {
        require(isAllocator[msg.sender], ErrorsLib.Unauthorized());
        allocateInternal(adapter, data, assets);
    }

    function allocateInternal(address adapter, bytes memory data, uint256 assets) internal {
        require(isAdapter[adapter], ErrorsLib.NotAdapter());

        accrueInterest();

        SafeERC20Lib.safeTransfer(asset, adapter, assets);
        (bytes32[] memory ids, int256 change) = IAdapter(adapter).allocate(data, assets, msg.sig, msg.sender);

        for (uint256 i; i < ids.length; i++) {
            Caps storage _caps = caps[ids[i]];
            _caps.allocation = (int256(_caps.allocation) + change).toUint256();

            require(_caps.absoluteCap > 0, ErrorsLib.ZeroAbsoluteCap());
            require(_caps.allocation <= _caps.absoluteCap, ErrorsLib.AbsoluteCapExceeded());
            require(
                _caps.relativeCap == WAD || _caps.allocation <= firstTotalAssets.mulDivDown(_caps.relativeCap, WAD),
                ErrorsLib.RelativeCapExceeded()
            );
        }
        emit EventsLib.Allocate(msg.sender, adapter, assets, ids, change);
    }

    function deallocate(address adapter, bytes memory data, uint256 assets) external {
        require(isAllocator[msg.sender] || isSentinel[msg.sender], ErrorsLib.Unauthorized());
        deallocateInternal(adapter, data, assets);
    }

    function deallocateInternal(address adapter, bytes memory data, uint256 assets)
        internal
        returns (bytes32[] memory)
    {
        require(isAdapter[adapter], ErrorsLib.NotAdapter());

        (bytes32[] memory ids, int256 change) = IAdapter(adapter).deallocate(data, assets, msg.sig, msg.sender);

        for (uint256 i; i < ids.length; i++) {
            Caps storage _caps = caps[ids[i]];
            require(_caps.allocation > 0, ErrorsLib.ZeroAllocation());
            _caps.allocation = (int256(_caps.allocation) + change).toUint256();
        }

        SafeERC20Lib.safeTransferFrom(asset, adapter, address(this), assets);
        emit EventsLib.Deallocate(msg.sender, adapter, assets, ids, change);
        return ids;
    }

    /// @dev Whether newLiquidityAdapter is an adapter is checked in allocate/deallocate.
    function setLiquidityAdapterAndData(address newLiquidityAdapter, bytes memory newLiquidityData) external {
        require(isAllocator[msg.sender], ErrorsLib.Unauthorized());
        liquidityAdapter = newLiquidityAdapter;
        liquidityData = newLiquidityData;
        emit EventsLib.SetLiquidityAdapterAndData(msg.sender, newLiquidityAdapter, newLiquidityData);
    }

    function setMaxRate(uint256 newMaxRate) external {
        require(isAllocator[msg.sender], ErrorsLib.Unauthorized());
        require(newMaxRate <= MAX_MAX_RATE, ErrorsLib.MaxRateTooHigh());

        accrueInterest();

        // Safe because newMaxRate <= MAX_MAX_RATE < 2**64-1.
        maxRate = uint64(newMaxRate);
        emit EventsLib.SetMaxRate(newMaxRate);
    }

    /* EXCHANGE RATE FUNCTIONS */

    function accrueInterest() public {
        (uint256 newTotalAssets, uint256 performanceFeeShares, uint256 managementFeeShares) = accrueInterestView();
        emit EventsLib.AccrueInterest(_totalAssets, newTotalAssets, performanceFeeShares, managementFeeShares);
        _totalAssets = newTotalAssets.toUint128();
        if (firstTotalAssets == 0) firstTotalAssets = newTotalAssets;
        if (performanceFeeShares != 0) createShares(performanceFeeRecipient, performanceFeeShares);
        if (managementFeeShares != 0) createShares(managementFeeRecipient, managementFeeShares);
        lastUpdate = uint64(block.timestamp);
    }

    /// @dev Returns newTotalAssets, performanceFeeShares, managementFeeShares.
    /// @dev The management fee is not bound to the interest, so it can make the share price go down.
    /// @dev The management fees is taken even if the vault incurs some losses.
    /// @dev Both fees are rounded down, so fee recipients could receive less than expected.
    /// @dev The performance fee is taken on the "distributed interest" (which differs from the "real interest" because
    /// of the max rate).
    function accrueInterestView() public view returns (uint256, uint256, uint256) {
        if (firstTotalAssets != 0) return (_totalAssets, 0, 0);
        uint256 elapsed = block.timestamp - lastUpdate;
        uint256 realAssets = IERC20(asset).balanceOf(address(this));
        for (uint256 i = 0; i < adapters.length; i++) {
            realAssets += IAdapter(adapters[i]).realAssets();
        }
        uint256 maxTotalAssets = _totalAssets + (_totalAssets * elapsed).mulDivDown(maxRate, WAD);
        uint256 newTotalAssets = MathLib.min(realAssets, maxTotalAssets);
        uint256 interest = newTotalAssets.zeroFloorSub(_totalAssets);

        // The performance fee assets may be rounded down to 0 if interest * fee < WAD.
        uint256 performanceFeeAssets = interest > 0 && performanceFee > 0 && canReceiveShares(performanceFeeRecipient)
            ? interest.mulDivDown(performanceFee, WAD)
            : 0;
        // The management fee is taken on newTotalAssets to make all approximations consistent (interacting less
        // increases fees).
        uint256 managementFeeAssets = elapsed > 0 && managementFee > 0 && canReceiveShares(managementFeeRecipient)
            ? (newTotalAssets * elapsed).mulDivDown(managementFee, WAD)
            : 0;

        // Interest should be accrued at least every 10 years to avoid fees exceeding total assets.
        uint256 newTotalAssetsWithoutFees = newTotalAssets - performanceFeeAssets - managementFeeAssets;
        uint256 performanceFeeShares =
            performanceFeeAssets.mulDivDown(totalSupply + virtualShares, newTotalAssetsWithoutFees + 1);
        uint256 managementFeeShares =
            managementFeeAssets.mulDivDown(totalSupply + virtualShares, newTotalAssetsWithoutFees + 1);

        return (newTotalAssets, performanceFeeShares, managementFeeShares);
    }

    /// @dev Returns previewed minted shares.
    function previewDeposit(uint256 assets) public view returns (uint256) {
        (uint256 newTotalAssets, uint256 performanceFeeShares, uint256 managementFeeShares) = accrueInterestView();
        uint256 newTotalSupply = totalSupply + performanceFeeShares + managementFeeShares;
        return assets.mulDivDown(newTotalSupply + virtualShares, newTotalAssets + 1);
    }

    /// @dev Returns previewed deposited assets.
    function previewMint(uint256 shares) public view returns (uint256) {
        (uint256 newTotalAssets, uint256 performanceFeeShares, uint256 managementFeeShares) = accrueInterestView();
        uint256 newTotalSupply = totalSupply + performanceFeeShares + managementFeeShares;
        return shares.mulDivUp(newTotalAssets + 1, newTotalSupply + virtualShares);
    }

    /// @dev Returns previewed redeemed shares.
    function previewWithdraw(uint256 assets) public view returns (uint256) {
        (uint256 newTotalAssets, uint256 performanceFeeShares, uint256 managementFeeShares) = accrueInterestView();
        uint256 newTotalSupply = totalSupply + performanceFeeShares + managementFeeShares;
        return assets.mulDivUp(newTotalSupply + virtualShares, newTotalAssets + 1);
   

Tags:
ERC20, Multisig, Mintable, Liquidity, Voting, Timelock, Upgradeable, Multi-Signature, Factory|addr:0xae7c83c4b76ad97089443e3d6ea57ff1776d101a|verified:true|block:23671242|tx:0xdb736b011a70d3a596191f53fc755888c070ecf926166fbea2f4c20e2ae06041|first_check:1761642150

Submitted on: 2025-10-28 10:02:32

Comments

Log in to comment.

No comments yet.