For the complete documentation index, see llms.txt. This page is also available as Markdown.

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 Vault spec checker to automatically check that your implementation correctly follows this specification before deploying.

See the Quick start for Vault Developers page for the full onboarding checklist.

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

This page documents the current vault and vault factory interfaces — VaultBaseV3 (which extends VaultBaseV2, which extends VaultBase) and VaultFactoryBaseV2 targeting spec v2.3. We recommend all new vaults and factories target these latest interfaces — VaultBaseV3 (not VaultBaseV2) and factory spec v2.3 (not v2.2) — even if your vault only ever handles the native gas token today: declaring vaultQuoteToken() == address(0) costs nothing and future-proofs the vault for ERC20 quote support. Older revisions (VaultBase/V1, VaultBaseV2, and the v2.1/v2.2 factory validation surfaces) are fully backwards-compatible and are summarized in the Appendix at the end of this page — they remain supported, but new vaults and factories should target the interfaces documented below, not the legacy ones.

The Vault Specification

VaultBase / VaultBaseV2 / VaultBaseV3 (current interface)

To be compatible with the VaultPortal system, your vault smart contract must inherit VaultBaseV3 (which extends VaultBaseV2, which extends VaultBase) — this is the recommended, latest interface for all new vaults, regardless of whether your vault handles native or ERC20 quote revenue. This is the complete, current interface chain, pulled directly from FlapVaultExample and VaultBaseV3.sol — including the current per-chain Portal and Guardian addresses (BNB mainnet, BNB testnet, and Robinhood Chain):

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 revenue (BNB, or an ERC20 quote token if using VaultBaseV3) and process it according to your vault's logic. For VaultBaseV3 vaults, receive() must follow the balance-delta accounting model described below.

  3. Implement vaultUISchema() — Return a VaultUISchema describing every user-facing method your vault exposes, so the UI can automatically render an interaction page without custom code. See the UI Schema Reference section for the full struct definitions and worked examples.

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

  5. If inheriting VaultBaseV3 (recommended for all new vaults) — override vaultQuoteToken() to declare your revenue currency (address(0) for native), and follow the balance-delta accounting model in receive(). See VaultBaseV3 — ERC20 quote token support below for the full checklist.

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.

Why the Beacon Proxy pattern matters

For any factory that will deploy more than a handful of vault instances, we recommend building on the Beacon Proxy pattern (UpgradeableBeacon + BeaconProxy, upgrade authority gated to the Flap Guardian). See Building Upgradeable Vaults — Beacon Proxy Pattern for the full implementation guide.

The beacon proxy pattern serves two distinct purposes:

  1. Launch before audit, fix after. An audit is not required to launch a token (see Get your vault verified below) — but it, and the beacon, are both required if you want the low-risk badge. If you launch on a beacon proxy and later decide to pursue an audit — or your audit turns up a bug — the implementation behind the beacon can be upgraded to ship the fix, without redeploying or migrating any already-launched token. Without a beacon, a discovered bug means the vault is stuck as-is, and the vault cannot be verified in the first place.

  2. Static analysis has blind spots. An audit is fundamentally a static analysis of the code as written — it does not catch every issue that only manifests at runtime under real market conditions (unexpected call patterns, edge-case reentrancy, gas griefing under specific mempool conditions, etc.). The beacon proxy gives Flap a way to "save the day" and patch a vault that passed audit but still hit an issue in production, again without asking every token creator to migrate.

If your vault is a one-instance-per-token, immutable-for-life design and you don't intend to pursue verification, you can skip the proxy and inherit VaultBaseV3 directly. The beacon pattern is required for any vault pursuing the low-risk verification badge, and is the default recommendation for factories that will deploy many vault instances or anyone who wants to launch before completing an audit.

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.

VaultBaseV3 — ERC20 quote token support

Historically, vaults only supported the native gas token (BNB / ETH) as the revenue currency. That's a consequence of how receive() gets triggered: a native value transfer invokes the recipient's receive(), but an ERC20 transfer executes the token contract's code, not the recipient's — the vault would receive the funds but never get a chance to run its own logic.

VaultBaseV3 (extends VaultBaseV2) lifts that restriction so tokens quoted in an ERC20 — stablecoins, tokenized equities / RWA, or any other ERC20 — can use vaults too. It adds exactly two members on top of VaultBaseV2 — vaultQuoteToken() and vaultSpecVersion() — shown in the current interface above.

How the ping works

An ERC20 transfer never invokes the recipient's code, so a vault receiving ERC20 tax revenue would otherwise have no way to know new funds arrived. V3 closes that gap with a wake call ("ping"), sent by the protocol: after every ERC20 quote payout, the TaxProcessor follows up with wallet.call{value: 0}("") — an empty-calldata, zero-value call to the vault. This invokes the vault's receive() exactly like a native transfer would, giving the vault a chance to run its accounting logic even though no value accompanies the call itself.

The TaxProcessor's dispatch upholds four invariants — design your vault against them:

  1. Transfer first, ping after. By the time your receive() runs, the ERC20 revenue is already sitting in your balance. The ping is purely a wake-up signal, not the transfer itself.

  2. Ping failures are ignored. If your receive() reverts, dispatch proceeds without you — the funds still arrive, but your logic did not run, and you will only recognize the revenue on the next wake. Do not rely on reverting to refuse revenue.

  3. Ping gas is the dispatcher's, not yours. The protocol does not cap the wake call — it forwards the dispatch caller's remaining gas (EIP-150), and dispatch is permissionless. The protocol's keeper runs the entire dispatch (swaps, dividends, burn, up to four wallet payouts) under one fixed gas budget; a heavy receive() makes the keeper's dispatch of your token fail until someone re-dispatches with a higher gas limit. Keep receive() lightweight — update accounting, emit an event, return. Put heavy work (swaps, staking, buybacks) behind explicit public functions driven by your own automation or the protocol's keeper/trigger services.

  4. You may be pinged spuriously. The same wallet can occupy several payout slots and get pinged more than once per dispatch, and anyone can call your receive() at any time. A wake that finds no new revenue must be a silent no-op.

Native revenue is unchanged — it still arrives as a plain value transfer invoking receive(). A V3 vault handles both currencies through the same code path; the accounting model below is currency-agnostic.

Normative accounting model

Since a ping never carries an amount, a V3 vault must track revenue by balance-delta accounting: compare the vault's current quote-token balance against a stored baseline, and treat any increase as newly-recognized revenue.

Rules:

  1. Recognize by delta, never by raw balance. Crediting balanceOf(this) as "new revenue" double-counts everything already recognized. Only balance - accountedQuote is new.

  2. Zero-delta wakes are silent no-ops. Required by ping law 4 above.

  3. Every outflow MUST decrement accountedQuote by the amount spent, in the same transaction. Forgetting this leaves the baseline above the real balance, and bal <= accountedQuote then suppresses revenue recognition forever — the vault deadlocks. This is the single most dangerous mistake a V3 vault can make.

  4. Direct transfers are credited late, and that is fine. Funds sent to the vault outside dispatch (donations, airdrops, misdirected transfers) trigger no ping; they are recognized on the next wake and are indistinguishable from revenue. Treat them as donations.

  5. Recognize before you act. Any function whose behavior depends on available revenue — spending it, paying it out, or gating on it — must run the sync routine first, so its outcome never depends on whether a ping happened to arrive beforehand. Exposing a permissionless sync() wrapper is recommended: it is the neutral, side-effect-free recognition entry point for keepers and UIs, and the recovery path when revenue arrives without a wake call (direct transfers, or pings administratively disabled on the processor).

Implementing a V3 vault — checklist

  1. Inherit from VaultBaseV3. All obligations from VaultBase and VaultBaseV2 still apply (implement description() and vaultUISchema(), use _getPortal() / _getGuardian(), honor the Guardian mandate).

  2. Override vaultQuoteToken() to return the revenue currency this vault instance accounts for — address(0) for the native gas token, the ERC20 address otherwise. It must equal the quote token of the tax token this vault serves, must be immutable for the life of the vault (set it once at initialization from the factory's newVault parameters), and must not revert. VaultPortal cross-checks it at launch; lenses and UIs read it to label the vault and to warn when a vault is wired to a token with a different quote.

  3. Implement receive() with the balance-delta accounting model above. This is required even if your quote is an ERC20 — the wake call lands there.

  4. Keep vaultSpecVersion() at the default ("v3") unless a later spec revision instructs otherwise.

  5. On the factory side (see VaultFactoryBaseV2 below):

    • isQuoteTokenSupported(quote) must answer truthfully for your vault implementation. This is enforced on-chain at launch — the VaultPortal rejects any launch whose quote your factory does not support. Misdeclaring support strands your users' tax revenue in a vault that ignores it.

    • factorySpecVersion() must return "v2.3" (or higher) so the VaultPortal applies the V3 validation flow.

    • newVault(taxToken, quoteToken, creator, vaultData) receives the launch quote token — pass it into your vault's initializer.

    • Creation-time first buys in an ERC20 quote are funded by the launcher, not the vault: the VaultPortal pulls quoteAmt from the launching user (via an EIP-2612 permit signed with the VaultPortal as spender, or a prior approval) and refunds any partial fill. Frontends integrating your factory should request that approval or permit; the vault itself plays no part in launch funding.

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 — 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 / VaultFactoryBaseV2 (current interface)

This is the complete, current factory interface, pulled directly from FlapVaultExample — including the current per-chain VaultPortal and Guardian addresses (BNB mainnet, BNB testnet, and Robinhood Chain). We recommend all new factories override factorySpecVersion() to return "v2.3" (not the "v2.2" shown as the abstract base's default below) and implement IVaultFactoryDividendV23 — see Spec version discovery below:

Implementation requirements:

  • Implement newVault() and isQuoteTokenSupported() (from IVaultFactory).

  • Override vaultDataSchema() to describe the fields your factory expects in vaultData. See the UI Schema Reference section.

  • Override _validateBeforeLaunch(...) (not onBeforeLaunch directly) to enforce your factory's product rules.

  • If your factory (or the vaults it creates) exposes any privileged / role-gated functions, the Guardian must also be granted the required role(s) — see the Flap Guardian mandate above; the same rule applies to factories.

Generic validation hook — onBeforeLaunch(bytes)

VaultPortal normalizes launch parameters from whichever entrypoint the user called (newTokenV6WithVault, newTokenV7WithVault, ...) into the shared LaunchValidationDataV1 payload and calls onBeforeLaunch(bytes) via staticcall. In most cases you should override _validateBeforeLaunch(...), not onBeforeLaunch directly — the default onBeforeLaunch already handles decoding.

Return value contract:

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

Concrete example — GiftV4VaultFactoryV2 (from the current FlapTaxVaults repo) enforces:

Spec version discovery — factorySpecVersion()

factorySpecVersion() tells VaultPortal which validation generation the factory belongs to:

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

Policy discovery — tokenCreationPolicies()

Policies are machine-readable UI hints that mirror whichever validation hook the factory actually uses — they let the UI show inline validation before a transaction is sent, but they enforce nothing on-chain by themselves.

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

Worked example:

v2.3: Computed dividend tokens — resolveDividendToken

Factory spec v2.3 adds support for a computed dividend token — a dividend token whose address cannot be known before the tax token is deployed, such as a Uniswap/PancakeSwap LP pair or a child token deployed deterministically from the tax token address.

At launch time, the caller passes the sentinel value MAGIC_DIVIDEND_COMPUTED as dividendToken. VaultPortal then checks that the factory declares factorySpecVersion() >= "v2.3", calls resolveDividendToken(predictedToken, launchVersion, launchParams) on the factory, and substitutes the returned address as the real dividendToken before forwarding to Portal.

This is a fully optional, opt-in extension — factories that don't need computed dividend tokens can ignore resolveDividendToken/IVaultFactoryDividendV23 entirely. However, we still recommend declaring factorySpecVersion() = "v2.3" (rather than staying on "v2.2") even if you don't need computed dividend tokens, because factorySpecVersion() >= "v2.3" is also the gate VaultPortal checks before allowing a factory to launch VaultBaseV3 vaults with an ERC20 quote token — see VaultBaseV3 — ERC20 quote token support above. See Using an LP Token or Child Token as the Dividend Token for the full implementation guide, including the IVaultFactoryDividendV23 interface, worked examples for LP-pair and child-token resolution, the on-chain flow diagram, and the version-gating table.

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). This is the complete, current file:

How the UI uses VaultDataSchema (factory side):

  1. UI calls factory.vaultDataSchema() to get the schema.

  2. For each field in fields, render the appropriate input widget based on fieldType ("string" → text, "address" → address input with checksum, "uint16"/"uint256"/"uint128" → number input, "bool" → checkbox, "bytes"/"bytes32" → hex input, "time" → date/time picker).

  3. If isArray == true, render an "Add Item" button for dynamic array entries.

  4. Encode the user input: abi.encode(tuple[]) if isArray, else abi.encode(tuple). Pass the result as vaultData in NewTaxTokenWithVaultParams.

Example — Single tuple schema:

Example — Array of tuples schema:

Example — Factory that ignores vaultData:

How the UI uses VaultUISchema (vault side):

  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) call immediately if no inputs, otherwise render input fields with a "Query" button. Write methods render a form with inputs; if approvals is non-empty, execute each ApproveAction (resolve token via tokenType, check allowance, call approve if needed) before sending the write transaction.

  4. Methods are displayed in the order returned.

Example — Hypothetical DonationVault:

Example — StakingVault with approve actions:

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

Get your vault verified

Getting your vault verified is optional. You can launch a token with your vault today without any audit or verification — see the Quick start for Vault Developers guide. Verification only matters if you want the low-risk badge shown on Flap's website and partner sites.

To get your vault or vault factory verified, you must pass an audit from our partner audit company (current cost: 0.5 BNB / 0.16 ETH per factory). Please reach out to our team to schedule this.

Before reaching out, make sure:

  • You have correctly implemented the Vault Specification (the description() method, the Guardian access to permissioned functions, and vaultUISchema() / vaultDataSchema()).

  • Your vault and factory contracts are deployed behind the beacon proxy pattern, with upgrade authority gated exclusively to the Flap Guardian. This is a hard requirement for verification — Flap reserves the right to upgrade the vault if the audit (or a later production issue) surfaces a bug, so that a fix can ship without asking every token creator to migrate. A non-upgradeable factory/vault, or one where upgrade authority sits with any address other than the Guardian, cannot be verified. If you need to change a non-beacon 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.

Appendix: Older spec versions

The current interface (VaultBaseV3 and VaultFactoryBaseV2 targeting spec v2.3, documented above) is fully backwards-compatible with everything below. Existing vaults built against these older revisions continue to work unmodified — this section exists purely for historical reference. New vaults and factories should target VaultBaseV3 and factory spec v2.3, documented earlier on this page, not these older revisions.

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 (onBeforeNewTokenV6WithVault) + machine-readable policy discovery

V2.2

(no change)

VaultFactoryBaseV2 (updated)

Wrapper-agnostic validation via onBeforeLaunch(bytes) and normalized launch payloads — this became the default

V2.3

(no change)

VaultFactoryBaseV2 + IVaultFactoryDividendV23 (optional)

Computed dividend tokens via resolveDividendToken — see dedicated tutorial

V3

VaultBaseV3 (extends VaultBaseV2)

VaultFactoryBaseV2 (factorySpecVersion() >= "v2.3")

ERC20 quote token support via vaultQuoteToken() + the ping/balance-delta accounting model — see VaultBaseV3 — ERC20 quote token support above

V1 — VaultBase / IVaultFactory: The original spec. Vaults implement description() and the Guardian mandate; factories implement newVault() and isQuoteTokenSupported(). No UI schema discovery — every new vault type needed a hand-built UI.

V2.1 — legacy validation hook: Before the wrapper-agnostic onBeforeLaunch(bytes) hook existed, factories that wanted to validate launch parameters had to override onBeforeNewTokenV6WithVault(NewTokenV6WithVaultParams calldata params), called by VaultPortal only for V6-style launches:

VaultPortal interprets the low-level call result as: (true, "") → accept and continue; (false, reason) → revert with reason; revert with error data → propagate; selector missing / empty returndata → treat as "no legacy hook present". This path is still reachable today by overriding factorySpecVersion() to return "v2.1", for factories that intentionally stay on the legacy V6-only validation surface instead of adopting onBeforeLaunch(bytes).

Migrating from V1 to the current spec: extend VaultBaseV2 / VaultFactoryBaseV2 instead of VaultBase / IVaultFactory directly, then implement vaultUISchema() / vaultDataSchema(). No other changes are required — all V1 obligations still apply unchanged.

Last updated