Skip to content

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

AspectVault v1.1Vault v2
Native Gates❌ None (must use supplyQueue + caps)✅ Four explicit gates (receive/send shares, receive/send assets)
GranularityCoarse: deposits only (via supplyQueue)Fine: deposits, withdrawals, transfers, fee accrual
TimelockCaps 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 batchingmulticall allows open–deposit–closemulticall supported but rarely needed for gates
Intended use casesSimple whitelisting of suppliersCompliance, 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

Gates in Vault v2

Vault v2 implements a four-gate system addressing different operational vectors:

1. Receive Shares Gate (receiveSharesGate)

  • Interface: IReceiveSharesGate with canReceiveShares(address) (TODO: redirect to the code)
  • Controls: Who can receive vault shares
  • Operations:
    • deposit(), mint() → checks canReceiveShares(onBehalf)
    • transfer(), transferFrom() → checks canReceiveShares(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 with canSendShares(address)
  • Controls: Who can send vault shares
  • Operations:
    • withdraw(), redeem() → checks canSendShares(onBehalf)
    • transfer(), transferFrom() → checks canSendShares(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 with canReceiveAssets(address)
  • Controls: Who can receive underlying assets from the vault
  • Operations:
    • withdraw(), redeem() → checks canReceiveAssets(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 with canSendAssets(address)
  • Controls: Who can deposit underlying assets into the vault
  • Operations:
    • deposit(), mint() → checks canSendAssets(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:

  1. Flashloaning liquidity
  2. Supplying to an adapter's market
  3. Withdrawing liquidity through forceDeallocate
  4. 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

  1. Deploy the v1.1 vault (MetaMorphoV1_1).
  2. Assign roles:
    • Owner → multisig
    • Curator → multisig (or another trusted address)
    • Allocator → hot wallet / automation bot that will run the batch transaction
  3. 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:

  1. Open queue (add target market).
  2. Deposit assets.
  3. 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.