# 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.2: Generic validation hook — `onBeforeLaunch(bytes)`](#v22-generic-validation-hook--onbeforelaunchbytes)
  * [V2.2: Validation payload — `LaunchValidationDataV1`](#v22-validation-payload--launchvalidationdatav1)
  * [V2.2: Spec version discovery — `factorySpecVersion()`](#v22-spec-version-discovery--factoryspecversion)
  * [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)                    | Legacy V6 validation hook + machine-readable policy discovery                          |
| V2.2    | *(no change)*                       | `VaultFactoryBaseV2` (current)                    | Wrapper-agnostic validation via `onBeforeLaunch(bytes)` and normalized launch payloads |

V2, V2.1, and V2.2 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. New factory implementations should generally target **V2.2**, which is the current default behavior of `VaultFactoryBaseV2` in the FlapTaxVaults repo.

## 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 a **legacy** validation hook that `VaultPortal` may call immediately before creating a V6 tax token. Factories that want to enforce constraints on the legacy `NewTokenV6WithVaultParams` wrapper can override this hook.

```solidity
function onBeforeNewTokenV6WithVault(IVaultPortalTypes.NewTokenV6WithVaultParams calldata params)
    external
    virtual
    returns (bool success, string memory reason)
{
    revert LegacyV6ValidationHookNotImplemented();
}
```

**Important current behavior:** in the current repo implementation, the base contract does **not** silently accept by default. It reverts with `LegacyV6ValidationHookNotImplemented()` so that new factories do not accidentally opt into the old V6-only validation surface.

**When this path is used:** `VaultPortal` only uses this hook for factories that are detected as **legacy V6 factories**. New V2.2 factories should use `onBeforeLaunch(bytes)` instead.

**Return value contract** — when the hook is implemented, `VaultPortal` calls it via a low-level call and interprets the result as follows:

| 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 with error data             | Unexpected error in hook                | Propagate the revert                      |
| Selector missing / empty returndata | Factory does not expose the legacy hook | Treat as “no legacy hook present”         |

**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 only when your factory is intentionally staying on the legacy V6 validation path.
{% endhint %}

### V2.2: Generic validation hook — `onBeforeLaunch(bytes)`

V2.2 introduces a **wrapper-agnostic** validation hook. Instead of coupling factory validation to a specific launch entrypoint such as `newTokenV6WithVault(...)`, `VaultPortal` now normalizes the launch parameters into a shared payload and calls the generic hook below:

```solidity
function onBeforeLaunch(bytes calldata validationData)
    external
    view
    virtual
    returns (bool success, string memory reason)
{
    LaunchValidationDataV1 memory data = abi.decode(validationData, (LaunchValidationDataV1));
    return _validateBeforeLaunch(data);
}
```

The default implementation is provided by `VaultFactoryBaseV2` itself. In most cases, your factory should **override `_validateBeforeLaunch(...)`**, not `onBeforeLaunch(...)` directly:

```solidity
function _validateBeforeLaunch(LaunchValidationDataV1 memory data)
    internal
    view
    virtual
    returns (bool success, string memory reason)
{
    data = data;
    return (true, "");
}
```

This makes factory validation independent from whether the user launched via V6, V7, or some future wrapper. `VaultPortal` is responsible for translating wrapper-specific params into the shared validation payload.

**Return value contract** — `VaultPortal` calls `onBeforeLaunch(bytes)` via low-level `staticcall` and interprets the result as follows:

| Outcome                             | Meaning                                                     | VaultPortal action                              |
| ----------------------------------- | ----------------------------------------------------------- | ----------------------------------------------- |
| Returns `(true, "")`                | Params are accepted                                         | Continue with token creation                    |
| Returns `(false, reason)`           | Params rejected                                             | Revert with `reason`                            |
| Reverts with error data             | Unexpected validation error                                 | Bubble up the revert                            |
| Selector missing / empty returndata | Factory claimed V2.2 but does not expose the hook correctly | Revert with `"Factory validation hook missing"` |

{% hint style="info" %}
`onBeforeLaunch(bytes)` is **read-only**. It is called via `staticcall`, so it should only inspect inputs and return pass/fail + reason. Any stateful setup still belongs in `newVault()`.
{% endhint %}

**When to use V2.2 vs V2.1:**

* **New factories should use V2.2** by overriding `_validateBeforeLaunch(...)` and keeping `factorySpecVersion()` at the default `"v2.2"`.
* **Legacy factories that still depend on `NewTokenV6WithVaultParams`** may explicitly stay on V2.1 by overriding `factorySpecVersion()` to return `"v2.1"` and implementing `onBeforeNewTokenV6WithVault(...)`.

**Concrete example — `GiftV4VaultFactoryV2`:**

The current repo's `src/GiftV4VaultFactoryV2.sol` uses `_validateBeforeLaunch(...)` to enforce these product rules:

* `tokenVersion == TOKEN_V3_PERMIT`
* `quoteToken == address(0)` (native BNB only)
* `dividendBps == 0`
* `dividendToken == address(0)`
* `buyTaxRate == 0`
* `sellTaxRate == 0`

```solidity
function _validateBeforeLaunch(LaunchValidationDataV1 memory data)
    internal
    pure
    override
    returns (bool success, string memory reason)
{
    if (data.tokenVersion != IPortalTypes.TokenVersion.TOKEN_V3_PERMIT) {
        return (false, "Gift V4 requires TOKEN_V3_PERMIT.");
    }
    if (data.quoteToken != address(0)) {
        return (false, "Gift V4 currently supports native BNB only.");
    }
    if (data.dividendBps != 0) {
        return (false, "Use tracker-only dividend mode. The vault decides when to deposit rewards.");
    }
    if (data.dividendToken != address(0)) {
        return (false, "Dividend claims should be paid in native BNB (wrapped internally by the Dividend contract).");
    }
    if (data.buyTaxRate != 0 || data.sellTaxRate != 0) {
        return (false, "Gift V4 requires buyTaxRate = sellTaxRate = 0%.");
    }
    return (true, "");
}
```

### V2.2: Validation payload — `LaunchValidationDataV1`

The generic V2.2 hook receives a normalized payload defined in `src/interfaces/IVaultFactory.sol`:

```solidity
struct LaunchValidationDataV1 {
    IPortalTypes.TokenVersion tokenVersion;
    address quoteToken;
    uint16 buyTaxRate;
    uint16 sellTaxRate;
    uint16 vaultBps;
    uint16 deflationBps;
    uint16 dividendBps;
    uint16 lpBps;
    address dividendToken;
    uint256 minimumShareBalance;
}
```

**Why this exists:** `newTokenV6WithVault(...)` and `newTokenV7WithVault(...)` do not expose fee data in the same shape. V6 passes separate top-level fields like `mktBps`, while V7 passes fee settings as `feeConfigs[]`. Rather than forcing every factory to understand multiple launch wrapper ABIs, `VaultPortal` converts both launch styles into the same semantic payload before validation.

**How `VaultPortal` builds the payload today:**

* For **V6 launches**, it maps:
  * `mktBps -> vaultBps`
  * `deflationBps -> deflationBps`
  * `dividendBps -> dividendBps`
  * `lpBps -> lpBps`
  * `dividendToken -> dividendToken`
  * `minimumShareBalance -> minimumShareBalance`
* For **V7 launches**, it scans `feeConfigs[]` and extracts the semantic equivalents for:
  * vault / marketing fee
  * deflation fee
  * dividend fee + dividend token + minimum share balance
  * LP fee

So from the factory's perspective, `_validateBeforeLaunch(...)` always sees the same shape regardless of which launch wrapper the user chose.

### V2.2: Spec version discovery — `factorySpecVersion()`

`VaultFactoryBaseV2` now exposes a version string that tells `VaultPortal` which validation generation the factory belongs to:

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

**Important differences from older V2.1-era docs:**

* In the current repo code, this method is **virtual**.
* The default return value is **`"v2.2"`**, not `"v2.1"`.
* Factories may override it to stay on an older validation generation, for example returning `"v2.1"` for a legacy V6-only factory.

**How `VaultPortal` interprets it:**

* `v2.2`, `v2.3`, `v3.0`, etc. → use the generic `onBeforeLaunch(bytes)` path
* `v2.1` and below → treat as legacy / probe `onBeforeNewTokenV6WithVault(...)`
* missing / reverting selector → probe for the legacy V6 hook directly

The parser uses **major/minor semantics**, so anything `v2.2+` counts as part of the normalized pre-launch validation generation.

{% hint style="warning" %}
Do not return `"v2.2"` unless your factory actually supports the generic validation path. If `VaultPortal` detects `v2.2+`, it will call `onBeforeLaunch(bytes)` and expect a valid `(bool,string)` response.
{% endhint %}

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

Policy discovery was introduced in V2.1 and remains part of V2.2. In the current repo, policies are best understood as **machine-readable UI hints** that mirror whichever validation hook the factory actually uses.

#### `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 normalized launch params (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 the normalized launch payload semantics.
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 happens inside the factory validation hook:
   * `onBeforeLaunch(bytes)` for V2.2+ factories
   * `onBeforeNewTokenV6WithVault(...)` for legacy V2.1 factories

{% hint style="warning" %}
Policies are **informational only**. Always implement the actual enforcement in the corresponding validation hook. Policies without a corresponding hook have no effect on-chain.
{% endhint %}

**Concrete example — `GiftV4VaultFactoryV2`:**

The current `src/GiftV4VaultFactoryV2.sol` returns six policies matching its V2.2 validation logic:

1. `tokenVersion == TOKEN_V3_PERMIT`
2. `quoteToken == address(0)`
3. `dividendBps == 0`
4. `dividendToken == address(0)`
5. `buyTaxRate == 0`
6. `sellTaxRate == 0`

This is a good pattern to follow: keep `tokenCreationPolicies()` and your real validation hook in sync so both the UI and on-chain behavior express the same product rules.

**Worked examples:**

```solidity
function tokenCreationPolicies() public pure returns (FactoryPolicy[] memory policies) {
    policies = new FactoryPolicy[](3);

    // Example A — dividendToken must equal a specific address (WBNB)
    policies[0] = 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%)
    policies[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;

    policies[2] = 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 the **normalized launch validation payload** used by the current factory spec.

```solidity
/// @notice A single constraint on normalized launch validation data.
struct FactoryPolicy {
    string target;      // Field name in normalized launch params
    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
}
```

For legacy v2.1 factories, these policies normally correspond to checks on `NewTokenV6WithVaultParams`. For v2.2+ factories, they normally correspond to checks on `LaunchValidationDataV1`. 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.
