Persistent storage: the structs and storage keys (Item/Map) that hold the contract’s state between calls.
Live source from contracts/launchpad/src/state.rs (425 lines). Generated from the deployed contract code.
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Addr;
use cw_storage_plus::{Item, Map};

#[cw_serde]
pub struct Config {
    /// v7: master switch for presale creation. When false, CreateToken
    /// with a presale config is rejected. Flip via UpdateConfig.
    #[serde(default)]
    pub presales_enabled: bool,
    /// AMM contract address (for graduation)
    pub amm_contract: Addr,
    /// CW20 base code ID (for instantiating tokens)
    pub cw20_code_id: u64,
    /// Creation fee in ubwick (80,000 BWICK = 80_000_000_000 ubwick)
    pub creation_fee: u128,
    /// Graduation threshold in ubwick (5,000,000 BWICK = 5_000_000_000_000 ubwick)
    pub graduation_threshold: u128,
    /// Buy fee in basis points (50 = 0.5%)
    pub buy_fee_bps: u16,
    /// Sell fee in basis points (350 = 3.5%)
    pub sell_fee_bps: u16,
    /// Creator's share of fees in basis points (e.g., 2000 = 20%)
    pub creator_fee_share_bps: u16,
    /// Admin address authorized to update oracle price and config
    #[serde(default = "default_admin")]
    pub admin: Addr,
    /// Target graduation market cap in micro-USD (6 decimals). Default: $10,000 = 10_000_000_000
    #[serde(default)]
    pub target_graduation_usd: u128,
    /// Minimum graduation threshold in ubwick. Default: 100,000 BWICK = 100_000_000_000
    #[serde(default)]
    pub min_graduation_threshold: u128,
    /// Maximum graduation threshold in ubwick. Default: 50,000,000 BWICK = 50_000_000_000_000
    #[serde(default)]
    pub max_graduation_threshold: u128,
    /// Target starting market cap in micro-USD. Default: $1,000 = 1_000_000_000
    #[serde(default)]
    pub target_starting_mc_usd: u128,
    /// Target total USD raised to graduate in micro-USD. Default: $2,000 = 2_000_000_000
    #[serde(default)]
    pub target_raised_usd: u128,
    /// BWICK-denominated graduation target in ubwick. When > 0, this overrides
    /// the USD-derived dynamic threshold (no oracle dependency). Still clamped
    /// to [min_graduation_threshold, max_graduation_threshold].
    #[serde(default)]
    pub target_raised_bwick: u128,
    /// Max wallet holding in basis points of total supply (300 = 3%). 0 = disabled.
    #[serde(default = "default_max_wallet_bps")]
    pub max_wallet_bps: u16,
    /// Address exempt from creation fee (e.g. auto-deploy bot). Empty = nobody exempt.
    #[serde(default = "default_fee_exempt")]
    pub fee_exempt_address: Addr,
    /// Optional address of the bwick-oracle contract. When set, BWICK/USD price for
    /// USD-denominated graduation logic is read live from the oracle's TWAP query
    /// instead of the legacy admin-set ORACLE_STATE. Empty = use the legacy local oracle.
    #[serde(default = "default_oracle_contract")]
    pub oracle_contract: Addr,
    /// Cooldown between reuses of a (normalized) name or symbol. 0 = use default.
    #[serde(default)]
    pub name_cooldown_seconds: u64,
    /// v4: code ID of the bwick-vesting CW contract. Used by
    /// `FinalizePresale` to instantiate a per-creator vesting account
    /// when `presale.creator_allocation_bps > 0`. Zero = vesting
    /// disabled (FinalizePresale rejects creator-allocation presales
    /// with `VestingNotConfigured`).
    #[serde(default)]
    pub vesting_code_id: u64,
}

fn default_admin() -> Addr {
    Addr::unchecked("")
}

fn default_fee_exempt() -> Addr {
    Addr::unchecked("")
}

fn default_oracle_contract() -> Addr {
    Addr::unchecked("")
}

fn default_max_wallet_bps() -> u16 {
    300
}

#[cw_serde]
pub struct OracleState {
    /// BWICK/USD price in micro-USD (6 decimals). E.g., $2.00 = 2_000_000
    #[serde(rename = "bwick_usd_price")]
    pub bwick_usd_price: u128,
    /// Block height of last price update
    pub last_update_height: u64,
    /// Timestamp (seconds) of last price update
    pub last_update_timestamp: u64,
}

#[cw_serde]
pub struct TokenMetadata {
    pub name: String,
    pub symbol: String,
    pub image: String,
    pub description: String,
    pub social_links: Vec<String>,
}

#[cw_serde]
pub struct Curve {
    /// CW20 token address (set after instantiation)
    pub token_address: Addr,
    /// Token metadata
    pub metadata: TokenMetadata,
    /// Creator address
    pub creator: Addr,
    /// Total tokens sold (starts at 0)
    pub tokens_sold: u128,
    /// Total BWICK accumulated in reserves
    pub bwick_reserves: u128,
    /// Whether curve has graduated
    pub graduated: bool,
    /// Block height when created
    pub created_at: u64,
    /// Total fees earned by creator
    pub creator_fees_earned: u128,
    /// Per-curve graduation threshold (ratcheted -- only increases).
    /// None for legacy curves created before v2.0.
    pub graduation_threshold_ubwick: Option<u128>,
    /// Virtual BWICK reserves at curve start (ubwick). 0 = use legacy hardcoded constants.
    #[serde(default)]
    pub virtual_bwick_start: u128,
    /// Virtual token reserves at curve start (utokens). 0 = use legacy hardcoded constants.
    #[serde(default)]
    pub virtual_tokens_start: u128,
    /// Constant product invariant K = virtual_bwick * virtual_tokens.
    #[serde(default)]
    pub curve_k: u128,
    /// Tokens available for purchase on this curve (utokens).
    #[serde(default)]
    pub tokens_on_curve: u128,
    /// Tokens reserved for LP at graduation (utokens).
    #[serde(default)]
    pub tokens_for_lp: u128,
    /// v4: LP lock duration (seconds from graduation). Forwarded to
    /// the AMM `CreatePool` so the locked LP seed has an unlock
    /// timestamp. `None` or `Some(0)` = permanent lock (current
    /// behavior pre-v4).
    #[serde(default)]
    pub lp_lock_seconds: Option<u64>,
}

pub const CONFIG: Item<Config> = Item::new("config");
pub const CURVES: Map<&Addr, Curve> = Map::new("curves");
/// Track pending token instantiation (token_addr not known yet)
pub const PENDING_CURVE: Item<PendingCurve> = Item::new("pending_curve");
pub const ORACLE_STATE: Item<OracleState> = Item::new("oracle_state");
/// Track last creation time for each normalized symbol (lowercase + strip non-alphanumeric).
/// Keys are the *normalized* form, so case / hyphens / spaces all collide.
pub const SYMBOL_COOLDOWN: Map<&str, u64> = Map::new("symbol_cd");
/// Track last creation time for each normalized name (lowercase + strip non-alphanumeric).
pub const NAME_COOLDOWN: Map<&str, u64> = Map::new("name_cd");
/// Permanent ban: once a curve graduates, its normalized name + symbol are
/// blocked forever. Anti-vampire: an attacker can't fork a successful
/// graduated token's branding even after the cooldown expires.
pub const NAME_PERMA_BLOCK: Map<&str, u64> = Map::new("name_block");
pub const SYMBOL_PERMA_BLOCK: Map<&str, u64> = Map::new("symbol_block");

/// Default reuse cooldown (3 hours). Configurable via UpdateConfig.
/// Short enough to allow honest re-pivots after a failed launch; long
/// enough that a copycat can't immediately fork a successful token's
/// branding the moment the original rugs/expires.
pub const DEFAULT_NAME_COOLDOWN_SECS: u64 = 3 * 3600;

/// Normalize a name/ticker for collision detection. Lowercases ASCII and
/// strips anything that isn't a letter or digit. This catches "BWICK",
/// "bwick", "B-W-I-C-K", "BWICK V2" → `bwickv2` etc, so an attacker can't
/// fork a successful token's branding by adding a separator.
pub fn normalize_for_cooldown(input: &str) -> String {
    input
        .chars()
        .filter(|c| c.is_ascii_alphanumeric())
        .map(|c| c.to_ascii_lowercase())
        .collect()
}

// ── Metadata voting (v3) ──────────────────────────────────────────────────
//
// Token holders weighted by current CW20 balance can vote on each metadata
// field (name / symbol / image / description). The contract maintains a
// running tally so the "currently winning" value for each field is O(1)
// to query. An off-chain (or in-dApp) sweeper calls ApplyMetadata every
// METADATA_APPLY_INTERVAL_SECS (default 600s) which snaps the winning
// value into the canonical metadata if it differs.
//
// Vote weight = CW20 balance snapshot at vote time. Voters can re-vote
// to refresh their snapshot or change their pick; the old weight is
// subtracted from the previous tallies before the new one is added.

/// Initial cooldown before the first apply for a token. After the first
/// apply, subsequent vote changes update applied metadata inline (no
/// cooldown — whoever's currently winning wins live).
pub const METADATA_FIRST_APPLY_DELAY_SECS: u64 = 600;
/// Total voting window per token (48h). After this, VoteMetadata +
/// ApplyMetadata are refused — metadata is locked.
pub const METADATA_VOTING_WINDOW_SECS: u64 = 48 * 3600;

#[cw_serde]
#[derive(Default)]
pub struct MetadataVote {
    /// The voter's pick for the name field, if any. None = no opinion.
    pub name: Option<String>,
    pub symbol: Option<String>,
    pub image: Option<String>,
    pub description: Option<String>,
    /// CW20 balance snapshotted at vote time.
    pub weight: u128,
}

#[cw_serde]
pub struct AppliedMetadata {
    pub name: String,
    pub symbol: String,
    pub image: String,
    pub description: String,
    /// Unix seconds — last time ApplyMetadata ran and possibly changed something.
    pub last_applied_at: u64,
    /// Unix seconds — when metadata voting opened for this token (set on
    /// curve create OR lazy-init for pre-v3 curves). Voting closes at
    /// `created_at_secs + METADATA_VOTING_WINDOW_SECS`.
    #[serde(default)]
    pub created_at_secs: u64,
    /// Unix seconds — first time anyone cast a non-empty vote on this
    /// token. 0 = no vote yet, the first-apply countdown hasn't started.
    /// METADATA_FIRST_APPLY_DELAY_SECS runs from this timestamp, not from
    /// token creation, so a sleepy token doesn't auto-snap to its own
    /// initial metadata 10 min after launch.
    #[serde(default)]
    pub first_vote_at: u64,
    /// Number of completed apply ticks. 0 = no apply yet (first one is
    /// gated behind METADATA_FIRST_APPLY_DELAY_SECS from first_vote_at).
    /// > 0 = subsequent vote changes take effect inline at vote time.
    #[serde(default)]
    pub apply_count: u64,
    /// Social links — voted on independently. Lazy-init parses
    /// curve.metadata.social_links by position [0]=twitter,[1]=telegram,
    /// [2]=website (the dApp convention since CreateTab). On apply we
    /// rebuild that Vec from these three fields, filtering empties.
    #[serde(default)]
    pub twitter: String,
    #[serde(default)]
    pub telegram: String,
    #[serde(default)]
    pub website: String,
}

/// Per-token per-voter MetadataVote.
pub const METADATA_VOTES: Map<(&Addr, &Addr), MetadataVote> = Map::new("meta_votes");

/// Sum of vote weights for (token, field, value). Keys are (token, "field:value").
/// We pack the field+value into a single &str key because cw-storage-plus
/// composite keys for owned Strings are noisier than this serialization.
pub const METADATA_TALLIES: Map<(&Addr, &str), u128> = Map::new("meta_tally");

/// Currently-canonical metadata per token. Set on token create from the
/// CreateToken args; updated by ApplyMetadata when voting majority differs.
pub const APPLIED_METADATA: Map<&Addr, AppliedMetadata> = Map::new("meta_applied");

#[cw_serde]
pub struct PendingCurve {
    pub metadata: TokenMetadata,
    pub creator: Addr,
    pub initial_bwick: u128,
    #[serde(default)]
    pub virtual_bwick_start: u128,
    #[serde(default)]
    pub virtual_tokens_start: u128,
    #[serde(default)]
    pub curve_k: u128,
    #[serde(default)]
    pub tokens_on_curve: u128,
    #[serde(default)]
    pub tokens_for_lp: u128,
    #[serde(default)]
    pub graduation_threshold_ubwick: u128,
    /// v4: optional presale config attached at CreateToken time. The
    /// pending curve carries it across the cw20 instantiation reply.
    /// `None` means classic flow (curve opens immediately).
    #[serde(default)]
    pub presale: Option<PresaleConfig>,
    /// v4: optional creator-vesting config. Funded after presale ends
    /// (or immediately on graduation if no presale).
    #[serde(default)]
    pub creator_vesting: Option<CreatorVestingConfig>,
    /// v4: optional LP-lock duration for the eventual AMM pool. Seconds
    /// from graduation time. `None` or 0 = permanent lock (current
    /// behavior).
    #[serde(default)]
    pub lp_lock_seconds: Option<u64>,
}

// ── Presale (v4) ──────────────────────────────────────────────────────────
//
// Presale is per-token. When CreateToken sets `presale: Some(cfg)`, the
// curve is created in `presale_pending` state and ALL public Buy/Sell
// is rejected until `FinalizePresale` runs. Presale buyers send BWICK
// + a Merkle proof; tokens transfer to them instantly (no buyer-side
// vesting per design). At end_time or hard-cap, anyone calls
// FinalizePresale which:
//   1) (optionally) instantiates a bwick-vesting account for the creator
//   2) seeds the AMM... wait no — seeds the BONDING CURVE with leftover
//      tokens + raised BWICK
//   3) sets `presale.finalized = true`, opening Buy/Sell
//
// The unsold-token math at finalize:
//   sold_during_presale = presale.tokens_sold
//   creator_allocation = total_supply * creator_allocation_bps / 10000
//   leftover_for_curve = total_supply - sold_during_presale - creator_allocation
// Raised BWICK becomes the curve's initial `bwick_reserves`, which means
// the existing graduation logic carries over with no special-case.

#[cw_serde]
pub struct PresaleConfig {
    /// Price in ubwick per utoken (6 decimals applied to both sides).
    /// e.g. 1_000_000 ubwick / utoken == 1 BWICK per 1 token.
    pub price_ubwick_per_utoken: u128,
    /// Hard cap on BWICK raised during presale. Once `bwick_raised`
    /// reaches this, further buys reject. Doubles as the soft cap
    /// "trigger" for an early finalize.
    pub hard_cap_ubwick: u128,
    /// Per-wallet cap (ubwick contributed). `None` = no per-wallet cap.
    pub per_wallet_cap_ubwick: Option<u128>,
    /// Unix seconds — earliest BuyPresale accepted.
    pub start_time: u64,
    /// Unix seconds — latest BuyPresale accepted. After this, anyone
    /// can call FinalizePresale even if hard cap wasn't hit.
    pub end_time: u64,
    /// SHA-256 Merkle root of allowlisted addresses. Leaf = sha256(addr_bytes).
    /// Empty `[0u8; 32]` is treated as "no allowlist" (open presale).
    pub merkle_root: [u8; 32],
    /// Basis points of total supply reserved for creator vesting.
    /// 0 = no creator allocation. Capped at MAX_CREATOR_ALLOCATION_BPS
    /// (default 2000 = 20%) at validation time.
    #[serde(default)]
    pub creator_allocation_bps: u16,
    /// v7 (MetaDAO-style): minimum BWICK that must be raised by end_time.
    /// If unmet at finalize, the presale fails and every contributor can
    /// claim a full refund. 0 = no minimum.
    #[serde(default)]
    pub min_raise_ubwick: u128,
    /// v7 (MetaDAO-style): when true, contributions keep being accepted
    /// past the hard cap. At finalize only `hard_cap_ubwick` is accepted,
    /// pro rata across contributors; the surplus is refundable at claim.
    #[serde(default)]
    pub allow_oversubscription: bool,
}

#[cw_serde]
pub struct PresaleState {
    pub config: PresaleConfig,
    /// Total BWICK contributed during the presale window.
    pub bwick_raised: u128,
    /// Total tokens transferred out to presale buyers.
    pub tokens_sold: u128,
    /// True after FinalizePresale runs. Curve Buy/Sell unblocks here.
    pub finalized: bool,
    /// v7: true when the presale closed below min_raise_ubwick. All
    /// contributions become refundable via ClaimPresale.
    #[serde(default)]
    pub failed: bool,
    /// v7: true for presales created under the escrow model (tokens are
    /// held by the launchpad until ClaimPresale after finalize). Legacy
    /// presales (false) transferred tokens at buy time; their claims are
    /// rejected to prevent double payout.
    #[serde(default)]
    pub escrowed: bool,
    /// Set on finalize when `creator_allocation_bps > 0`. Address of
    /// the freshly instantiated bwick-vesting contract.
    pub creator_vesting_contract: Option<Addr>,
}

#[cw_serde]
pub struct CreatorVestingConfig {
    /// Cliff length in seconds, counted from presale end_time (or from
    /// curve creation if no presale). 0 = no cliff.
    pub cliff_seconds: u64,
    /// Total vesting duration in seconds, counted from the same anchor.
    /// `duration_seconds <= cliff_seconds` means a pure cliff drop.
    /// `cliff_seconds == 0` means pure linear.
    pub duration_seconds: u64,
}

/// Hard upper bound on creator allocation. The protocol-level cap so a
/// creator can't reserve everything for themselves. Configurable via
/// UpdateConfig in a future migration; baked in for v4.
pub const MAX_CREATOR_ALLOCATION_BPS: u16 = 2000; // 20%

/// Per-token presale state. Created at CreateToken if presale config
/// was provided. After `finalized = true` it's read-only history.
pub const PRESALES: Map<&Addr, PresaleState> = Map::new("presales");

/// (token_address, buyer) -> cumulative ubwick contributed during presale.
/// Used for per-wallet cap enforcement. Not pruned post-finalize so the
/// UI can show per-buyer history.
pub const PRESALE_CONTRIBUTIONS: Map<(&Addr, &Addr), u128> = Map::new("presale_contrib");
/// v7: (token, buyer) -> true once ClaimPresale paid out (tokens or refund).
pub const PRESALE_CLAIMED: Map<(&Addr, &Addr), bool> = Map::new("presale_claimed");

/// Per-token vesting instance address, mirror of `PresaleState.creator_vesting_contract`.
/// Stored separately so queries don't need to load the whole presale state.
pub const VESTING_INSTANCES: Map<&Addr, Addr> = Map::new("vesting_instances");

// Reply IDs are declared inline in contract.rs: `REPLY_CW20_INSTANTIATE = 1`
// (existing) and `REPLY_VESTING_INSTANTIATE = 2` (v4 new).

/// Stash data needed to finish FinalizePresale across the vesting-
/// instantiation reply. token_address keys it.
#[cw_serde]
pub struct PendingFinalize {
    pub token_address: Addr,
    pub creator_allocation_utokens: u128,
    pub vesting_funding_utokens: u128,
}

pub const PENDING_FINALIZE: Item<PendingFinalize> = Item::new("pending_finalize");