Skip to content

Latest commit

 

History

History
178 lines (130 loc) · 10.1 KB

File metadata and controls

178 lines (130 loc) · 10.1 KB

Pumpcade protocol — IDL facts

This file is derived from pumpcade_idl.json at the repo root, which is byte-identical to the on-chain Anchor IDL account BBmdk3qX3dnSTczwAsCFraUEX9WoL6BhZCi9eNP943CX (name:"pumpcade_protocol", version:"0.1.0", spec:"0.1.0"). The cade.market JS bundle does not embed the IDL — the client builds instruction data directly (see frontend.md) — so the bundle is not a cross-check for this file. Where the deployed binary diverges from the IDL, see Current IDL / binary divergences.

Verified: 2026-06-18 (devnet).

Anchor metadata

Field Value
name pumpcade_protocol
version 0.1.0
spec 0.1.0
description Created with Anchor

Instruction set

Seven protocol instructions plus standard Anchor IDL ops (IdlCreateAccount, IdlResize, IdlClose, IdlCreateBuffer, IdlWrite, IdlSetAuthority, IdlSetBuffer).

Instruction Effect IDL signer slot
initialize_factory One-shot; sets factory authority + treasury authority
configure_fees Sets protocol / creator / streamer fee bps. Total cap enforced by error 6015 authority
update_treasury Redirects treasury account authority
create_market Allocates Market PDA + token vault PDA authority
place_bet Transfers tokens from user → vault; deducts protocol / creator / streamer fees inline; writes UserPosition user
resolve_market Writes winning_side: bool onto the Market authority
claim_winnings Winner pulls payout from vault. Single-use per UserPosition.claimed user

The IDL marks each authority slot as signer: true but declares no relations, has_one, or address constraint pinning it to factory.authority. Any additional restriction is enforced inside the instruction handler — see trust-model.md for empirical verification on create_market.

Resolution and payout are decoupled: resolve_market records the winning side, then each winner independently calls claim_winnings.

Account labels (per-instruction, from IDL)

factory, authority, vault, token_vault, user_token_account, treasury_token_account, creator_token_account, streamer_token_account, user_position, system_program, token_program.

place_bet references the three fee receivers (treasury_token_account, creator_token_account, streamer_token_account) as separate token accounts.

PDA seeds

PDA Seeds
Factory ["factory"] (singleton)
Market ["market", market_id_u64_le] (arg)
TokenVault ["vault", market_pda]
UserPosition ["position", market_pda, user_pubkey]

market_id is supplied as a u64 argument to create_market. The Factory's market_count field is a separate on-chain counter; the program does not require the supplied market_id to equal it.

Account schemas

Factory (singleton)

8-byte Anchor discriminator
authority           pubkey
treasury            pubkey
market_count        u64
protocol_fee_bps    u16
creator_fee_bps     u16
streamer_fee_bps    u16
bump                u8

Market (one per market)

8-byte discriminator
market_id           u64
authority           pubkey
creator             pubkey
streamer            pubkey
token_mint          pubkey
token_vault         pubkey
description         string  (max 200 chars, enforced by error 6000)
creation_time       i64
end_time            i64
yes_pool            u64
no_pool             u64
yes_shares_total    u64
no_shares_total     u64
total_pool          u64
total_claimed       u64
resolved            bool
winning_side        Option<bool>
bump                u8

token_mint is per-market: each Market chooses its own SPL mint at creation. The program does not enforce a protocol-wide betting currency.

UserPosition (one per user × market)

8-byte discriminator
user                pubkey
market              pubkey
yes_shares          u64
no_shares           u64
yes_amount          u64
no_amount           u64
claimed             bool
bump                u8

claimed is the single-use guard for claim_winnings.

Error enum (from IDL)

Code Name Message
6000 DescriptionTooLong max 200 characters
6001 InvalidEndTime must be in the future
6002 InvalidAmount invalid bet amount
6003 MarketResolved already resolved
6004 MarketEnded no more bets allowed
6005 MarketAlreadyResolved already resolved (duplicate guard)
6006 MarketNotEnded not ended yet
6007 MarketNotResolved not resolved yet
6008 AlreadyClaimed winnings already claimed
6009 NoWinningPosition user has no winning position
6010 InvalidPayout invalid payout amount
6011 MathOverflow math overflow
6012 Unauthorized
6013 NoWinningBets no one bet on the winning side
6014 InsufficientVaultFunds vault can't pay all winners
6015 FeesTooHigh total fees > 10%
6016 InvalidTreasury
6017 InvalidCreator
6018 InvalidStreamer

There is no refund/rescue instruction in the IDL.

Events

The IDL events array is empty. The program emits human-readable Program log: strings only (Bet placed: …, Market <id> resolved. Winner: …, Winnings claimed: …, Factory initialized with authority: …, Fees configured: …, Market <id> created: <description> (custom_duration: …, expected_duration: …s)). Off-chain consumers must parse log strings rather than subscribe to Anchor events.

Observed economic behavior (on-chain, not IDL-declared)

The IDL declares accounts and instructions but not the arithmetic the place_bet/claim_winnings handlers perform. The following are verified read-only on devnet.

Fees taken upfront

place_bet deducts the three Factory fees from the bet before the remainder enters the pool. Verified on tx 5dUTpL…Zr4 (market 55116): a 100,000,000 bet credited 99,000,000 to the vault and 1,000,000 (1% = the 45+10+45 bps total) to the fee receiver. On devnet the treasury/creator/streamer token accounts collapse to the single admin key, so the 45/10/45 split is not separable in transfers — it rests on the Factory bps fields (see on-chain-state.md).

Time-weighted shares

A bet mints fewer shares than its token amount, scaled by how early it lands in the round. Across 89 positions sampled on market 55114, the per-position shares / amount ratio spans 0.2001 → 1.0000 with none above 1.0 — consistent with a 1.0× multiplier for the earliest bets decaying toward ~0.2× near the deadline. Only the 1.0× ceiling and ~0.2× floor are visible from position data; the exact multiplier function was later recovered from the deployed bytecode — see ../reverse-engineering/. Pumpcade's own docs describe the same shape at a high level (docs.cade.market). The sampled market in on-chain-state.md shows the same effect (yes 127.71M amount → 110.02M shares ≈ 0.86×).

Payout formula (principal-protected parimutuel)

claim_winnings pays a winner their principal back plus a proportional cut of the losing pool:

payout = principal + floor(your_winning_shares × losing_pool / total_winning_shares)

Verified to the exact lamport on market 55114 (NO won): 3,960,000 + floor(793,574 × 3,096,720,000 / 1,264,291,840) = 5,903,757, matching the claim tx BW6Pna…nw4g. Winners never lose principal; the losing side funds the winners' profit. The inputs are the Market fields yes_pool/no_pool/yes_shares_total/no_shares_total and UserPosition.{yes,no}_shares.

Current IDL / binary divergences

The on-chain IDL is admin-rewritable. Two divergences between the IDL and the deployed BPF binary are observable from live transactions and direct testing. Both matter for any client that builds instructions or maps errors from the IDL:

create_market instruction data

The IDL lists five arguments (market_id u64, description string, end_time i64, creator pubkey, streamer pubkey), which sum to 88 + len(description) bytes after the 8-byte discriminator. Live transactions of this instruction carry 8–9 additional trailing bytes beyond that. Submitting an instruction with only the five IDL-declared args fails inside the program with Anchor error 102 (InstructionDidNotDeserialize: The program could not deserialize the given instruction); appending nine zero bytes (0x00 0x00 … 0x00) allows deserialization to proceed. The exact types of the trailing fields are not declared in the IDL; the program's emitted Market <id> created log line names custom_duration and expected_duration, consistent with two additional fields after the IDL-declared ones.

Sample range observed (1.75 h window, 23 create_market instructions): total instruction-data byte sizes from 125 to 155.

Error code 6013 label

The IDL labels code 6013 as NoWinningBets with message No one bet on the winning side. The BPF binary emits Error Code: Unauthorized. Error Number: 6013 for this code in transactions where the instruction handler rejects an unauthorized caller (see trust-model.md for a reproduction). Client code that maps error codes by IDL name will misclassify this case.

Reproducing the divergence checks

  • IDL byte-equivalence. getAccountInfo on BBmdk…943CX returns 3,544 bytes laid out as Anchor IDL account: 8-byte discriminator + 32-byte authority + 4-byte u32 LE data_len + zlib-compressed JSON. Decompressing yields the same JSON as pumpcade_idl.json.
  • create_market trailing bytes. Decode the data field of any recent create_market instruction (filter program instructions whose first 8 bytes equal 67e261ebc8bcfbfe); the byte length will exceed 88 + len(description).
  • Error 6013 label. Construct a create_market instruction signed by any keypair other than factory.authority and submit it via sendTransaction with skipPreflight=true; the resulting transaction's log includes Error Code: Unauthorized. Error Number: 6013.