Vault & VaultFactory Specification
Table of Contents
Quick start
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.
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.
For a complete working example of a vault and vault factory implementation, check out the FlapVaultExample repository.
There are two generations of the vault specification, with a point release of the factory spec:
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:
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.).Implement
receive()— Accept BNB from the tax token and process the revenue according to your vault's logic.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 aVaultUISchemadescribing 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.
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():
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
newVaultmethod 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 aVaultDataSchemadescribing the fields your factory expects in thevaultDataparameter.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
onBeforeNewTokenV6WithVaultVaultFactoryBaseV2 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:
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)
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 contract — VaultPortal calls onBeforeLaunch(bytes) via low-level staticcall and interprets the result as follows:
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 keepingfactorySpecVersion()at the default"v2.2".Legacy factories that still depend on
NewTokenV6WithVaultParamsmay explicitly stay on V2.1 by overridingfactorySpecVersion()to return"v2.1"and implementingonBeforeNewTokenV6WithVault(...).
Concrete example — GiftV4VaultFactoryV2:
The current repo's src/GiftV4VaultFactoryV2.sol uses _validateBeforeLaunch(...) to enforce these product rules:
tokenVersion == TOKEN_V3_PERMITquoteToken == address(0)(native BNB only)dividendBps == 0dividendToken == address(0)buyTaxRate == 0sellTaxRate == 0
V2.2: Validation payload — LaunchValidationDataV1
LaunchValidationDataV1The 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 -> vaultBpsdeflationBps -> deflationBpsdividendBps -> dividendBpslpBps -> lpBpsdividendToken -> dividendTokenminimumShareBalance -> 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()
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 genericonBeforeLaunch(bytes)pathv2.1and below → treat as legacy / probeonBeforeNewTokenV6WithVault(...)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.
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.
V2.1+: Policy discovery — tokenCreationPolicies
tokenCreationPoliciesPolicy 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()
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:
"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:
Call
factory.tokenCreationPolicies()to get the policy array.For each policy, decode
valueusing the known ABI type fortargetfrom the normalized launch payload semantics.Apply
operatoras a client-side validation rule on the corresponding input field and displaydescriptionas an inline hint or error message.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+ factoriesonBeforeNewTokenV6WithVault(...)for legacy V2.1 factories
Policies are informational only. Always implement the actual enforcement in the corresponding validation hook. Policies without a corresponding hook have no effect on-chain.
Concrete example — GiftV4VaultFactoryV2:
The current src/GiftV4VaultFactoryV2.sol returns six policies matching its V2.2 validation logic:
tokenVersion == TOKEN_V3_PERMITquoteToken == address(0)dividendBps == 0dividendToken == address(0)buyTaxRate == 0sellTaxRate == 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:
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% ofmsg.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. If0, 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. If0, the raw value is displayed.For non-numeric fields (
string,address,bytes,bool),decimalsshould always be0.
VaultDataSchema
Returned by VaultFactoryBaseV2.vaultDataSchema(). Describes the shape of the vaultData bytes the factory expects:
How the UI uses VaultDataSchema:
Call
factory.vaultDataSchema()to get the schema.For each field in
fields, render the appropriate input widget based onfieldType.If
isArray == true, render an "Add Item" button for dynamic array entries.Encode the user input: if
isArrayuseabi.encode(tuple[]); otherwise useabi.encode(tuple).The encoded bytes are passed as
vaultDatainNewTaxTokenWithVaultParams.
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:
Display
vaultTypeas a badge/header anddescriptionas subtitle.Always call
vault.description()and display the result as a dynamic status banner (polled periodically).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. Ifapprovalsis non-empty, execute eachApproveActionbefore sending the write transaction.
Methods are displayed in the order returned.
ApproveAction workflow:
Resolve the token address from
tokenType:"taxToken"→ callvault.taxToken(),"lpToken"→ callvault.lpToken(), unknown → skip (forward-compatible).Read the amount from the user's input field named by
amountFieldName(already scaled bydecimals).Check current allowance:
token.allowance(user, vault). If sufficient, skip.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, andvaultUISchema()/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.
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.
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