Skip to content

Fees and rewards

Anyone can collect a vault’s fees. Each collection pays Blast its share (or holds it on a partnered vault) and pays every fee recipient. Of the holder allocation, Quote stays in the vault for reward rounds; Meme is paid to the governed FeePolicy.destination, recorded by HolderMemePaid.

  • Cetus: cetus::collect_fees<Meme, Quote, CoinA, CoinB>(vault, fee_policy, cetus_config, burn_manager, pool). pool is the vault’s Pool<CoinA, CoinB>, in the same coin order as migration.
  • Bluefin: bluefin::collect_fees<Meme, Quote>(vault, fee_policy, bluefin_config, pool, clock). pool is the vault’s Pool<Meme, Quote>.
const tx = new Transaction();
tx.moveCall({
target: `${PKG}::bluefin::collect_fees`,
typeArguments: [MEME, QUOTE],
arguments: [tx.object(VAULT), tx.object(FEE_POLICY), tx.object(BLUEFIN_CONFIG), tx.object(POOL), tx.object.clock()],
});

Another pool aborts with EPositionPoolMismatch. A collection of zero is valid and emits nothing. Neither adapter collects venue incentive rewards.

On a partnered vault, follow each collection with presale_vault::pay_protocol_lp_fees_with_partner(vault, fee_policy, partner, clock). Holding nothing is valid, so it can always follow a collection.

A team that adds liquidity to Cetus or Bluefin itself can lock that position for good in a Vault. The vault then splits LP fees exactly as a migrated sale’s vault does: Blast’s LP-fee share first, then the team’s fee split between its fee recipients and holder-reward rounds. Anyone may lock their own position.

  1. Create the pool and open and fund the position with the venue’s own calls.
  2. Build a fee_split(fee_recipients, holder_rewards_bps), as for a presale.
  3. Lock it:
    • Cetus: cetus::new_vault<Meme, Quote, CoinA, CoinB>(position, fee_split, partner_id, creation_policy, fee_policy, pool, burn_manager, clock). pool is the Pool<CoinA, CoinB> holding the position, with Meme and Quote in the pool’s order. The position is burned through Cetus LP-burn, and the vault keeps its fee-claiming proof.
    • Bluefin: bluefin::new_vault<Meme, Quote>(position, fee_split, partner_id, creation_policy, fee_policy, pool, clock). pool is the Pool<Meme, Quote> holding the position; a pool whose first coin is not Meme is not supported. The vault holds the position.
  • The CreationPolicy must allow the quote and the venue, the same check a presale passes.
  • The vault snapshots the FeePolicy’s current LP-fee rates, which Blast’s FeeAdmin may change later, as on any vault.
  • Its presale_id and presale_cap_id are the zero ID, so it has no residuals and no PresaleCap claims from it. VaultCreated carries the zero presale_id.
  • partner_id is an Option<ID>. A named partner must be one the CreationPolicy accepts now; Blast’s LP fees are then held for pay_protocol_lp_fees_with_partner, as on a partnered sale’s vault.
  • A position from another pool aborts with EPositionPoolMismatch; one with no liquidity aborts with presale_vault::EZeroLiquidity.
  • Fees are collected with the same collect_fees calls. Publishing holder-reward rounds still needs Blast’s RewardsAdmin witness.

When a sale’s pool is seeded, a little of either coin may not fit the position. The PresaleCap holder claims it once with presale_vault::claim_migration_residuals(vault, presale_cap), which returns (Coin<Meme>, Coin<Quote>). Transfer both in the same transaction. It aborts with ENoMigrationResiduals when both are empty.

The holder Quote share of LP fees accrues in the vault. Blast’s RewardsAdmin publishes rounds funded from the available holder balances. A round keeps its holder budget separate from an optional Quote reserve for gas reimbursement.

  • Publish: holder_rewards::publish_round(vault, witness, fee_policy, expected_treasury, number, start_checkpoint, end_checkpoint, root, manifest_digest, leaf_count, meme_amount, quote_amount, gas_reserve_amount, claim_deadline_ms, clock). witness is an AdminWitness<RewardsAdmin>. The round number must equal vault.next_round, starting at zero. Its checkpoint interval must be nonempty and begin at or after the previous round ended; skipped intervals need no empty round. Root and manifest digest are each 32 bytes, the leaf count is 1 to 65,536, and the holder budget must be positive. Holder funding plus reserve must fit the vault’s balances.

  • Treasury check: expected_treasury must still equal FeePolicy.destination when published, or the call aborts with ETreasuryChanged. The checked address is saved on the round. A zero gas reserve disables reimbursement and needs no settlement.

  • Claim: holder_rewards::claim(round, index, beneficiary, meme_amount, quote_amount, proof). Anyone can relay; payment goes to beneficiary. Each index pays once.

  • Settle gas reserve: holder_rewards::settle_gas_reserve(round, witness, vault, reimbursement_amount), by RewardsAdmin, once. It pays up to the reserved amount to the saved treasury and returns unused Quote to the vault. Holder claims never touch this reserve. Actual gas costs are determined off chain; the contract enforces the reserve bound.

  • Close: holder_rewards::close_round(round, vault, clock). Anyone, once now ≥ claim_deadline_ms. Returns unclaimed holder balances and any unsettled gas reserve to the correct vault and deletes the round. Claims work until the round is closed; passing the deadline alone does not close it.

Leaf digest, with the same node hash and proof rules as allowlists:

blake2b256( 0x00 || "blast-presale-holder-rewards-v1"
|| bcs(vault_id: ID) // 32 bytes
|| bcs(round_number: u64) || bcs(index: u64)
|| bcs(beneficiary: address)
|| bcs(meme_amount: u64) || bcs(quote_amount: u64) )

All amounts are atomic units. All rates are basis points out of 10,000.

Supply, raise, and every LP-fee split use one method. Each share gets floor(X × share ÷ 10,000). Leftover units go one each to the shares with the largest remainder, ties to the earlier share. Every unit lands in exactly one share.

  • Supply, in order: buyers, pool, Blast’s token rate, then token recipients.
  • Raise, in order: raise recipients, Blast’s raise rate, then the pool with what remains.

With T total commitments, R the raise target, A the buyers’ tokens, and c one wallet’s commitment:

  • Tokens: floor(A × c ÷ T)
  • Overflow refund: floor((T − R) × c ÷ T)
  • The last wallet to claim takes everything left of both coins, so no rounding dust is stranded.
  • Presale price: R ÷ A quote per atomic token. Pool opening price: pool quote ÷ pool tokens.
  • Under a hard cap, T equals R and every refund is zero. If the sale already overflowed before its hard cap started, the hard cap accepts nothing more and the overflow fills pro rata as above.

Show earlier claim previews as exact and the last one as “at least”.

Each sale snapshots FeePolicy’s rates at creation. Later set_fees calls never touch existing sales.

  • Raise fee and token fee: apportioned at creation, paid at migration. Overflow refunds are never charged.
  • LP fees: at each collection Blast takes ceil(gross × rate ÷ 10,000) of each coin, at the vault’s current rates. Blast’s FeeAdmin can change a vault’s rates after migration.
  • Every payout goes to FeePolicy.destination as it is when paid.
  • Before start_ms, Blast can replace a sale’s rates. A changed raise fee moves Quote to or from the pool; a changed token fee moves tokens to or from the buyers’ allocation. Recipients keep their amounts.

A partner’s cut is floor(fee × share_bps ÷ 10,000) of Blast’s raise fee and LP fees, never of the token fee, using the partner’s share at payment time (at most 8,000 basis points).

After Blast’s part, each coin is apportioned over the fee recipients in order, then the holder share. Ties favor fee recipients over holders and earlier recipients over later ones. Recipients are paid as coins; a zero amount sends nothing. Holder Quote stays in the vault for rounds, while holder Meme is paid to the governed fee-policy destination. The split itself never changes.