# Vault & VaultFactory Specification

## Table of Contents

* [Quick start](#quick-start)
* [Overview](#overview)
* [The Vault Specification](#the-vault-specification)
  * [VaultBase (V1)](#vaultbase-v1)
  * [VaultBaseV2](#vaultbasev2)
  * [Example vault — BlackHoleVault](#example-vault--blackholevault)
  * [The Flap Guardian](#the-flap-guardian)
  * [Adapter for legacy vaults](#adapter-for-legacy-vaults)
* [VaultFactory Specification](#vaultfactory-specification)
  * [IVaultFactory (V1)](#ivaultfactory-v1)
  * [VaultFactoryBaseV2](#vaultfactorybasev2)
  * [V2.1: Validation hook — `onBeforeNewTokenV6WithVault`](#v21-validation-hook--onbeforenewtokenv6withvault)
  * [V2.1: Policy discovery — `tokenCreationPolicies`](#v21-policy-discovery--tokencreationpolicies)
  * [Recommended commission fee structure](#recommended-commission-fee-structure)
* [UI Schema Reference (IVaultSchemasV1)](#ui-schema-reference-ivaultschemasv1)
  * [FieldDescriptor](#fielddescriptor)
  * [VaultDataSchema](#vaultdataschema)
  * [FactoryPolicy](#factorypolicy)
  * [VaultUISchema & supporting types](#vaultuischema--supporting-types)
* [Get your vault verified](#get-your-vault-verified)
* [FAQ](#faq)

## Quick start

1. **Follow the example** — Work through the [FlapVaultExample repository](https://github.com/flap-sh/FlapVaultExample) to see a complete, working vault and vault factory implementation. It is the fastest way to understand the patterns you need to follow.
2. **Verify your implementation** — Once you have written your vault, use the [FlapVaultSpecChecker](https://github.com/flap-sh/FlapVaultSpecChecker) AI toolkit to automatically check that your implementation correctly follows this specification before deploying.

## Overview

Flap's vault system allows anyone to build smart contracts for managing and distributing BNB revenue generated from tax tokens. Vaults can be customized to fit various use cases — splitting revenue among multiple recipients, routing funds based on social proof, funding a game treasury, or anything else you can imagine.

{% hint style="info" %}
**Permissionless vault creation** — You can now create and deploy your own vaults and vault factories **without any permission or registration**. Vault factories no longer need to be registered in the VaultPortal before they can be used to launch tokens. Users can use any vault they want when launching tokens.
{% endhint %}

{% hint style="warning" %}
For a complete working example of a vault and vault factory implementation, check out the [FlapVaultExample repository](https://github.com/flap-sh/FlapVaultExample).
{% endhint %}

There are two generations of the vault specification, with a point release of the factory spec:

| Version | Vault base contract                 | Factory base contract                             | Key addition                                        |
| ------- | ----------------------------------- | ------------------------------------------------- | --------------------------------------------------- |
| V1      | `VaultBase`                         | `IVaultFactory`                                   | Core spec — `description()`, Guardian mandate       |
| V2      | `VaultBaseV2` (extends `VaultBase`) | `VaultFactoryBaseV2` (implements `IVaultFactory`) | On-chain UI schema for automatic UI generation      |
| V2.1    | *(no change)*                       | `VaultFactoryBaseV2` (updated)                    | Validation hook + machine-readable policy discovery |

V2 and V2.1 are **fully backwards-compatible** with V1. Existing vaults that extend `VaultBase` are unaffected. New vault implementations should extend `VaultBaseV2` and new factory implementations should extend `VaultFactoryBaseV2` to gain UI schema support.

## The Vault Specification

### VaultBase (V1)

To be compatible with the VaultPortal system, your vault smart contract must inherit the `VaultBase` contract:

```solidity
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.13;

/// @title VaultBase
/// @notice Abstract base contract for all vault implementations
/// @author The Flap Team
abstract contract VaultBase {
    /// @notice Error thrown when the current chain is not supported
    error UnsupportedChain(uint256 chainId);

    /// @notice Get the Portal address for the current chain
    /// @dev Currently supports BNB Chain (chain ID 56) and BNB Testnet (chain ID 97)
    /// @return portal The Portal contract address
    function _getPortal() internal view returns (address portal) {
        uint256 chainId = block.chainid;
        if (chainId == 56) {
            return 0xe2cE6ab80874Fa9Fa2aAE65D277Dd6B8e65C9De0;
        } else if (chainId == 97) {
            return 0x5bEacaF7ABCbB3aB280e80D007FD31fcE26510e9;
        }
        revert UnsupportedChain(chainId);
    }

    /// @notice Get the Guardian address for the current chain
    /// @dev Currently supports BNB Chain (chain ID 56) and BNB Testnet (chain ID 97)
    /// @return guardian The Guardian contract address
    function _getGuardian() internal view returns (address guardian) {
        uint256 chainId = block.chainid;
        if (chainId == 56) {
            return 0x9e27098dcD8844bcc6287a557E0b4D09C86B8a4b;
        } else if (chainId == 97) {
            return 0x76Fa8C526f8Bc27ba6958B76DeEf92a0dbE46950;
        }
        revert UnsupportedChain(chainId);
    }

    /// @notice Returns a description of the vault
    /// @dev Must be overridden to provide a dynamic description based on the vault's current state
    /// @return A string describing the vault's current state and configuration
    function description() public view virtual returns (string memory);
}
```

**Implementation requirements:**

1. **Implement `description()`** — Return a dynamic string that describes the vault's current state. The description should change based on the vault's state (e.g. balance, streaming status, etc.).
2. **Implement `receive()`** — Accept BNB from the tax token and process the revenue according to your vault's logic.
3. **Guardian mandate** — If you have any permissioned functions that should be triggered by an external address, and it is not suitable to make them public (e.g. buyback which may be sandwich attacked), you **must** also give the Guardian address the permissions alongside other allowed addresses as a backup. See the [Flap Guardian](#the-flap-guardian) section for details.

### VaultBaseV2

`VaultBaseV2` inherits from `VaultBase` and adds a single new abstract method: `vaultUISchema()`. This allows the UI to automatically discover and render the vault's user-facing methods — without needing custom code for each vault type.

```solidity
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.13;

import {VaultBase} from "./VaultBase.sol";
import {VaultUISchema} from "./IVaultSchemasV1.sol";

/// @title VaultBaseV2
/// @author The Flap Team
abstract contract VaultBaseV2 is VaultBase {
    /// @notice Returns the UI schema describing which methods the UI should
    ///         render for this vault.
    /// @return schema The complete UI schema for this vault.
    function vaultUISchema() public pure virtual returns (VaultUISchema memory schema);
}
```

**What changes from V1:**

* All V1 obligations still apply (`description()`, `receive()`, Guardian mandate).
* You must additionally override `vaultUISchema()` to return a `VaultUISchema` describing every user-facing method your vault exposes. See the [UI Schema Reference](#ui-schema-reference-ivaultschemasv1) section for the full struct definitions.

**Why this matters:** Without a self-describing mechanism, every new vault type would require a custom UI to be built and deployed before users could interact with it. `vaultUISchema()` solves this problem by enabling **automatic UI generation for any future vault type**.

{% hint style="info" %}
Existing vault types (FlapXVault, SplitVault, SnowBallVault, BlackHoleVault) already have purpose-built UIs. `VaultBaseV2` is designed for **future** vault implementations — the UI can automatically generate a full interaction page for any vault that implements this interface.
{% endhint %}

**Example — Hypothetical DonationVault:**

```solidity
function vaultUISchema() public pure override returns (VaultUISchema memory schema) {
    schema.vaultType = "DonationVault";
    schema.description = "Collects BNB donations and lets a designated charity withdraw.";
    schema.methods = new VaultMethodSchema[](3);

    // View: totalDonated() — no inputs, one uint256 output
    schema.methods[0].name        = "totalDonated";
    schema.methods[0].description = "Returns the total BNB donated so far.";
    schema.methods[0].outputs     = new FieldDescriptor[](1);
    schema.methods[0].outputs[0]  = FieldDescriptor("total", "uint256", "Total BNB donated", 18);
    schema.methods[0].approvals   = new ApproveAction[](0);

    // Write: donate() — msg.value input
    // fieldType "msg.value": the UI sets tx.value on the transaction instead of
    // ABI-encoding the amount as a calldata parameter. Only valid on write methods.
    schema.methods[1].name          = "donate";
    schema.methods[1].description   = "Send BNB as a donation.";
    schema.methods[1].inputs        = new FieldDescriptor[](1);
    schema.methods[1].inputs[0]     = FieldDescriptor("amount", "msg.value", "BNB to donate", 18);
    schema.methods[1].outputs       = new FieldDescriptor[](0);
    schema.methods[1].approvals     = new ApproveAction[](0);
    schema.methods[1].isWriteMethod = true;

    // Write: withdraw() — no inputs
    schema.methods[2].name          = "withdraw";
    schema.methods[2].description   = "Withdraws accumulated donations to the charity address.";
    schema.methods[2].inputs        = new FieldDescriptor[](0);
    schema.methods[2].outputs       = new FieldDescriptor[](0);
    schema.methods[2].approvals     = new ApproveAction[](0);
    schema.methods[2].isWriteMethod = true;
}
```

**Example — StakingVault with approve actions:**

```solidity
function vaultUISchema() public pure override returns (VaultUISchema memory schema) {
    schema.vaultType = "StakingVault";
    schema.description = "Stakes tax tokens or LP tokens to earn rewards.";
    schema.methods = new VaultMethodSchema[](2);

    // View: stakedBalance(address)
    schema.methods[0].name = "stakedBalance";
    schema.methods[0].description = "Returns the staked balance for a given user.";
    schema.methods[0].inputs = new FieldDescriptor[](1);
    schema.methods[0].inputs[0] = FieldDescriptor("user", "address", "The user address to query", 0);
    schema.methods[0].outputs = new FieldDescriptor[](1);
    schema.methods[0].outputs[0] = FieldDescriptor("balance", "uint256", "Staked token balance", 18);

    // Write: deposit(uint256) — requires ERC-20 approval of the tax token
    schema.methods[1].name = "deposit";
    schema.methods[1].description = "Stake tax tokens into the vault to earn rewards.";
    schema.methods[1].inputs = new FieldDescriptor[](1);
    schema.methods[1].inputs[0] = FieldDescriptor("amount", "uint256", "Amount of tax tokens to stake", 18);
    schema.methods[1].approvals = new ApproveAction[](1);
    schema.methods[1].approvals[0] = ApproveAction("taxToken", "amount");
    schema.methods[1].isWriteMethod = true;
}
```

**Example — NoteVault (string input, paginated array output):**

```solidity
function vaultUISchema() public pure override returns (VaultUISchema memory schema) {
    schema.vaultType   = "NoteVault";
    schema.description = "A public on-chain diary. Anyone can submit a note with an optional BNB tip. "
                         "All entries are stored on-chain and readable page-by-page.";

    schema.methods = new VaultMethodSchema[](3);

    // View: noteCount() — no inputs, plain uint256 output (raw count, decimals = 0)
    schema.methods[0].name        = "noteCount";
    schema.methods[0].description = "Total number of notes submitted.";
    schema.methods[0].inputs      = new FieldDescriptor[](0);
    schema.methods[0].outputs     = new FieldDescriptor[](1);
    schema.methods[0].outputs[0]  = FieldDescriptor("count", "uint256", "Total notes", 0);
    schema.methods[0].approvals   = new ApproveAction[](0);

    // View: getNotes(uint256 offset, uint256 limit) — paginated, returns Note[]
    // isOutputArray = true: outputs describe the fields of *each tuple* in the returned array,
    // not a single return value. The UI renders a table with one row per Note.
    schema.methods[1].name           = "getNotes";
    schema.methods[1].description    = "Fetch a page of notes. Use offset + limit to paginate.";
    schema.methods[1].inputs         = new FieldDescriptor[](2);
    schema.methods[1].inputs[0]      = FieldDescriptor("offset", "uint256", "Start index (0-based)", 0);
    schema.methods[1].inputs[1]      = FieldDescriptor("limit",  "uint256", "Max items to return",   0);
    schema.methods[1].outputs        = new FieldDescriptor[](3);
    schema.methods[1].outputs[0]     = FieldDescriptor("author",    "address", "Author address", 0);
    schema.methods[1].outputs[1]     = FieldDescriptor("createdAt", "time",    "Submitted at",   0);
    schema.methods[1].outputs[2]     = FieldDescriptor("content",   "string",  "Note content",   0);
    schema.methods[1].isOutputArray  = true;   // ← output is Note[], not a single tuple
    schema.methods[1].approvals      = new ApproveAction[](0);

    // Write: submitNote(string content) — string input + optional msg.value tip
    schema.methods[2].name          = "submitNote";
    schema.methods[2].description   = "Submit a note. Attach a BNB tip (optional, set 0 to skip).";
    schema.methods[2].inputs        = new FieldDescriptor[](2);
    schema.methods[2].inputs[0]     = FieldDescriptor("content", "string",    "Note content",      0);
    schema.methods[2].inputs[1]     = FieldDescriptor("tip",     "msg.value", "Optional BNB tip", 18);
    schema.methods[2].outputs       = new FieldDescriptor[](0);
    schema.methods[2].approvals     = new ApproveAction[](0);
    schema.methods[2].isWriteMethod = true;
}
```

### The Flap Guardian

The Flap Guardian is a privileged address that can always call permissioned functions in vault contracts that implement the Vault Specification. The Guardian serves as a backup mechanism to ensure that critical functions can be executed even if the primary authorized addresses are unable to do so.

At this moment, the Guardian is an empty but upgradeable contract managed by Flap:

```solidity
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.13;

/// @title FlapGuardian
/// @notice Guardian contract for vault emergency management and CTO cases
/// @dev Currently does nothing, but this contract is upgradeable.
contract FlapGuardian {
    function version() external pure returns (string memory) {
        return "0.0.1";
    }
}
```

Ideally, we don't need to implement any functionality in the Guardian contract. It just serves as a trusted address that can step in when necessary to protect users' funds.

{% hint style="warning" %}
If your vault has permissioned functions, you **must** grant the Guardian the same permissions. The Guardian's access **must not** be revocable by any other account — only the Guardian itself may renounce its own access.

When using OpenZeppelin's `AccessControl`, override `revokeRole()`:

```solidity
function revokeRole(bytes32 role, address account)
    public
    override
    onlyRole(getRoleAdmin(role))
{
    address guardian = _getGuardian();
    if (account == guardian) {
        revert CannotRevokeGuardianRole();
    }
    super.revokeRole(role, account);
}
```

{% endhint %}

### Adapter for legacy vaults

If you have built a vault to receive tax token revenue before the Vault Specification was introduced, you can still make your vault compatible with the VaultPortal system by creating an adapter contract that inherits from `VaultBase` and wraps around your existing vault. The adapter will implement the `description()` method and forward calls to your legacy vault as needed. This way, you can leverage VaultPortal features without modifying your original vault contract.

Please reach out to our team for assistance in creating an adapter for your legacy vault.

## VaultFactory Specification

A vault factory deploys vault instances for new tax tokens. When a user launches a token through the `VaultPortal`, the portal calls your factory's `newVault()` method to create the vault.

{% hint style="info" %}
**No registration required** — In V2, any contract that implements `VaultFactoryBaseV2` can be passed to the VaultPortal to launch tokens. You do not need to register your factory on-chain.
{% endhint %}

### IVaultFactory (V1)

The base interface that all vault factories must implement:

```solidity
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.13;

/// @title IVaultFactory
/// @notice Interface that all vault factory contracts must implement
interface IVaultFactory {
    error OnlyVaultPortal();
    error ZeroAddress();

    /// @notice Creates a new vault instance for a tax token
    /// @dev IMPORTANT: The taxToken does not exist yet when this method is called.
    ///      The VaultPortal predicts the token address and passes it here.
    ///      The actual token will be created AFTER the vault is created.
    /// @param taxToken The predicted address of the tax token (not yet deployed)
    /// @param quoteToken The quote token address (e.g., address(0) for native BNB)
    /// @param creator The original msg.sender to VaultPortal who initiated token creation
    /// @param vaultData Custom encoded data specific to this vault type
    /// @return vault The address of the newly created vault
    function newVault(address taxToken, address quoteToken, address creator, bytes calldata vaultData)
        external
        returns (address vault);

    /// @notice Checks if a quote token is supported by this vault factory
    /// @param quoteToken The quote token address to check
    /// @return supported True if the quote token is supported, false otherwise
    function isQuoteTokenSupported(address quoteToken) external view returns (bool supported);
}
```

**Key points:**

* Currently, only BNB (`quoteToken == address(0)`) is supported as the quote token, but future support for other quote tokens may be added.
* The `newVault` method is called by the VaultPortal when a new tax token is being created. The tax token does **not** exist yet when this method is called — the VaultPortal predicts the token address and passes it. The actual token is created **after** the vault.

### VaultFactoryBaseV2

`VaultFactoryBaseV2` implements `IVaultFactory` and adds a new abstract method: `vaultDataSchema()`. This allows the UI to discover what `vaultData` encoding the factory expects, and render the launch form accordingly.

```solidity
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.13;

import {IVaultFactory} from "./IVaultFactory.sol";
import {VaultDataSchema} from "./IVaultSchemasV1.sol";

/// @title VaultFactoryBaseV2
/// @author The Flap Team
abstract contract VaultFactoryBaseV2 is IVaultFactory {
    error UnsupportedChain(uint256 chainId);

    /// @notice Returns the schema describing the `vaultData` bytes expected
    ///         by this factory's `newVault()` method.
    /// @return schema The vault data schema for this factory.
    function vaultDataSchema() public pure virtual returns (VaultDataSchema memory schema);

    /// @notice Get the VaultPortal address for the current chain.
    function _getVaultPortal() internal view returns (address vaultPortal) {
        uint256 chainId = block.chainid;
        if (chainId == 56) {
            return 0x90497450f2a706f1951b5bdda52B4E5d16f34C06;
        } else if (chainId == 97) {
            return 0x027e3704fC5C16522e9393d04C60A3ac5c0d775f;
        }
        revert UnsupportedChain(chainId);
    }

    /// @notice Get the Guardian address for the current chain.
    function _getGuardian() internal view returns (address guardian) {
        uint256 chainId = block.chainid;
        if (chainId == 56) {
            return 0x9e27098dcD8844bcc6287a557E0b4D09C86B8a4b;
        } else if (chainId == 97) {
            return 0x76Fa8C526f8Bc27ba6958B76DeEf92a0dbE46950;
        }
        revert UnsupportedChain(chainId);
    }
}
```

**What changes from V1:**

* All V1 obligations still apply (`newVault()`, `isQuoteTokenSupported()`).
* You must additionally override `vaultDataSchema()` to return a `VaultDataSchema` describing the fields your factory expects in the `vaultData` parameter.
* The factory now provides `_getVaultPortal()` and `_getGuardian()` helpers.

**Example — Single tuple schema:**

```solidity
function vaultDataSchema() public pure override returns (VaultDataSchema memory schema) {
    schema.description = "Creates a staking vault. Specify a reward rate and lock duration.";
    schema.fields = new FieldDescriptor[](2);
    schema.fields[0] = FieldDescriptor("rewardRateBps", "uint16", "Annual reward rate in basis points", 0);
    schema.fields[1] = FieldDescriptor("lockDuration", "uint256", "Lock duration in seconds", 0);
    schema.isArray = false;
}
```

**Example — Array of tuples schema:**

```solidity
function vaultDataSchema() public pure override returns (VaultDataSchema memory schema) {
    schema.description = "Creates a vault that distributes received BNB "
        "among a dynamic set of payees by basis-point shares.";
    schema.fields = new FieldDescriptor[](2);
    schema.fields[0] = FieldDescriptor("payee", "address", "Payee wallet address", 0);
    schema.fields[1] = FieldDescriptor("bps", "uint16", "Basis points share (10000 = 100%)", 0);
    schema.isArray = true;  // vaultData = abi.encode((address,uint16)[])
}
```

**Example — Factory that ignores vaultData:**

```solidity
function vaultDataSchema() public pure override returns (VaultDataSchema memory schema) {
    schema.description = "Creates a vault with no configurable parameters. "
        "No user input is required — vaultData is ignored.";
    schema.fields = new FieldDescriptor[](0);
    schema.isArray = false;
}
```

### V2.1: Validation hook — `onBeforeNewTokenV6WithVault`

`VaultFactoryBaseV2` v2.1 adds an **optional** validation hook that VaultPortal calls immediately before creating a new V3 tax token. Factories that want to enforce constraints on token creation parameters (such as requiring a specific `dividendToken` or restricting `quoteToken` choices) should override this hook.

```solidity
function onBeforeNewTokenV6WithVault(IVaultPortalTypes.NewTokenV6WithVaultParams calldata params)
    external
    virtual
    returns (bool success, string memory reason)
{
    return (true, "");  // default: accept all params
}
```

**The hook is optional.** The default implementation always returns `(true, "")`. Factories that do not need to validate params do not need to override it.

**Return value contract** — VaultPortal calls this hook via a low-level call to distinguish three outcomes:

| Outcome                   | Meaning                  | VaultPortal action                        |
| ------------------------- | ------------------------ | ----------------------------------------- |
| Returns `(true, "")`      | Params are accepted      | Continue with token creation              |
| Returns `(false, reason)` | Params rejected          | Revert with `reason` as the error message |
| Reverts                   | Unexpected error in hook | Propagate the revert                      |

**Example — Enforce `dividendToken` equals `quoteToken`:**

```solidity
function onBeforeNewTokenV6WithVault(IVaultPortalTypes.NewTokenV6WithVaultParams calldata params)
    external
    override
    returns (bool success, string memory reason)
{
    address resolvedQuote = params.quoteToken == address(0)
        ? WBNB
        : params.quoteToken;

    if (params.dividendToken != resolvedQuote) {
        return (false, "Dividend token must equal the quote token.");
    }
    return (true, "");
}
```

{% hint style="info" %}
The hook receives the full `NewTokenV6WithVaultParams` struct so it can inspect any field — tax rates, quote token, dividend token, salt, etc. Override it whenever your vault type has requirements that the core protocol does not enforce.
{% endhint %}

### V2.1: Policy discovery — `tokenCreationPolicies`

To complement the validation hook, v2.1 adds two read-only methods so the UI can **discover** a factory's constraints before the user submits a transaction.

#### `factorySpecVersion()`

```solidity
function factorySpecVersion() public pure returns (string memory) {
    return "v2.1";
}
```

* Returns `"v2.1"` to signal that `tokenCreationPolicies()` is available.
* **Not virtual** — the spec version is fixed by the base contract and must not be overridden.
* The UI should call this via a low-level `staticcall`. A revert (or the absence of this selector) indicates a pre-v2.1 factory without policy discovery.

#### `tokenCreationPolicies()`

```solidity
function tokenCreationPolicies() public pure virtual returns (FactoryPolicy[] memory policies) {
    return new FactoryPolicy[](0);
}
```

Returns an array of `FactoryPolicy` structs describing the constraints this factory enforces. The default implementation returns an empty array (no declared policies).

**`FactoryPolicy` struct:**

```solidity
struct FactoryPolicy {
    string target;      // Field name in NewTokenV6WithVaultParams (e.g. "dividendToken")
    string operator;    // Comparison operator (see table below)
    bytes  value;       // ABI-encoded expected value
    string description; // Human-readable hint shown in the UI
}
```

**Supported operators:**

| Operator  | Meaning                                        |
| --------- | ---------------------------------------------- |
| `"eq"`    | Field must equal `value`                       |
| `"neq"`   | Field must not equal `value`                   |
| `"gt"`    | Field must be strictly greater than `value`    |
| `"gte"`   | Field must be ≥ `value`                        |
| `"lt"`    | Field must be strictly less than `value`       |
| `"lte"`   | Field must be ≤ `value`                        |
| `"in"`    | Field must be one of a set (ABI-encoded array) |
| `"notIn"` | Field must not be any of a set                 |

Unknown operators must be **ignored** by the UI (forward-compatible).

**How the UI uses policies:**

1. Call `factory.tokenCreationPolicies()` to get the policy array.
2. For each policy, decode `value` using the known ABI type for `target` (from `NewTokenV6WithVaultParams`).
3. Apply `operator` as a client-side validation rule on the corresponding input field and display `description` as an inline hint or error message.
4. Policies do **not** disable fields or block submission — they are advisory hints only. The actual enforcement always happens inside `onBeforeNewTokenV6WithVault`.

{% hint style="warning" %}
Policies are **informational only**. Always implement `onBeforeNewTokenV6WithVault` as the authoritative enforcement gate. Policies without a corresponding hook have no effect on-chain.
{% endhint %}

**Worked examples:**

```solidity
// Example A — dividendToken must equal a specific address (WBNB)
FactoryPolicy({
    target:      "dividendToken",
    operator:    "eq",
    value:       abi.encode(address(0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c)),
    description: "Dividend token must equal the quote token (WBNB)."
})

// Example B — dividendBps must be at least 100 (1%)
FactoryPolicy({
    target:      "dividendBps",
    operator:    "gte",
    value:       abi.encode(uint256(100)),
    description: "Dividend BPS must be at least 100 (1%) for this vault type."
})

// Example C — quoteToken must be one of an allowed set
address[] memory allowed = new address[](2);
allowed[0] = WBNB;
allowed[1] = USDT;

FactoryPolicy({
    target:      "quoteToken",
    operator:    "in",
    value:       abi.encode(allowed),
    description: "Quote token must be WBNB or USDT."
})
```

### Recommended commission fee structure

Vault factories can charge a commission fee from the tax revenue. The fee structure must be clearly described in the vault's `description()` method. The recommended fee calculation is based on the tax rate (`taxRateBps`) and the received tax revenue (`msg.value`):

* If `taxRate` ≤ 1% (100 bps), the fee is **6%** of `msg.value`.
* If `taxRate` > 1%, the fee is `(msg.value * 6) / taxRateBps`.

```solidity
receive() external payable {
    if (msg.value == 0) return;

    if (taxRateBps == 0) {
        try ITaxToken(taxToken).taxRate() returns (uint256 _taxRate) {
            if (_taxRate > 0) {
                taxRateBps = _taxRate;
            }
        } catch {}
    }

    uint256 fee = 0;
    if (taxRateBps <= 100) {
        // 6% of msg.value if taxRate <= 1%
        fee = msg.value * 600 / 10000;
    } else {
        // Examples:
        //   1% (100 bps)  → 6%
        //   2% (200 bps)  → 3%
        //   3% (300 bps)  → 2%
        //  10% (1000 bps) → 0.6%
        fee = (msg.value * 6) / taxRateBps;
    }

    // your main logic — accumulate fee or send fee
}
```

## UI Schema Reference (IVaultSchemasV1)

The shared struct definitions in `IVaultSchemasV1.sol` power the automatic UI generation for both vault factories (token launch forms) and vaults (vault interaction pages).

### FieldDescriptor

The unified leaf type shared by both the factory schema and the vault UI schema:

```solidity
/// @notice Describes a single field (parameter, return value, or vault-data component).
struct FieldDescriptor {
    string name;        // Machine-readable name (e.g. "recipient", "bps", "amount")
    string fieldType;   // Solidity ABI type string (e.g. "address", "uint256", "string")
    string description; // Human-readable explanation shown as label/tooltip
    uint8 decimals;     // Decimal precision hint for numeric fields
}
```

**Supported `fieldType` values:**

| `fieldType`   | UI widget        | Notes                                                                                                               |
| ------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------- |
| `"string"`    | Text input       |                                                                                                                     |
| `"address"`   | Address input    | With checksum validation                                                                                            |
| `"uint16"`    | Number input     | 0–65535                                                                                                             |
| `"uint256"`   | Big-number input |                                                                                                                     |
| `"uint128"`   | Number input     |                                                                                                                     |
| `"bool"`      | Checkbox         |                                                                                                                     |
| `"bytes"`     | Hex input        |                                                                                                                     |
| `"bytes32"`   | Hex input        | 32 bytes                                                                                                            |
| `"time"`      | Date/time picker | Alias for `uint256`; value is a Unix timestamp in seconds. Displayed as human-readable time or countdown in outputs |
| `"msg.value"` | Number input     | **Special Type**. Represents `tx.value` (wei). **Not** ABI-encoded. Only valid for write method inputs.             |

**`decimals` behaviour:**

* **Factory encoding (input):** If `decimals > 0`, the UI multiplies the user's input by $10^{\text{decimals}}$ before ABI-encoding. If `0`, the raw value is used.
* **Vault display (output):** If `decimals > 0`, the UI divides the raw on-chain value by $10^{\text{decimals}}$ for display. If `0`, the raw value is displayed.
* For non-numeric fields (`string`, `address`, `bytes`, `bool`), `decimals` should always be `0`.

### VaultDataSchema

Returned by `VaultFactoryBaseV2.vaultDataSchema()`. Describes the shape of the `vaultData` bytes the factory expects:

```solidity
/// @notice Describes the shape of the `vaultData` bytes expected by a factory's `newVault()`.
struct VaultDataSchema {
    string description;          // Free-form explanation shown to the user
    FieldDescriptor[] fields;    // Ordered list of tuple components
    bool isArray;                // true = vaultData is abi.encode(tuple[])
}
```

**How the UI uses VaultDataSchema:**

1. Call `factory.vaultDataSchema()` to get the schema.
2. For each field in `fields`, render the appropriate input widget based on `fieldType`.
3. If `isArray == true`, render an "Add Item" button for dynamic array entries.
4. Encode the user input: if `isArray` use `abi.encode(tuple[])`; otherwise use `abi.encode(tuple)`.
5. The encoded bytes are passed as `vaultData` in `NewTaxTokenWithVaultParams`.

If `fields` is empty and `isArray` is false, the factory ignores `vaultData` entirely.

### FactoryPolicy

Returned by `VaultFactoryBaseV2.tokenCreationPolicies()`. Each struct describes one constraint the factory enforces on `NewTokenV6WithVaultParams`:

```solidity
/// @notice A single constraint on the parameters passed to newTokenV6WithVault.
struct FactoryPolicy {
    string target;      // Field name in NewTokenV6WithVaultParams
    string operator;    // "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "in" | "notIn"
    bytes  value;       // ABI-encoded expected value (type inferred from target field)
    string description; // Human-readable hint shown in the UI
}
```

See the [V2.1: Policy discovery](#v21-policy-discovery--tokencreationpolicies) section for the full operator table, encoding rules, and worked examples.

### VaultUISchema & supporting types

Returned by `VaultBaseV2.vaultUISchema()`. Describes the vault's entire UI surface:

```solidity
/// @notice An ERC-20 approve action the UI must execute before calling a write method.
struct ApproveAction {
    string tokenType;       // "taxToken" or "lpToken"
    string amountFieldName; // Name of the input field whose value is the approve amount
}

/// @notice Describes a single view or write method the UI should render.
struct VaultMethodSchema {
    string name;                   // Solidity method name
    string description;            // Human-readable explanation
    FieldDescriptor[] inputs;      // Ordered input parameters
    FieldDescriptor[] outputs;     // Ordered return values
    ApproveAction[] approvals;     // ERC-20 approvals required before a write
    bool isInputArray;             // true = input is tuple[]
    bool isOutputArray;            // true = output is tuple[]
    bool isWriteMethod;            // true = state-changing, false = view
}

/// @notice Top-level schema describing the vault's entire UI surface.
struct VaultUISchema {
    string vaultType;              // e.g. "FlapXVault", "SplitVault"
    string description;            // Overall explanation of the vault
    VaultMethodSchema[] methods;   // All methods the UI should render (ordered)
}
```

**How the UI uses VaultUISchema:**

1. Display `vaultType` as a badge/header and `description` as subtitle.
2. Always call `vault.description()` and display the result as a dynamic status banner (polled periodically).
3. For each method:
   * **View methods** (`isWriteMethod == false`): If no inputs, call immediately and display results. If inputs exist, render input fields with a "Query" button.
   * **Write methods** (`isWriteMethod == true`): Render a form with inputs. If `approvals` is non-empty, execute each `ApproveAction` before sending the write transaction.
4. Methods are displayed in the order returned.

**ApproveAction workflow:**

1. Resolve the token address from `tokenType`: `"taxToken"` → call `vault.taxToken()`, `"lpToken"` → call `vault.lpToken()`, unknown → skip (forward-compatible).
2. Read the amount from the user's input field named by `amountFieldName` (already scaled by `decimals`).
3. Check current allowance: `token.allowance(user, vault)`. If sufficient, skip.
4. Send `token.approve(vault, amount)` and wait for confirmation.

## Get your vault verified

Please reach out to our team if you want to get your vault or vault factory verified by us or our auditing partners.

Before reaching out, make sure:

* You have correctly implemented the Vault Specification (the `description()` method, the Guardian access to permissioned functions, and `vaultUISchema()` / `vaultDataSchema()` if using V2).
* Your factory contract is not upgradeable. If you want to upgrade your factory in the future, you must deploy a new factory contract and get it verified again.
* The commission fee structure is clearly described in the vault's `description()` method.
* You have passed some basic AI auditing tools — see the FAQ below.

{% hint style="warning" %}
**We strongly recommend using AI auditing tools before deploying your smart contracts and tokens.** This can help you resolve critical vulnerabilities before submitting for manual review.
{% endhint %}

## FAQ

### How to use AI to audit your smart contracts?

You can use any AI for auditing your smart contracts. For example, you can use [Google's AI Studio which is free to use](https://aistudio.google.com/prompts/new_chat?model=gemini-2.5-pro). The prompt is as follows:

{% code overflow="wrap" %}

```
You are a professional smart contract auditor. Generate an audit report for the following Solidity smart contract code. Identify potential vulnerabilities, code smells, questions, and best practice violations. Provide recommendations for improvements.

Return the result in markdown format and put the markdown in a code block.

========================

<Your Solidity Code Here>
```

{% endcode %}

This can help you identify common vulnerabilities and issues in your smart contracts. If your comments are clear enough, it will even help identify logical issues. However, AI tools are not perfect and may miss vulnerabilities or provide incorrect suggestions. Always review your smart contracts thoroughly before deploying them on mainnet.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.flap.sh/flap/developers/vault-developers/vault-and-vaultfactory-specification.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
