Vault & VaultFactory Specification

Table of Contents

Quick start

  1. Follow the example — Work through the FlapVaultExample repository 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 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.

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.

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:

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 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.

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 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.

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.

Example — Hypothetical DonationVault:

Example — StakingVault with approve actions:

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

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:

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.

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.

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.

IVaultFactory (V1)

The base interface that all vault factories must implement:

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.

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:

Example — Array of tuples schema:

Example — Factory that ignores vaultData:

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.

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:

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.

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:

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

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 contractVaultPortal 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"

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().

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

V2.2: Validation payload — LaunchValidationDataV1

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

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:

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.

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()

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

FactoryPolicy struct:

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

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:

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.

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:

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:

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.

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 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:

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.

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. The prompt is as follows:

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.

Last updated