Skip to main content

Vaults

A tax token collects a slice of every buy and sell. That slice goes to a fee receiver. In the plain tax-token launch the fee receiver is a wallet. In a vault launch the fee receiver is a contract deployed at launch time, so the revenue lands somewhere with rules: split it across wallets, stream it to one recipient, hold it for an identity, or anything else a vault author codes.

This page is for integrators who want to launch with a vault or read one. If you want to write your own vault, see custom-vault.md.

Contents​

How revenue reaches a vault​

   trader ──buy/sell──▶ tax token
│ collects tax on the transfer
▼
settles the tax ← permissionless; anyone can poke it
│ splits by the allocation you set at launch
┌──────────────┼───────────────┬───────────────┐
▼ ▼ ▼ ▼
fee receiver holders burn liquidity
(the vault) (dividends) (0xdead) (adds LP)

The token does not push revenue on every trade. It accrues tax and settles in batches. When it settles, the fee receiver slice is sent to the vault. Your vault records the revenue and pays it out later, on its own distribute() / claim() calls. This batching is why a vault always has an "accrue now, pay out later" shape.

CheesePad's 10% platform fee on the creator tax is a separate charge, not part of your allocation; see the token fee model for how it fits alongside the tax.

The four allocation slices come from the allocation you set in taxConfig at launch, and they must sum to 10000 BPS:

SliceWhere it goes
feeReceiverthe vault
holderstoken holders, as dividends (needs a non-zero minHolderShare)
burnthe dead address
liquidityadded back as LP

For a vault to ever get paid, allocation.feeReceiver must be above zero and at least one tax side must be non-zero. The launchpad checks this and reverts the launch otherwise.

Launching with a vault​

Use createMicroSaleTaxTokenWithVaultV2. On top of the usual CreateParamsV2, you pass a vault factory address, an ABI-encoded vaultData blob that configures the specific vault, and the dexKey naming the DEX to list on.

function createMicroSaleTaxTokenWithVaultV2(
CreateParamsV2 calldata params, // same struct as the tax-token launch, incl. taxConfig
address vaultFactory, // a factory the platform trusts (see Factory addresses)
bytes calldata vaultData, // ABI-encoded config for this vault; shape depends on factory
address dexKey // the DEX to list on
) external payable returns (address fundManager);

What the launchpad does, in order, all fail-closed (any failure reverts the whole launch):

  1. Confirms the factory points at this launchpad.
  2. Confirms the factory accepts the sale's quote token.
  3. Confirms the factory understands the current launch-data revision.
  4. Calls the factory's review hook with your launch parameters; it must approve.
  5. Has the factory deploy the vault.
  6. Confirms the vault's revenue mode matches the quote token (see below).
  7. Binds the vault as the token's fee receiver.

You never wire anything up yourself. If the factory approves, you get a live token whose tax flows into a fresh vault.

The same handshake backs a bonding curve tax token: createBondingCurveTaxTokenWithVaultV2 takes a vaultFactory, vaultData, and dexKey and runs the identical checks. The constraint is that the vault's revenue mode must match the curve's quote currency — native mode for a BNB curve, ERC20 mode for an ERC20-quoted curve (see Native vs ERC20 revenue). Everything below applies to both launch styles.

Encoding vaultData​

vaultData is opaque to the launchpad; each factory defines its own shape. A factory advertises that shape through vaultDataSchema(), so a UI can build the form generically. To encode by hand, follow the schema's field order and types. Concrete examples for the two shipped vaults are below.

Getting the vault address back​

After launch, read it from the sale:

const sale = await launchpad.getMicroSaleByFundManager(fundManager);
console.log("vault:", sale.vault); // the deployed vault
console.log("via factory:", sale.vaultFactory); // the factory that produced it

The sale also records which factory produced the vault, so an off-chain allowlist can badge vaults that came from a trusted factory.

Native vs ERC20 revenue​

A vault handles revenue in exactly one asset, decided at launch:

  • Native mode when the sale's quote token is WBNB. Revenue arrives as BNB.
  • ERC20 mode otherwise. Revenue arrives as the quote token, and stray native coins are rejected.

The launchpad asserts that the vault's mode matches the quote token, so a mismatched factory cannot brick trading. For an ERC20-quote launch, contributors raise in that ERC20 quote token too — see microsale.md. The quote token comes from the template binding, so a wallet-receiver or no-tax launch against an ERC20-bound template raises in that quote token as well; the difference with a vault is only where the fee-receiver slice lands.

The vaults CheesePad ships​

Two ready-made vaults, each with its own factory. You point createMicroSaleTaxTokenWithVaultV2 at the factory and pass the matching vaultData.

SplitterVault: fixed payees by share​

Splits revenue across a fixed list of wallets by basis points. Up to 50 payees, shares must sum to 10000, no duplicates, no zero addresses.

vaultData is abi.encode(address[] payees, uint16[] bps):

const payees = ["0xAaa...", "0xBbb..."];
const bps = [7000, 3000]; // 70% / 30%; must sum to 10000

const vaultData = ethers.AbiCoder.defaultAbiCoder().encode(
["address[]", "uint16[]"],
[payees, bps]
);

await launchpadRW.createMicroSaleTaxTokenWithVaultV2(params, SPLITTER_FACTORY, vaultData, dexKey, {
value: creationFee,
});

Using the vault after launch:

  • distribute() is permissionless. It credits each payee's share of the accrued balance and pushes it to them. If a payee cannot receive (a reverting contract, for example), their share is kept as owed for them to pull later, and the others still get paid.
  • claim() lets a payee pull their own owed balance.
  • splitConfig() returns the payees and their bps; owed(address) returns a payee's pending balance; credited() returns the running total credited.

SplitterVault accepts any quote token.

GiftVault: stream to the wallet behind an X handle​

Holds a token's revenue for the person behind an X (Twitter) handle. Nobody is paid until a wallet is bound to the handle, and the bind is authorized by a platform relayer signature, so only the real owner of the handle can claim.

vaultData is abi.encode(string xHandle, uint256 timeoutPeriod):

const vaultData = ethers.AbiCoder.defaultAbiCoder().encode(
["string", "uint256"],
["cheesepad", 30n * 24n * 60n * 60n] // handle, and 30-day bind window
);

await launchpadRW.createMicroSaleTaxTokenWithVaultV2(params, GIFT_FACTORY, vaultData, dexKey, {
value: creationFee,
});

timeoutPeriod is between 1 and 365 days. The GiftVault factory also requires allocation.holders to be above zero, because of the fallback below.

How it plays out:

  • Bind. The wallet owner gets a relayer-signed proof (an EIP-712 signature over the handle, the token, the beneficiary wallet, a nonce, and an expiry) and calls bindBeneficiary(beneficiary, nonce, validUntil, signature). From then on, revenue streams to that wallet. Binding is open only until the timeout, and only once the proof checks out.
  • Withdraw. After binding, distribute() pushes accrued revenue to the beneficiary, and claim() lets them pull it.
  • Lock. The beneficiary can call lock() to cut the relayer and platform out of any future rebind.
  • Rescue. Before the timeout and before a lock, the platform backstop can correct a wrongly-bound wallet. It only redirects future revenue.
  • Dividend fallback. If nobody binds before the timeout, anyone can call distributeDividend(), which routes the unclaimed revenue to the token's holders as dividends. This is why the factory insists on a holder slice. Once it fires, the vault is permanently in dividend mode and binding is closed.

Reading a vault generically​

Every vault describes its own read fields and actions through vaultUISchema(). A UI can render a vault it has never seen: it reads the schema, calls each read method, and decodes the result using the declared types. The common shape:

const vault = new ethers.Contract(sale.vault, VaultABI, provider);

const isNative = await vault.isNativeRevenue(); // true = BNB, false = ERC20
const quote = await vault.quoteToken(); // revenue asset
const desc = await vault.description(); // one-liner for listings
const schema = await vault.vaultUISchema(); // methods to render (reads + writes)

vaultUISchema() returns a vaultType, a description, and a list of methods. Each method is either a read field (the UI calls it and shows the typed result) or a write action (the UI renders a button and an input form, plus any ERC20 approvals it needs first). The schema types are covered in custom-vault.md and events-and-data.md.

The important part for a generic reader: a vault only surfaces what its schema declares. There is no automatic storage introspection, so trust the schema as the list of everything worth showing.

Factory addresses​

VaultNetworkFactory
SplitterVaultBNB (56)0x9d04B0Bfe281411d4A01a69BD89f7Ba6b7F5A59b
SplitterVaultBNB testnet (97)0xbB5165684832a8Cda1121081EBb168ca6636abc6
SplitterVaultRobinhood (4663)0x58DBab8C3529492c12Ee913313Cf600f522a39D9
GiftVaultBNB (56)0xFa36D3e2240BEDB22bad35D8FD4165AAf0598896
GiftVaultBNB testnet (97)0x8227F4330E3aa43c94b9fDc409D71A6C6C2033B7
GiftVaultRobinhood (4663)0x7FD34AeA57250aC66C59448339316aF745f6B9ef

A factory is stateless and immutable. A new version is a new address, so confirm the current one with the CheesePad team before you automate launches against it.