Gates in Morpho Vaults
Morpho Vaults (v1 and v2) support different mechanisms to restrict access to deposits, withdrawals, and transfers. Vault v2 introduces first-class gates as external contracts with timelock and abdication guarantees, while Vault v1.1 achieves gating operationally by manipulating the supplyQueue
and market caps.
High-Level Comparison
Aspect | Vault v1.1 | Vault v2 |
---|---|---|
Native Gates | ❌ None (must use supplyQueue + caps) | ✅ Four explicit gates (receive/send shares, receive/send assets) |
Granularity | Coarse: deposits only (via supplyQueue ) | Fine: deposits, withdrawals, transfers, fee accrual |
Timelock | Caps increases are timelocked (24h–2w) | All gate changes timelocked (configurable, e.g. up to 3 weeks) |
Abdication | ❌ Not supported | ✅ Supported (permanent permissionless or permanently gated) |
Atomic batching | ✅ multicall allows open–deposit–close | ✅ multicall supported but rarely needed for gates |
Intended use cases | Simple whitelisting of suppliers | Compliance, allowlists, permissioned transfers |
Gates in Vault v2
Executive Summary
Gates in Vault v2 are external smart contracts that provide access control and compliance mechanisms for vault operations. They enable selective permissions for share transfers, asset deposits, and asset withdrawals while making it possible to maintain the vault's non-custodial guarantees. Gates are optional (if not set, operations are unrestricted) and can be timelocked for security.
Core Architecture
Four Gate Types

Vault v2 implements a four-gate system addressing different operational vectors:
1. Receive Shares Gate (receiveSharesGate
)
- Interface:
IReceiveSharesGate
withcanReceiveShares(address)
(TODO: redirect to the code) - Controls: Who can receive vault shares
- Operations:
deposit()
,mint()
→ checkscanReceiveShares(onBehalf)
transfer()
,transferFrom()
→ checkscanReceiveShares(to)
- Performance/management fee accrual → checks
canReceiveShares(feeRecipient)
- ⚠️ Critical Risk: Can prevent depositors from getting back shares deposited on other contracts. Note that if the gate reverts instead of returning
false
, it can cause a denial of service (DoS) across core vault operations.
2. Send Shares Gate (sendSharesGate
)
- Interface:
ISendSharesGate
withcanSendShares(address)
- Controls: Who can send vault shares
- Operations:
withdraw()
,redeem()
→ checkscanSendShares(onBehalf)
transfer()
,transferFrom()
→ checkscanSendShares(from)
- ⚠️ Critical Risk: Can lock users out of exiting the vault or prevent depositors from getting back shares deposited on other contracts
3. Receive Assets Gate (receiveAssetsGate
)
- Interface:
IReceiveAssetsGate
withcanReceiveAssets(address)
- Controls: Who can receive underlying assets from the vault
- Operations:
withdraw()
,redeem()
→ checkscanReceiveAssets(receiver)
- Special Rule: Vault itself (
address(this)
) always bypasses this gate - ⚠️ Critical Risk: Can prevent people from receiving their assets upon withdrawals
4. Send Assets Gate (sendAssetsGate
)
- Interface:
ISendAssetsGate
withcanSendAssets(address)
- Controls: Who can deposit underlying assets into the vault
- Operations:
deposit()
,mint()
→ checkscanSendAssets(msg.sender)
- Non-critical: Cannot block users' funds, while still being able to gate supplies
Technical Implementation Deep Dive
Gate Checking Logic
// From VaultV2.sol - Gate validation functions
function canReceiveShares(address account) public view returns (bool) {
return receiveSharesGate == address(0) ||
IReceiveSharesGate(receiveSharesGate).canReceiveShares(account);
}
function canSendShares(address account) public view returns (bool) {
return sendSharesGate == address(0) ||
ISendSharesGate(sendSharesGate).canSendShares(account);
}
function canReceiveAssets(address account) public view returns (bool) {
return account == address(this) ||
receiveAssetsGate == address(0) ||
IReceiveAssetsGate(receiveAssetsGate).canReceiveAssets(account);
}
function canSendAssets(address account) public view returns (bool) {
return sendAssetsGate == address(0) ||
ISendAssetsGate(sendAssetsGate).canSendAssets(account);
}
Critical Implementation Requirements
Gates MUST be designed knowing it should never revert, returning always boolean values. Also gates should not use a malicious amount of gas (like something close to the block size).
Governance and Security Model
Timelock Protection
Gates follow the vault's timelock system:
// Curator functions for gate management (timelockable)
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);
}
The Two Types of Permanent Guarantees with Gate Abdication
// Example: Permanently disable receive shares gate changes
vault.abdicateSubmit(IVaultV2.setReceiveSharesGate.selector);
The abdicateSubmit()
function allows a curator to make an irreversible commitment regarding a vault's configuration, providing users with permanent guarantees. When applied to gates, this creates two distinct and powerful outcomes: ensuring a vault is either permanently permissionless or permanently governed by a fixed set of rules:
Type 1. The Permanently Ungated Vault (Abdicating to Zero)
This strategy provides the strongest guarantee of permissionless access.
- Goal: To ensure a vault can never have a specific type of gate imposed on it.
- How it Works: The curator first ensures the target gate address is set to address(0) (disabling it). They then call abdicateSubmit on that gate's setter function (e.g., setReceiveSharesGate).
- Result: This action permanently locks the gate address to address(0). No one can ever set a gate contract for that function in the future. This is a powerful tool for creating truly decentralized vaults where users are assured that access rules will never be introduced.
Type 2. The Permanently Gated Vault (Abdicating to a Non-Zero Gate)
This strategy is designed for vaults that must adhere to a fixed, unchangeable set of compliance rules.
- Goal: To lock a vault into a specific, non-modifiable access control policy.
- How it Works: The curator sets the gate to the address of a deployed gate contract and then calls abdicateSubmit on the setter function.
- Result: The vault becomes permanently bound to that gate contract. The rules defined within that contract can never be altered by changing the gate address.
Crucial Security Consideration: This guarantee is only meaningful if the gate contract itself has immutable behavior. If the gate contract contains administrative functions that allow an owner to modify its rules (e.g., change a whitelist), then abdicating simply transfers the power to control access from the vault's curator to the gate's administrator. For a true permanent guarantee, the gate contract must be designed without such administrative controls, or its ownership must be renounced.
Bundler Example Integration
Ref: https://github.com/morpho-org/vault-v2/blob/main/test/examples/GateExample.sol (unaudited example)
// From GateExample.sol - Handles Bundler3 operations
contract GateExample is IReceiveSharesGate, ISendSharesGate,
IReceiveAssetsGate, ISendAssetsGate {
...
mapping(address => bool) public isBundlerAdapter;
mapping(address => bool) public whitelisted;
...
function whitelistedOrHandlingOnBehalf(address account) internal view returns (bool) {
return whitelisted[account] ||
(isBundlerAdapter[account] &&
whitelisted[IBundlerAdapter(account).BUNDLER3().initiator()]);
}
function canSendShares(address account) external view returns (bool) {
return whitelistedOrHandlingOnBehalf(account);
}
function canReceiveShares(address account) external view returns (bool) {
return whitelistedOrHandlingOnBehalf(account);
}
function canReceiveAssets(address account) external view returns (bool) {
return whitelistedOrHandlingOnBehalf(account);
}
function canSendAssets(address account) external view returns (bool) {
return whitelistedOrHandlingOnBehalf(account);
}
...
}
Special Cases and Edge Conditions
Vault Self-Operations
The vault itself (address(this)
) is always allowed to receive assets, regardless of the receiveAssetsGate
configuration. This prevents the vault from blocking its own internal operations:
function canReceiveAssets(address account) public view returns (bool) {
return account == address(this) || // Vault always bypasses
receiveAssetsGate == address(0) ||
IReceiveAssetsGate(receiveAssetsGate).canReceiveAssets(account);
}
Fee Recipient Gate Checks
Important: If a gate is set and reverts for fee recipients when fees are non-zero, interest accrual will revert. This makes proper gate configuration critical for vault operation.
Impact on Non-Custodial Guarantees
Force Deallocate Mechanism
function forceDeallocate(address adapter, bytes memory data, uint256 assets, address onBehalf)
external returns (uint256)
This function allows users to perform in-kind redemptions by:
- Flashloaning liquidity
- Supplying to an adapter's market
- Withdrawing liquidity through
forceDeallocate
- Repaying the flashloan
More details are provided here.
Testing Considerations
The codebase includes comprehensive gate testing:
GateExampleTest.sol
- Tests the reference implementation (Disclaimer: this should not be used in production as is).GatingTest.sol
- Tests all gate integration points- Tests verify proper error handling for each gate type
- Tests confirm vault self-operations bypass receive assets gate
Gates in Vault v1
Vault v1.1 does not include first-class gates. Instead, access control can be emulated by playing with the supplyQueue
and market caps, combined with multicall
. This provides a practical gating mechanism for suppliers.
Key Mechanics
- Deposits only work if the
supplyQueue
is non-empty. If empty,deposit
reverts and the vault is effectively closed. - Allocator role can update the queue at any time (no timelock).
- A market must have a non-zero cap to appear in the queue.
- Cap increases are timelocked (24h–2w), but cap decreases are instant.
- The vault supports
multicall
, allowing atomic open–deposit–close flows to prevent front-running.
Tutorial
One-Time Setup
- Deploy the v1.1 vault (
MetaMorphoV1_1
). - Assign roles:
- Owner → multisig
- Curator → multisig (or another trusted address)
- Allocator → hot wallet / automation bot that will run the batch transaction
- Prepare the market: ensure cap > 0 (requires timelock if raising).
Keep the Vault Closed by Default
// Curator or Allocator
vault.setSupplyQueue(new MarketParams); // []
No one can deposit, as the queue is empty.
Open Deposit Window (Privileged Supplier)
Perform an atomic batch via multicall
:
- Open queue (add target market).
- Deposit assets.
- Close queue (reset to empty).
Example in viem
import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
import { morphoVaultAbi } from "./abi";
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const client = createWalletClient({
account,
chain: mainnet,
transport: http(RPC_URL),
});
const VAULT_ADDRESS = "0x...";
const TARGET_MARKET = "0x..."; // market id
const AMOUNT = 1_000n * 10n ** 18n;
const openQueueCall = {
abi: morphoVaultAbi,
functionName: "setSupplyQueue",
args: [[TARGET_MARKET]],
};
const depositCall = {
abi: morphoVaultAbi,
functionName: "deposit",
args: [AMOUNT, account.address],
};
const closeQueueCall = {
abi: morphoVaultAbi,
functionName: "setSupplyQueue",
args: [[]],
};
await client.writeContract({
address: VAULT_ADDRESS,
abi: morphoVaultAbi,
functionName: "multicall",
args: [[
client.encodeFunctionData(openQueueCall),
client.encodeFunctionData(depositCall),
client.encodeFunctionData(closeQueueCall),
]],
});
Operational Caveats
- Always batch open–deposit–close; otherwise, others may front-run deposits.
- Keep caps at 0 by default and raise only when scheduling controlled deposits.
- This mechanism gates suppliers only; withdrawals remain unaffected.
Final notes on Vault v1 Gating operation:
- The supplier must sign the batch and be the allocator itself.
- If you use a Gnosis Safe, craft a Multisend with those three calls and have the Safe execute it.