Vouchers
A voucher is a pre-funded right to buy a BattleMeme token at the current pool price, redeemable by a specific person (or a whitelisted wallet). The campaign creator locks ETH up-front; recipients redeem on-chain; the contract auto-swaps the ETH into tokens and forwards them.
Vouchers are independent of the battle lifecycle. They don’t queue tokens, don’t affect scoring, don’t consume any battle ETH. They’re a marketing / airdrop primitive built on top of the pool — that’s it.
Why vouchers exist
Most launchpads handle airdrops with off-chain spreadsheets or pre-mints reserved for the team. Both are bad:
| Approach | Problem |
|---|---|
| Off-chain CSV airdrop | Single-tx mass-transfer drains gas / favors bots; no replay protection |
| Pre-mint reserved for team | Breaks fair launch; rug risk |
| ”Just buy and gift it” | Creator pays gas + slippage twice; no anti-bot guard |
Vouchers solve all three:
- Pre-funded ETH is locked in
VoucherManager, not on the creator’s hot wallet. - One redeem per recipient is enforced on-chain (
hasClaimedInCampaignmap for Link mode,whitelistRedeemedByfor Whitelist mode). - Distribution is off-chain (DM / QR / email / whitelist file) → no public mempool race, no MEV bots.
- Token state is checked at redeem — vouchers can’t be redeemed when the token is QUEUE/WARMUP/RESTING/ELIMINATED/LOST. INIT, BATTLE, GRADUATED only.
Two modes
🔗 Link mode — ECDSA-signed vouchers
The creator picks a signer address (themselves, or a dedicated keypair on a server) and pre-signs one voucher per slot off-chain. Each signed message is a unique redeem ticket. Recipient pastes the link / scans the QR; the contract verifies the signature.
One-per-wallet enforcement. A wallet can only redeem one voucher per Link campaign. The signature also enforces single-use per slot. So mass-spamming the same link can’t drain a campaign.
Signature replay protection. Each digest binds (VoucherManager address, chainId, campaignId, voucherIndex), so the same signature can’t be replayed across networks or campaigns.
📋 Whitelist mode — Merkle proof
The creator publishes a Merkle root over (wallet, allocation) leaves. Whitelisted wallets self-redeem with a proof; the contract verifies. No signatures needed.
Leaf hash:
Total ETH locked: .
Per-wallet allocation. Unlike Link mode, a wallet may redeem multiple times in Whitelist mode — up to their declared allocation. So you can give a KOL 10 vouchers worth in a single tree entry.
When to use which mode
| Scenario | Mode |
|---|---|
| Generic airdrop link in your Telegram / Discord — first-comers win | Link |
| KOL marketing — each KOL gets 1 specific QR you DM them | Link |
| Pre-announced allowlist for token sale → followers self-redeem | Whitelist |
| Internal team / partner allocation with specific quotas | Whitelist |
| Anti-bot: don’t want the airdrop to be public-claimable on-chain | Whitelist |
| Want recipients to redeem before knowing if they’re picked | Link (sealed envelope vibe) |
Constraints
| Limit | Value | Why |
|---|---|---|
MIN_ETH_PER_VOUCHER | 0.001 ETH | Sweep economics; tiny vouchers aren’t worth gas |
MAX_VOUCHERS_PER_CAMPAIGN | 1000 | Bounds gas for cancellation paths |
| Eligible token states | INIT, BATTLE, GRADUATED | Frozen states (QUEUE, WARMUP, ELIMINATED, LOST) reject redeem |
| Per-wallet limit (Link) | 1 voucher per campaign | Anti-spray |
| Per-wallet limit (Whitelist) | Declared allocation | Set by creator at tree-build time |
Cancellation
The campaign creator can cancel un-redeemed vouchers any time:
cancelCampaign(campaignId, indexes[])— refund specific slots (idempotent; already-redeemed indexes silently skipped)cancelAll(campaignId)— fast-path; sets campaigncancelled = trueand refunds everything un-redeemed in one tx
Refund always goes to campaign.creator (the original deployer). Already-redeemed vouchers can’t be clawed back.
Token state guard
A campaign cannot be created or redeemed against a token in a non-tradable state. The contract checks hook.getState(poolId) at both create-time and redeem-time:
| State | Create? | Redeem? |
|---|---|---|
NONE | ❌ | ❌ |
INIT | ✅ | ✅ |
QUEUE | ❌ | ❌ |
WARMUP | ❌ | ❌ |
BATTLE | ✅ | ✅ |
RESTING | ❌ | ❌ |
ELIMINATED | ❌ | ❌ |
GRADUATED | ✅ | ✅ |
LOST | ❌ | ❌ |
This means if you set up a Whitelist campaign during INIT and the token enters QUEUE before everyone has redeemed, redemption is paused — they’ll have to wait for BATTLE or finalize.
Security model
- Signatures are EIP-191 personal-sign wrapping
keccak256(VoucherManager, chainId, campaignId, voucherIndex). Any wallet that caneth_sign/eth_personal_signcan be the campaign signer. - Merkle leaves use the OZ
StandardMerkleTreedouble-hash convention:keccak256(bytes.concat(keccak256(abi.encode(wallet, allocation)))). The contract exposeswhitelistLeaf(wallet, allocation)as a helper so off-chain builders match exactly. - No signature replay across networks. ChainId is baked into the digest.
- Slippage floor. Redeem requires
minTokensOut. Recipients (or their UI) should set this to avoid bad fills if the pool moves between sign-and-share and redeem.
How vouchers interact with battles
They don’t, directly. But:
- Voucher redeems credit
totalETHRaisedlike any normal buy — so a busy voucher campaign can push a token toward the 5 ETH queue threshold or push score during a battle round. - Voucher redeems are subject to the same 1.3% pool fee — the 0.3% creator share + 1.0% LP fee share apply normally.
- Voucher redeems pause when the token is frozen (QUEUE / WARMUP / RESTING / ELIMINATED / LOST) — they can’t bypass the trading freeze.
So you can totally use vouchers as a “rally minute” tool during WARMUP-prep — pre-mint a Whitelist allocation for your community, then drop the proof when WARMUP ends and BATTLE opens, to pump R1 score.
See also: Use vouchers (step-by-step UI guide), VoucherManager contract (full API).