Whitepaper

BUMS

A 1024-supply, fully on-chain pixel character and audio collection built on a hybrid ERC-20 / ERC-721 base, with a Uniswap v4 spray-point hook.

Network
Ethereum mainnet
Supply
1,024
Standards
ERC-20 + ERC-721 hybrid
Source of truth
launch/src/

1. Summary

BUMS is a 1,024-supply pixel character and audio collection. Each token is rendered as a 128×128 SVG and ships with a procedurally generated WAV signature, both encoded directly in the contract — no IPFS, no hosted reveal.

The collection is built on a hybrid ERC-20 / ERC-721 base, so every BUM is simultaneously a unique non-fungible token and a unit of fungible value. Half of the supply (512) starts wrapped as BUMS and seeds a Uniswap v4 pool plus a vault used for liquidity-backed mechanics. The other half (512) is a free public claim pool, drained one token at a time on a global cooldown.

Swap volume through the v4 pool is metered by a custom hook that awards spray points. Spray points subsidize the unwrap surcharge or pay for non-destructive rerolls, giving liquidity participants a recurring utility loop after launch.

2. Contracts at a glance

ContractRole
wavNFTHybrid ERC-20 / ERC-721 base class. BUMS is wavNFT.
BUMSHybrid token core. Free claims, rerolls, admin controls, per-token salt locking.
WrappedBUMSERC-20 wrapper contract. The wrapped token name and symbol are both BUMS.
BUMSMetadataTrait derivation, JSON metadata, image + audio URI assembly.
BUMSArtOn-chain SVG image helper using BumRenderer.
BUMSAudioOn-chain WAV helper using AudioSynth.
SprayPointsRegistryCanonical spray points ledger with an allowlist for earning hooks.
BumsSprayHookUniswap v4 hook that reports swap-earned spray points into the registry.
BumRendererPacked 4-bit 128×128 pixel SVG renderer (library).
BumPaletteFace, background, and hat palette helpers (library).

No ERC-2981 royalties are implemented.

3. Supply & token IDs

Total supply is fixed at 1,024. The hybrid base exposes NFT token IDs in the range 1025 .. 2048. On deployment, the BUMS contract owns the entire NFT supply. initialize() distributes them as follows:

ID rangeCountInitial destinationPurpose
1025 .. 1536512WrappedBUMS vaultBacks BUMS supply
1537 .. 2048512BUMS claim poolFree public claim inventory

Initialization mints 512 × 1e18 BUMS to the BUMS contract, which the deployment script then uses to seed the v4 pool as a single-sided BUMS sell wall — the position is placed entirely below the initial tick, so the pool starts with 0 ETH and 512 BUMS, then accumulates ETH as buyers arrive. No deployer ETH is paired into liquidity at launch.

4. Free claim pool

Free claims are handled by BUMS.claim(). Rules:

  • The claim pool starts with 512 tokens.
  • Each claim returns one random token from the remaining pool. Random index is uint256(blockhash(block.number - 1)) % claimPoolLength.
  • Per-token salt is locked before the transfer, so any indexer reading metadata after the claim sees the final identity.
  • One claim per wallet. Once an address has claimed, it cannot claim again. Tracked in hasClaimed[address]. The immutable deployer is exempt — see Section 5.
  • A randomized cooldown of 90–110 blocks (inclusive, ~20–25 minutes) gates the next claim. The cooldown is global, not per wallet — only one claim can clear per cooldown gate.
  • The cooldown roll mixes blockhash, claimer, tokenId, and remaining pool length. It is unpredictable but not user-controllable.
  • Claims stop when claimPoolLength == 0.

5. Claim-control switches

Two one-way admin switches replace the previous claim-cancellation mechanism. Each is irreversible once flipped, and neither moves any tokens directly — they only change which gates apply on subsequent claim() calls. This means the deployer can never sweep unclaimed supply in a single transaction.

onlyDeployerCanClaim()

  • Admin-only. Sets deployerOnly = true.
  • From this point on, only the immutable deployer address can call claim().
  • The inter-claim cooldown gate continues to apply, so the deployer drains remaining supply at the same pace as the public would have. To remove the cooldown gate as well, also call endCooldown().
  • Emits DeployerOnlyClaimsEnabled(admin).

endCooldown()

  • Admin-only. Sets cooldownEnded = true.
  • nextClaimBlock is no longer checked. Claims can clear back-to-back in the same block.
  • The per-wallet limit still applies, so non-deployer wallets remain capped at one BUM each.
  • Emits CooldownEnded(admin).

Combined effect: with endCooldown() alone, the remaining supply opens for a public free-for-all where each wallet may claim at most one. With both onlyDeployerCanClaim() and endCooldown() set, the deployer can drain the rest of the pool in rapid succession. There is no path that lets the deployer mass-sweep unclaimed supply in a single transaction.

6. BUMS, wrap, unwrap

WrappedBUMS is an 18-decimal ERC-20 wrapper for BUM NFTs. Its token name and symbol are both BUMS.

Wrap

wrap(tokenIds):

  • Transfers the listed BUM NFTs from the caller into the wrapper vault.
  • Mints 1 × 1e18 BUMS per NFT.
  • Requires NFT approval to the wrapper.

Standard unwrap

unwrap(count):

  • Burns count × 1e18 BUMS as redemption.
  • Burns an additional 10% surcharge (UNWRAP_BURN_BPS = 1000). Total burn is count × 1.1e18.
  • Releases count random NFTs from the wrapper vault.
  • Locks per-token salt on first user receipt.

The surcharge makes a wrap → unwrap round-trip cost 0.1 BUMS per NFT and over-collateralizes the vault relative to outstanding BUMS over time.

Spray-assisted unwrap

unwrapWithSpray(count, maxPointsToSpend):

  • Same random vault release as standard unwrap.
  • Burns the base count × 1e18 BUMS.
  • Spray points cover some or all of the 10% surcharge. Conversion rate: 10,000 points = 1 BUMS of surcharge coverage.
  • Points are capped by maxPointsToSpend, the user's available points, and the surcharge needed.
  • For one unwrap, the full 0.1 BUMS surcharge is fully covered by 1,000 points.

7. Rerolls

Rerolls keep the same token ID and locked salt but increment rerollNonce[tokenId]. Because the metadata seed includes the nonce, the rendered image and audio change while the token identity (ID + salt) stays stable.

BUMS reroll

BUMS.reroll(tokenId):

  • Caller must own tokenId.
  • Burns 1 × 1e18 BUMS from the caller.
  • Destroys one random vault token by transferring it to BURN_ADDR.
  • Increments rerollNonce[tokenId].
  • Emits Rerolled and MetadataUpdate.

Spray reroll

BUMS.sprayReroll(tokenId):

  • Caller must own tokenId.
  • Costs 10,000 spendable spray points.
  • Does not burn BUMS. Does not destroy a vault token.
  • Increments rerollNonce[tokenId].
  • Emits Rerolled and MetadataUpdate.

8. Spray points & v4 hook

BumsSprayHook is a Uniswap v4 hook with two active permissions: beforeInitialize and afterSwap. All other hook entrypoints (beforeAddLiquidity, etc.) are explicitly disabled and revert. The hook is swap-only by design, and it reports earned points into SprayPointsRegistry rather than holding balances itself.

Pool binding

On the first beforeInitialize call, the hook binds itself to one expected pool, checking:

  • Expected currency0 and currency1
  • Fee tier
  • Tick spacing
  • Hook address

Any other initialization reverts. After binding, only swaps in the bound pool can earn points.

Point award

After every swap, the hook computes absolute ETH-side volume and awards:

1,000 points per 1 ETH of ETH-side volume
ETH-side volumePoints
0.001 ETH1
0.1 ETH100
1 ETH1,000
10 ETH10,000

If hookData is exactly 32 bytes encoding a non-zero address, points credit to that beneficiary. Otherwise, points credit to the swap sender.

Balances and spending

SprayPointsRegistry tracks two balances per user:

  • lifetimePoints[user] — total earned, never decreases.
  • spendablePoints[user] — unspent balance.

Only approved earning hooks can call earn(...). Only BUMS and WrappedBUMS can call spend(...). This keeps point balances stable even if a future hook/pool replacement is approved.

Spend use cases:

Use caseIDCost
Unwrap surcharge coverage1variable, capped by surcharge
Spray reroll210,000 points

9. Token identity, salt, metadata

Seed

Each token's metadata seed is computed as:

keccak256(abi.encodePacked("Seed:", tokenId, effectiveSalt, rerollNonce))

effectiveSalt is:

  • tokenSalt[tokenId] once locked.
  • The global salt only for provisional preview before first claim or first vault unwrap.

Salt locking

Per-token salt locks on free claim, including deployer-only claim drains, or first unwrap from the wrapper vault. Salt preimage:

keccak256(abi.encodePacked(
  "BUMS-token-salt:",
  block.prevrandao,
  recipient,
  block.number,
  tokenId,
  address(this)
))

The address(this) term binds salts to the deploying contract instance. block.prevrandao is the post-Merge randomness source.

Stickiness

Once locked, a salt is sticky:

  • Direct ERC-721 transfers preserve it.
  • Wrap → unwrap round trips preserve it.
  • Rerolls preserve it; only rerollNonce[tokenId] increments.

Metadata refresh

Because the rendered seed changes when salt locks (preview → canonical), marketplaces that aggressively cache tokenURI can show a stale preview. Two public utilities exist for cache busting:

  • refreshMetadata(tokenId) — emits MetadataUpdate(tokenId) for one token.
  • refreshMetadataBatch(fromId, toId) — emits BatchMetadataUpdate(fromId, toId).

Both are public, not admin-gated. Anyone can ping a token to force marketplaces to re-fetch metadata. There is no spam vector beyond the caller's own gas. MetadataUpdate is also emitted automatically on every reroll.

10. Traits

The rendered image is a 128×128 on-chain SVG. Trait derivation reads bytes from the seed and from the audio mode pair (m, n):

TraitCardinalityDerivation
Hat shape2m % 2 == 0 ? Cap : Beanie
Eyes4n band: closed / XX / dots / sideeye
Mouth5seed[3] % 5
Face color3seed[8] % 3 (warm tones)
Hat color6seed[10] % 6 (neon pool)
Background color6(m + n) % 6 (neon pool)
Star count5seed[11] % 5 → 0..4 stars
Star layout8seed[12] % 8

Background and face are split by construction so the head is always legible against its field.

11. Audio

Each BUM has a procedurally generated WAV signature. The mode pair (m, n) is derived from the seed:

m = (seed[0] & 0x7) + 2     // range 2..9
n = (seed[1] & 0x7) + 2     // range 2..9
if (n == m) {
  if (n == 9) n = 2;
  else        n = n + 1;
}

The bass frequency for the pair is:

f_mn = 6.875 × (m² + n²)   Hz

For gas reasons, the contract stores f × 256 (i.e. 1760 × (m² + n²)) and divides on display.

Token metadata is fully on-chain, but split into a light default and a heavy on-demand variant:

  • tokenURI(tokenId) returns UTF-8 JSON with image + traits.
  • image is data:image/svg+xml;base64,....
  • animation_url is not included in default metadata — keeping marketplaces from inlining the WAV blob and slowing every render.
  • audioURI(tokenId) returns data:audio/wav;base64,.... The Studio reads this directly for live playback.
  • fullTokenURI(tokenId) returns the heavy image + audio version for high-gas read calls (explorers, archival, deep links).

No IPFS or hosted reveal is required for image or audio metadata.

12. Trust model & admin

BUMS is admin-mutable in narrow ways. There is no multisig handoff and no plan to renounce admin. The admin key is the deployer EOA and remains so indefinitely. This is the trust posture for the project's lifetime — users should evaluate that before participating.

BUMS admin powers

FunctionAccessCardinality
setInitialAddresses(wrapper, metadata)adminonce
initialize()adminonce
setMetadata(newMetadata)adminrepeatable
setSprayPoints(sprayPoints)adminonce
setAdmin(newAdmin)adminrepeatable
seedInitialLiquidity(...)adminonce
onlyDeployerCanClaim()adminonce (one-way)
endCooldown()adminonce (one-way)

Metadata mutability

setMetadata swaps the entire BUMSMetadata contract address. This is the upgrade path for trait derivation, image rendering, audio synthesis, and JSON assembly. After a swap, every token's image and audio output reflects the new metadata contract. The admin retains this power for the lifetime of the project.

WrappedBUMS

The wrapper has one admin-style power: setSprayPoints(...), callable only by the BUMS contract, only once. Wrap, unwrap, and spray-assisted unwrap are otherwise permissionless.

SprayPointsRegistry

The registry admin can approve or revoke earning hooks with setEarningHook(hook, allowed) and can transfer registry admin with setAdmin(newAdmin). Existing points remain in the registry if a new hook and pool are deployed later.

Public utilities

refreshMetadata and refreshMetadataBatch are public, not admin-gated. They exist as marketplace cache-busting utilities — see Section 9.

Not present

There is no royalty admin path. ERC-2981 is not implemented. There is no pause function. There is no upgrade proxy on BUMS or WrappedBUMS — only the metadata contract is hot-swappable.

13. Deployment

Primary deployment script: launch/script/DeployWithLP.s.sol. Sequence:

  1. Deploy BUMS. No ETH is attached at construction — the launch uses a single-sided BUMS LP, not an ETH-paired one.
  2. Deploy WrappedBUMS.
  3. Deploy BUMSArt.
  4. Deploy BUMSAudio.
  5. Deploy BUMSMetadata(bums, art, audio).
  6. Set wrapper and metadata on BUMS via setInitialAddresses.
  7. Distribute the supply via chunked initialization: 8 calls to BUMS.initializeChunk(64, 64) followed by BUMS.finalizeInitialize(). Chunked because a monolithic initialize() would exceed the post-Fusaka per-tx gas cap of 16,777,216 (EIP-7825).
  8. Deploy SprayPointsRegistry(admin, bums, wrapper).
  9. CREATE2-mine and deploy the v4 hook through the canonical CREATE2 deployer at 0x4e59…956C. The hook address low bits encode the required permissions: BEFORE_INITIALIZE | AFTER_SWAP = (1 << 13) | (1 << 6). Mining loop is bounded at 100,000 iterations.
  10. Configure BUMS and WrappedBUMS to spend from the registry.
  11. Approve the deployed hook as a registry earning hook.
  12. Initialize the v4 pool through the hook (which validates the pool binding).
  13. Mint a single-sided LP position from MIN_TICK = -887200 to MAX_TICK = 62400, escrowing 512 BUMS and 0 ETH. Because the position sits entirely below the initial tick (INIT_TICK = 62600), it acts as a pure BUMS sell wall — buyers swap ETH in, the pool accumulates that ETH inside the position. No ETH seed capital required from the deployer.
  14. Post-deploy: admin remains with the deployer EOA. There is no scripted handoff and no planned renouncement.

Launch constants

ItemValue
v4 fee tier1% (10,000 bps)
Tick spacing200
LP composition512 BUMS, 0 ETH (single-sided)
Initial tick62600 (≈ 525 BUMS / ETH)
LP tick range-88720062400 (entirely below initial tick)
LP liquidity22_611_997_192_205_801_033
Init-chunk size64 wraps + 64 claim-pool slots per call (8 chunks total)

External addresses

The deploy script targets Ethereum mainnet and references the canonical Uniswap v4, Permit2, and CREATE2 deployments:

PoolManager0x000000000004444c5dc75cB358380D2e3dE08A90
PositionManager0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e
Permit20x000000000022D473030F116dDEE9F6B43aC78BA3
CREATE2 deployer0x4e59b44847b379578588920cA78FbF26c0B4956C

Last updated: 2026-05-04. Primary truth: launch/src/, launch/test/, launch/script/DeployWithLP.s.sol. If anything in this document conflicts with the on-chain contracts, the contracts are final.