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);
Submitted on: 2025-10-28 10:07:39
Comments
Log in to comment.
No comments yet.