The logic: the instantiate/execute/query entry points and every handler behind them. This is where the rules actually run.
Live source from contracts/launchpad/src/contract.rs (3985 lines). Generated from the deployed contract code.
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{
    entry_point, to_binary, Addr, BankMsg, Binary, Coin, Deps, DepsMut,
    Env, MessageInfo, Reply, Response, StdResult, SubMsg, Uint128, WasmMsg,
    StdError,
};
use cw_storage_plus::Item;
use cw2::set_contract_version;
use cw_utils::parse_instantiate_response_data;

use crate::error::ContractError;
use crate::msg::{
    ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, CurveResponse, AllCurvesResponse,
    ProgressResponse, ConfigResponse, SimulateBuyResponse, SimulateSellResponse,
    OracleResponse, MetadataFieldState, MetadataStateResponse, MetadataVoteResponse,
    PresaleContributionResponse, PresaleResponse,
};
use crate::state::{
    normalize_for_cooldown, AppliedMetadata, Config, CreatorVestingConfig, Curve, MetadataVote,
    OracleState, PendingCurve, PendingFinalize, PresaleConfig, PresaleState, TokenMetadata,
    APPLIED_METADATA, CONFIG, CURVES, DEFAULT_NAME_COOLDOWN_SECS, MAX_CREATOR_ALLOCATION_BPS,
    METADATA_FIRST_APPLY_DELAY_SECS, METADATA_TALLIES, METADATA_VOTES,
    METADATA_VOTING_WINDOW_SECS, NAME_COOLDOWN, NAME_PERMA_BLOCK, ORACLE_STATE, PENDING_CURVE,
    PENDING_FINALIZE, PRESALES, PRESALE_CLAIMED, PRESALE_CONTRIBUTIONS, SYMBOL_COOLDOWN, SYMBOL_PERMA_BLOCK,
    VESTING_INSTANCES,
};

const CONTRACT_NAME: &str = "crates.io:bwick-launchpad";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");

// ── Oracle integration ────────────────────────────────────────────────────────
//
// Mirror of the bwick-oracle contract's QueryMsg::Twap response. Defined inline
// here to avoid a dependency cycle on the oracle crate. As long as the field
// names match, serde will deserialize correctly.

#[cw_serde]
struct OracleTwapQueryMsg {
    twap: OracleTwapArgs,
}

#[cw_serde]
struct OracleTwapArgs {
    window_seconds: Option<u64>,
}

#[cw_serde]
struct OracleTwapResponse {
    bwick_usd_price: Uint128,
    sol_usd_price: Uint128,
    last_update_height: u64,
    last_update_timestamp: u64,
    source: String,
    fresh: bool,
}

/// Returns the current BWICK/USD price in micro-USD (6 decimals), preferring
/// the live oracle contract's TWAP when configured. Falls back to the legacy
/// admin-set ORACLE_STATE when the oracle contract is not configured or the
/// query fails. Returns 0 if no source provides a price.
fn get_bwick_usd_price(deps: Deps, config: &Config) -> u128 {
    if !config.oracle_contract.as_str().is_empty() {
        let req = OracleTwapQueryMsg {
            twap: OracleTwapArgs { window_seconds: Some(60) },
        };
        if let Ok(resp) = deps.querier.query_wasm_smart::<OracleTwapResponse>(
            config.oracle_contract.as_str(),
            &req,
        ) {
            if resp.fresh && resp.bwick_usd_price.u128() > 0 {
                return resp.bwick_usd_price.u128();
            }
        }
    }
    // Fallback: legacy admin-set local oracle.
    ORACLE_STATE
        .may_load(deps.storage)
        .ok()
        .flatten()
        .map(|o| o.bwick_usd_price)
        .unwrap_or(0)
}

// Reply ID for CW20 instantiation
const REPLY_CW20_INSTANTIATE: u64 = 1;
// v4: Reply ID for bwick-vesting instantiation triggered by FinalizePresale.
const REPLY_VESTING_INSTANTIATE: u64 = 2;
// v4: bwick-vesting code ID. Populated via UpdateConfig at runtime (TODO:
// add a Config.vesting_code_id field + migrate). For now, the FinalizePresale
// path will fail loudly if Config doesn't carry a vesting code id.
const VESTING_CONTRACT_LABEL_PREFIX: &str = "bwick-vesting-";

// Fixed total supply: 100 thousand tokens with 6 decimals
// Starting FDV = base_price * total_supply = 0.01 USD * 100k = $1,000
pub const TOTAL_SUPPLY: u128 = 100_000_000_000; // 100 thousand * 10^6

// ===========================================
// Constant Product Curve Constants
// ===========================================

/// Tokens available for purchase on the bonding curve (79.31% of supply)
pub const TOKENS_ON_CURVE: u128 = 79_310_000_000; // 79.31k * 10^6

/// Tokens reserved for AMM liquidity pool at graduation (20.69% of supply)
pub const TOKENS_FOR_LP: u128 = 20_690_000_000; // 20.69k * 10^6

/// Virtual BWICK reserves at curve start (in ubwick)
/// Determines starting price and total BWICK raised at graduation.
/// Higher value = more BWICK raised at graduation, lower starting price.
pub const VIRTUAL_BWICK_START: u128 = 34_800_000_000_000; // 34,800,000 BWICK

/// Virtual token reserves at curve start (in base units with 6 decimals)
/// Scaled from pump.fun's 1.073B virtual tokens (/ 10 for 100M supply).
/// Must be > TOKENS_ON_CURVE for the math to work.
pub const VIRTUAL_TOKENS_START: u128 = 107_300_000_000_000; // 107.3M tokens

/// Constant product invariant: k = virtual_bwick * virtual_tokens
/// This remains constant throughout the curve's lifecycle.
/// = 34_800_000_000_000 * 107_300_000_000_000
pub const K: u128 = 3_734_040_000_000_000_000_000_000_000;

// ===========================================
// Dynamic Curve Parameter Computation
// ===========================================

/// Precision multiplier for fixed-point sqrt computation
const CURVE_PRECISION: u128 = 1_000_000;

#[derive(Debug)]
struct CurveParams {
    virtual_bwick_start: u128,
    virtual_tokens_start: u128,
    curve_k: u128,
    tokens_on_curve: u128,
    tokens_for_lp: u128,
}

/// Integer square root using Newton's method
fn isqrt(n: u128) -> u128 {
    if n == 0 {
        return 0;
    }
    let mut x = n;
    let mut y = (x + 1) / 2;
    while y < x {
        x = y;
        y = (x + n / x) / 2;
    }
    x
}

/// Compute per-curve bonding curve parameters from USD targets and oracle price.
///
/// All USD values in micro-USD (6 decimals). BWICK amounts in ubwick.
/// Token amounts in utokens (with 6 decimal places).
///
/// Key formulas (S = sqrt(graduation_mc / starting_mc)):
///   tokens_on_curve = raised * TOTAL_SUPPLY / (starting_mc * S)
///   virtual_tokens  = tokens_on_curve * S / (S - 1)
///   raised_ubwick     = raised_usd * 10^6 / bwick_price
///   virtual_bwick     = raised_ubwick / (S - 1)
///   K               = virtual_bwick * virtual_tokens
fn compute_curve_params(
    bwick_usd_price: u128,
    target_starting_mc_usd: u128,
    target_graduation_mc_usd: u128,
    target_raised_usd: u128,
    raised_ubwick_override: u128,
) -> Result<CurveParams, ContractError> {
    // v6: when a BWICK-denominated raise target is configured the oracle is
    // not needed: the USD targets only contribute dimensionless ratios
    // (graduation/starting MC and raised/starting MC), and the absolute
    // BWICK raise comes straight from the override. Curves sized this way
    // keep the same appreciation multiple regardless of BWICK's USD price.
    if bwick_usd_price == 0 && raised_ubwick_override == 0 {
        return Err(ContractError::OraclePriceRequired {});
    }
    if target_starting_mc_usd == 0 {
        return Err(ContractError::InvalidCurveParams {
            reason: "target_starting_mc_usd must be > 0".to_string(),
        });
    }
    if target_graduation_mc_usd <= target_starting_mc_usd {
        return Err(ContractError::InvalidCurveParams {
            reason: "graduation MC must be > starting MC".to_string(),
        });
    }

    // 1. MC ratio (integer)
    let mc_ratio = target_graduation_mc_usd / target_starting_mc_usd;

    // 2. S_scaled = isqrt(mc_ratio * PRECISION^2) ≈ sqrt(mc_ratio) * PRECISION
    let s_scaled = isqrt(mc_ratio * CURVE_PRECISION * CURVE_PRECISION);
    if s_scaled <= CURVE_PRECISION {
        return Err(ContractError::InvalidCurveParams {
            reason: "sqrt(mc_ratio) must be > 1".to_string(),
        });
    }
    let s_minus_one = s_scaled - CURVE_PRECISION;

    // 3. tokens_on_curve (utokens) = raised * TOTAL_SUPPLY * PRECISION / (starting_mc * S_scaled)
    let toc_num = target_raised_usd
        .checked_mul(TOTAL_SUPPLY)
        .and_then(|v| v.checked_mul(CURVE_PRECISION))
        .ok_or_else(|| ContractError::InvalidCurveParams {
            reason: "overflow computing tokens_on_curve numerator".to_string(),
        })?;
    let toc_denom = target_starting_mc_usd
        .checked_mul(s_scaled)
        .ok_or_else(|| ContractError::InvalidCurveParams {
            reason: "overflow computing tokens_on_curve denominator".to_string(),
        })?;
    let tokens_on_curve = toc_num / toc_denom;

    if tokens_on_curve == 0 || tokens_on_curve >= TOTAL_SUPPLY {
        return Err(ContractError::InvalidCurveParams {
            reason: format!(
                "tokens_on_curve out of range: {} (must be 0 < x < {})",
                tokens_on_curve, TOTAL_SUPPLY
            ),
        });
    }

    // 4. virtual_tokens (utokens) = tokens_on_curve * S_scaled / s_minus_one
    let virtual_tokens_start = tokens_on_curve
        .checked_mul(s_scaled)
        .ok_or_else(|| ContractError::InvalidCurveParams {
            reason: "overflow computing virtual_tokens".to_string(),
        })?
        / s_minus_one;

    // 5. raised_ubwick: BWICK override wins; otherwise convert the USD
    //    target at the oracle price.
    let raised_ubwick = if raised_ubwick_override > 0 {
        raised_ubwick_override
    } else {
        target_raised_usd
            .checked_mul(1_000_000)
            .ok_or_else(|| ContractError::InvalidCurveParams {
                reason: "overflow computing raised_ubwick".to_string(),
            })?
            / bwick_usd_price
    };

    // 6. virtual_bwick (ubwick) = raised_ubwick * PRECISION / s_minus_one
    let virtual_bwick_start = raised_ubwick
        .checked_mul(CURVE_PRECISION)
        .ok_or_else(|| ContractError::InvalidCurveParams {
            reason: "overflow computing virtual_bwick".to_string(),
        })?
        / s_minus_one;

    if virtual_bwick_start == 0 {
        return Err(ContractError::InvalidCurveParams {
            reason: "virtual_bwick_start is zero (price too high or raised too low)".to_string(),
        });
    }

    // 7. K = virtual_bwick * virtual_tokens
    let curve_k = virtual_bwick_start
        .checked_mul(virtual_tokens_start)
        .ok_or_else(|| ContractError::InvalidCurveParams {
            reason: "overflow computing K (virtual reserves too large)".to_string(),
        })?;

    // 8. tokens_for_lp
    let tokens_for_lp = TOTAL_SUPPLY - tokens_on_curve;

    Ok(CurveParams {
        virtual_bwick_start,
        virtual_tokens_start,
        curve_k,
        tokens_on_curve,
        tokens_for_lp,
    })
}

/// Check that buyer's holding after purchase won't exceed max_wallet_bps of total supply.
/// Queries the CW20 contract for the buyer's current balance.
/// Skips the check if max_wallet_bps is 0 (disabled).
fn check_max_wallet(
    deps: &DepsMut,
    token_address: &str,
    buyer_address: &str,
    tokens_out: u128,
    max_wallet_bps: u16,
) -> Result<(), ContractError> {
    if max_wallet_bps == 0 {
        return Ok(());
    }

    let limit = TOTAL_SUPPLY * (max_wallet_bps as u128) / 10000;

    // Query buyer's current CW20 balance
    let balance_resp: cw20::BalanceResponse = deps.querier.query_wasm_smart(
        token_address,
        &cw20::Cw20QueryMsg::Balance {
            address: buyer_address.to_string(),
        },
    )?;
    let current = balance_resp.balance.u128();
    let would_become = current + tokens_out;

    if would_become > limit {
        return Err(ContractError::MaxWalletExceeded {
            current,
            buying: tokens_out,
            would_become,
            limit,
        });
    }

    Ok(())
}

/// Extract per-curve constants, falling back to legacy hardcoded values if not set.
fn get_curve_constants(curve: &Curve) -> (u128, u128, u128, u128, u128) {
    if curve.curve_k > 0 {
        (
            curve.virtual_bwick_start,
            curve.virtual_tokens_start,
            curve.curve_k,
            curve.tokens_on_curve,
            curve.tokens_for_lp,
        )
    } else {
        (VIRTUAL_BWICK_START, VIRTUAL_TOKENS_START, K, TOKENS_ON_CURVE, TOKENS_FOR_LP)
    }
}

// ===========================================
// Constant Product Curve Functions
// ===========================================

/// Calculate current spot price at given tokens_sold using constant product curve.
/// Returns price in ubwick per whole token (per 10^6 base units).
///
/// Spot price = virtual_bwick / virtual_tokens * 10^6
/// Simplified: price = k * 10^6 / (virtual_tokens_start - tokens_sold)^2
fn calculate_price_cp(tokens_sold: u128, virtual_tokens_start: u128, k: u128) -> u128 {
    let virtual_tokens = virtual_tokens_start - tokens_sold;
    k * 1_000_000 / (virtual_tokens * virtual_tokens)
}

/// Calculate tokens received for BWICK input using constant product formula.
/// Fee is deducted from bwick_input BEFORE applying to curve.
/// Returns (tokens_out, fee_amount) in base units.
///
/// Formula: tokens_out = virtual_tokens_current - k / (virtual_bwick_current + bwick_after_fee)
fn calculate_buy_cp(
    tokens_sold: u128,
    bwick_input: u128,
    buy_fee_bps: u16,
    vt_start: u128,
    k: u128,
    toc: u128,
) -> Result<(u128, u128), ContractError> {
    // Deduct buy fee
    let fee = bwick_input * (buy_fee_bps as u128) / 10000;
    let bwick_after_fee = bwick_input - fee;

    if bwick_after_fee == 0 {
        return Err(ContractError::NoTokensAvailable {});
    }

    // Current virtual reserves
    let virtual_tokens = vt_start - tokens_sold;
    let virtual_bwick = k / virtual_tokens;

    // After adding BWICK to pool
    let new_virtual_bwick = virtual_bwick + bwick_after_fee;
    let new_virtual_tokens = k / new_virtual_bwick;
    let tokens_out = virtual_tokens - new_virtual_tokens;

    // Cap at remaining curve tokens
    let tokens_remaining = toc.saturating_sub(tokens_sold);
    let tokens_out = tokens_out.min(tokens_remaining);

    if tokens_out == 0 {
        return Err(ContractError::NoTokensAvailable {});
    }

    Ok((tokens_out, fee))
}

/// Calculate BWICK returned for tokens sold back using constant product formula.
/// Fee is deducted from BWICK output AFTER computing curve value.
/// Returns (bwick_out_after_fee, total_fee) in ubwick.
///
/// Formula: bwick_returned_before_fee = virtual_bwick_current - k / (virtual_tokens_current + tokens_returned)
fn calculate_sell_cp(
    tokens_sold: u128,
    tokens_input: u128,
    sell_fee_bps: u16,
    vt_start: u128,
    k: u128,
) -> Result<(u128, u128), ContractError> {
    if tokens_input > tokens_sold {
        return Err(ContractError::Std(StdError::generic_err(
            "Cannot sell more tokens than have been sold"
        )));
    }

    // Current virtual reserves
    let virtual_tokens = vt_start - tokens_sold;
    let virtual_bwick = k / virtual_tokens;

    // After returning tokens to the pool
    let new_virtual_tokens = virtual_tokens + tokens_input;
    let new_virtual_bwick = k / new_virtual_tokens;

    // BWICK to return (before fee)
    let bwick_before_fee = virtual_bwick - new_virtual_bwick;

    // Apply sell fee
    let total_fee = bwick_before_fee * (sell_fee_bps as u128) / 10000;
    let bwick_out = bwick_before_fee - total_fee;

    Ok((bwick_out, total_fee))
}

// ===========================================
// Dynamic Graduation Threshold Functions
// ===========================================

/// Compute dynamic graduation threshold from oracle price.
///
/// Formula: threshold_ubwick = target_raised_usd * 10^6 / bwick_usd_price
/// Result is clamped to [min_threshold, max_threshold].
/// If price is 0, returns fallback_threshold.
///
/// All USD values in micro-USD (6 decimals).
/// All BWICK values in ubwick (6 decimals).
fn compute_dynamic_threshold(
    bwick_usd_price: u128,        // micro-USD per BWICK
    target_raised_usd: u128,    // micro-USD target raised
    min_threshold: u128,         // ubwick
    max_threshold: u128,         // ubwick
    fallback_threshold: u128,    // ubwick (used when price is 0)
    target_raised_bwick: u128,   // ubwick override; 0 = use USD path
) -> u128 {
    // v5: BWICK-denominated override. Skips the oracle entirely so graduation
    // is deterministic across price swings. Still clamped to the safety bounds.
    if target_raised_bwick > 0 {
        return target_raised_bwick.max(min_threshold).min(max_threshold);
    }

    if bwick_usd_price == 0 {
        return fallback_threshold;
    }

    let raw = target_raised_usd
        .checked_mul(1_000_000)
        .expect("target * 10^6 overflow")
        / bwick_usd_price;

    raw.max(min_threshold).min(max_threshold)
}

/// Compute effective threshold for a specific curve, applying ratchet logic.
///
/// - If curve has a stored threshold (Some(t)): effective = max(t, computed)
/// - If legacy curve (None): effective = max(legacy_fallback, computed)
///
/// This is a pure function for testability. The caller loads state and oracle.
fn effective_threshold_pure(
    stored_threshold: Option<u128>,  // curve.graduation_threshold_ubwick
    computed_threshold: u128,        // from compute_dynamic_threshold
    legacy_fallback: u128,           // config.graduation_threshold (old global)
) -> u128 {
    match stored_threshold {
        Some(t) => t.max(computed_threshold),
        None => legacy_fallback.max(computed_threshold),
    }
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> Result<Response, ContractError> {
    // Validate AMM contract address
    let amm_contract = deps.api.addr_validate(&msg.amm_contract)?;
    let admin = deps.api.addr_validate(&msg.admin)?;

    // Validate fees (buy <= 5%, sell <= 10%)
    if msg.buy_fee_bps > 500 || msg.sell_fee_bps > 1000 {
        return Err(ContractError::InvalidFees {});
    }

    // Validate threshold bounds
    let min_thresh = msg.min_graduation_threshold.u128();
    let max_thresh = msg.max_graduation_threshold.u128();
    if min_thresh > max_thresh {
        return Err(ContractError::InvalidThresholdBounds {
            min: min_thresh,
            max: max_thresh,
        });
    }

    let config = Config {
        presales_enabled: false,
        amm_contract,
        cw20_code_id: msg.cw20_code_id,
        creation_fee: msg.creation_fee.u128(),
        graduation_threshold: msg.graduation_threshold.u128(),
        buy_fee_bps: msg.buy_fee_bps,
        sell_fee_bps: msg.sell_fee_bps,
        creator_fee_share_bps: msg.creator_fee_share_bps,
        admin,
        target_graduation_usd: msg.target_graduation_usd.u128(),
        min_graduation_threshold: min_thresh,
        max_graduation_threshold: max_thresh,
        target_starting_mc_usd: msg.target_starting_mc_usd.u128(),
        target_raised_usd: msg.target_raised_usd.u128(),
        target_raised_bwick: msg.target_raised_bwick.u128(),
        max_wallet_bps: msg.max_wallet_bps,
        fee_exempt_address: Addr::unchecked(""),
        oracle_contract: Addr::unchecked(""),
        name_cooldown_seconds: 0, // 0 = use the 7-day default
        vesting_code_id: 0,        // set via UpdateConfig or Migrate
    };
    CONFIG.save(deps.storage, &config)?;

    // Initialize oracle state with zeroes (no price yet)
    let oracle_state = OracleState {
        bwick_usd_price: 0,
        last_update_height: 0,
        last_update_timestamp: 0,
    };
    ORACLE_STATE.save(deps.storage, &oracle_state)?;

    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

    Ok(Response::new()
        .add_attribute("action", "instantiate")
        .add_attribute("amm_contract", msg.amm_contract)
        .add_attribute("cw20_code_id", msg.cw20_code_id.to_string())
        .add_attribute("creation_fee", msg.creation_fee)
        .add_attribute("graduation_threshold", msg.graduation_threshold))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::CreateToken {
            name,
            symbol,
            image,
            description,
            social_links,
            presale,
            creator_vesting,
            lp_lock_seconds,
        } => execute_create_token(
            deps,
            env,
            info,
            name,
            symbol,
            image,
            description,
            social_links,
            presale,
            creator_vesting,
            lp_lock_seconds,
        ),
        ExecuteMsg::Buy { token_address, min_tokens_out } => {
            execute_buy(deps, env, info, token_address, min_tokens_out)
        }
        ExecuteMsg::Receive(cw20_msg) => execute_receive(deps, env, info, cw20_msg),
        ExecuteMsg::Graduate { token_address } => {
            execute_graduate(deps, env, info, token_address)
        }
        ExecuteMsg::UpdateBwickPrice { bwick_usd_price } => {
            execute_update_bwick_price(deps, env, info, bwick_usd_price)
        }
        ExecuteMsg::UpdateConfig {
            target_graduation_usd,
            min_graduation_threshold,
            max_graduation_threshold,
            target_starting_mc_usd,
            target_raised_usd,
            target_raised_bwick,
            max_wallet_bps,
            fee_exempt_address,
            oracle_contract,
            name_cooldown_seconds,
            vesting_code_id,
            presales_enabled,
        } => execute_update_config(
            deps,
            info,
            target_graduation_usd,
            min_graduation_threshold,
            max_graduation_threshold,
            target_starting_mc_usd,
            target_raised_usd,
            target_raised_bwick,
            max_wallet_bps,
            fee_exempt_address,
            oracle_contract,
            name_cooldown_seconds,
            vesting_code_id,
            presales_enabled,
        ),
        ExecuteMsg::VoteMetadata { token_address, name, symbol, image, description } => {
            execute_vote_metadata(deps, env, info, token_address, name, symbol, image, description)
        }
        ExecuteMsg::ApplyMetadata { token_address } => {
            execute_apply_metadata(deps, env, info, token_address)
        }
        ExecuteMsg::BuyPresale { token_address, merkle_proof, min_tokens_out } => {
            execute_buy_presale(deps, env, info, token_address, merkle_proof, min_tokens_out)
        }
        ExecuteMsg::FinalizePresale { token_address } => {
            execute_finalize_presale(deps, env, info, token_address)
        }
        ExecuteMsg::ClaimPresale { token_address } => {
            execute_claim_presale(deps, env, info, token_address)
        }
    }
}

#[allow(clippy::too_many_arguments)]
fn execute_create_token(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    name: String,
    symbol: String,
    image: String,
    description: String,
    social_links: Vec<String>,
    presale: Option<PresaleConfig>,
    creator_vesting: Option<CreatorVestingConfig>,
    lp_lock_seconds: Option<u64>,
) -> Result<Response, ContractError> {
    // v4: validate presale config up-front. If presale is None the rest
    // of the function behaves exactly as v3.
    if let Some(cfg) = presale.as_ref() {
        validate_presale_config(cfg, env.block.time.seconds())?;
    }
    // Suppress unused-variable warnings for the v4 fields that are
    // wired through PendingCurve but not yet acted on by the reply +
    // graduate handlers. The follow-up commit will hook them up.
    let _ = (&creator_vesting, &lp_lock_seconds);
    let config = CONFIG.load(deps.storage)?;

    // v7: presales ship dormant; flip Config.presales_enabled to launch them.
    if presale.is_some() && !config.presales_enabled {
        return Err(ContractError::PresalesDisabled {});
    }
    if let Some(ref presale_cfg) = presale {
        if presale_cfg.min_raise_ubwick > presale_cfg.hard_cap_ubwick {
            return Err(ContractError::InvalidPresaleConfig {
                reason: "min_raise_ubwick cannot exceed hard_cap_ubwick".into(),
            });
        }
    }

    // Verify creation fee sent (80,000 BWICK) — exempt address skips fee
    let bwick_sent = extract_bwick_from_funds(&info.funds)?;
    let is_exempt = !config.fee_exempt_address.as_str().is_empty()
        && info.sender == config.fee_exempt_address;
    if !is_exempt && bwick_sent < config.creation_fee {
        return Err(ContractError::InsufficientCreationFee {
            expected: config.creation_fee,
            got: bwick_sent,
        });
    }

    // Anti-vampire name/symbol reuse limits. Two layers:
    //   - permanent block: if a curve with this normalized name/symbol has
    //     ever graduated, it's reserved forever. Anti-fork.
    //   - cooldown: 7-day default; configurable via UpdateConfig. Prevents
    //     a copycat from re-deploying immediately after a rugged / failed
    //     curve. Normalization strips case + non-alphanumeric so cheap
    //     evasions ("BWICK V2", "B-WICK", "bwick_") all collide.
    let cooldown = if config.name_cooldown_seconds > 0 {
        config.name_cooldown_seconds
    } else {
        DEFAULT_NAME_COOLDOWN_SECS
    };
    let now = env.block.time.seconds();
    let symbol_key = normalize_for_cooldown(&symbol);
    let name_key = normalize_for_cooldown(&name);

    // Reject empty normalization (e.g. only symbols/whitespace).
    if symbol_key.is_empty() || name_key.is_empty() {
        return Err(ContractError::Std(StdError::generic_err(
            "name and symbol must contain at least one letter or digit",
        )));
    }

    if SYMBOL_PERMA_BLOCK.has(deps.storage, &symbol_key) {
        return Err(ContractError::NameCooldown {
            field: "symbol (permanently taken — graduated token)".into(),
            value: symbol.clone(),
            seconds_ago: 0,
            remaining: u64::MAX,
        });
    }
    if NAME_PERMA_BLOCK.has(deps.storage, &name_key) {
        return Err(ContractError::NameCooldown {
            field: "name (permanently taken — graduated token)".into(),
            value: name.clone(),
            seconds_ago: 0,
            remaining: u64::MAX,
        });
    }

    if let Some(last_used) = SYMBOL_COOLDOWN.may_load(deps.storage, &symbol_key)? {
        let elapsed = now.saturating_sub(last_used);
        if elapsed < cooldown {
            return Err(ContractError::NameCooldown {
                field: "symbol".into(),
                value: symbol.clone(),
                seconds_ago: elapsed,
                remaining: cooldown - elapsed,
            });
        }
    }

    if let Some(last_used) = NAME_COOLDOWN.may_load(deps.storage, &name_key)? {
        let elapsed = now.saturating_sub(last_used);
        if elapsed < cooldown {
            return Err(ContractError::NameCooldown {
                field: "name".into(),
                value: name.clone(),
                seconds_ago: elapsed,
                remaining: cooldown - elapsed,
            });
        }
    }

    // Reserve the symbol and name immediately (before SubMsg)
    SYMBOL_COOLDOWN.save(deps.storage, &symbol_key, &now)?;
    NAME_COOLDOWN.save(deps.storage, &name_key, &now)?;

    // Load oracle price (prefer live oracle contract, fall back to local) and
    // compute per-curve bonding curve parameters.
    let bwick_usd_price = get_bwick_usd_price(deps.as_ref(), &config);
    if bwick_usd_price == 0 {
        return Err(ContractError::OraclePriceRequired {});
    }

    let params = compute_curve_params(
        bwick_usd_price,
        config.target_starting_mc_usd,
        config.target_graduation_usd, // graduation MC target
        config.target_raised_usd,
        config.target_raised_bwick,
    )?;

    // Compute initial graduation threshold for this curve
    let grad_threshold = compute_dynamic_threshold(
        bwick_usd_price,
        config.target_raised_usd,
        config.min_graduation_threshold,
        config.max_graduation_threshold,
        config.graduation_threshold,
        config.target_raised_bwick,
    );

    // Store pending curve info for reply handler
    let pending = PendingCurve {
        metadata: TokenMetadata {
            name: name.clone(),
            symbol: symbol.clone(),
            image,
            description,
            social_links,
        },
        creator: info.sender.clone(),
        initial_bwick: bwick_sent,
        virtual_bwick_start: params.virtual_bwick_start,
        virtual_tokens_start: params.virtual_tokens_start,
        curve_k: params.curve_k,
        tokens_on_curve: params.tokens_on_curve,
        tokens_for_lp: params.tokens_for_lp,
        graduation_threshold_ubwick: grad_threshold,
        presale,
        creator_vesting,
        lp_lock_seconds,
    };
    PENDING_CURVE.save(deps.storage, &pending)?;

    // Instantiate CW20 token with this contract as minter
    // All tokens go to this contract (held by curve)
    let cw20_instantiate_msg = cw20_base::msg::InstantiateMsg {
        name,
        symbol,
        decimals: 6,
        initial_balances: vec![cw20::Cw20Coin {
            address: env.contract.address.to_string(),
            amount: Uint128::from(TOTAL_SUPPLY),
        }],
        mint: None, // No minting after creation (fixed supply)
        marketing: None,
    };

    let instantiate_msg = WasmMsg::Instantiate {
        admin: None,
        code_id: config.cw20_code_id,
        msg: to_binary(&cw20_instantiate_msg)?,
        funds: vec![],
        label: format!("bwick-launchpad-{}", pending.metadata.symbol),
    };

    // Use SubMsg to get reply with contract address
    let sub_msg = SubMsg::reply_on_success(instantiate_msg, REPLY_CW20_INSTANTIATE);

    Ok(Response::new()
        .add_submessage(sub_msg)
        .add_attribute("action", "create_token")
        .add_attribute("creator", info.sender)
        .add_attribute("initial_bwick", bwick_sent.to_string()))
}

/// Extract BWICK amount from funds
fn extract_bwick_from_funds(funds: &[Coin]) -> Result<u128, ContractError> {
    for coin in funds {
        if coin.denom == "ubwick" {
            return Ok(coin.amount.u128());
        }
    }
    Err(ContractError::InsufficientFunds {})
}

fn execute_update_bwick_price(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    bwick_usd_price: Uint128,
) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    if info.sender != config.admin {
        return Err(ContractError::Unauthorized {});
    }
    if bwick_usd_price.is_zero() {
        return Err(ContractError::ZeroPrice {});
    }

    let oracle = OracleState {
        bwick_usd_price: bwick_usd_price.u128(),
        last_update_height: env.block.height,
        last_update_timestamp: env.block.time.seconds(),
    };
    ORACLE_STATE.save(deps.storage, &oracle)?;

    Ok(Response::new()
        .add_attribute("action", "update_bwick_price")
        .add_attribute("price", bwick_usd_price)
        .add_attribute("height", env.block.height.to_string()))
}

#[allow(clippy::too_many_arguments)]
fn execute_update_config(
    deps: DepsMut,
    info: MessageInfo,
    target_graduation_usd: Option<Uint128>,
    min_graduation_threshold: Option<Uint128>,
    max_graduation_threshold: Option<Uint128>,
    target_starting_mc_usd: Option<Uint128>,
    target_raised_usd: Option<Uint128>,
    target_raised_bwick: Option<Uint128>,
    max_wallet_bps: Option<u16>,
    fee_exempt_address: Option<String>,
    oracle_contract: Option<String>,
    name_cooldown_seconds: Option<u64>,
    vesting_code_id: Option<u64>,
    presales_enabled: Option<bool>,
) -> Result<Response, ContractError> {
    let mut config = CONFIG.load(deps.storage)?;
    if info.sender != config.admin {
        return Err(ContractError::Unauthorized {});
    }

    if let Some(val) = target_graduation_usd {
        config.target_graduation_usd = val.u128();
    }
    if let Some(val) = min_graduation_threshold {
        config.min_graduation_threshold = val.u128();
    }
    if let Some(val) = max_graduation_threshold {
        config.max_graduation_threshold = val.u128();
    }
    if let Some(val) = target_starting_mc_usd {
        config.target_starting_mc_usd = val.u128();
    }
    if let Some(val) = target_raised_usd {
        config.target_raised_usd = val.u128();
    }
    if let Some(val) = target_raised_bwick {
        config.target_raised_bwick = val.u128();
    }
    if let Some(val) = presales_enabled {
        config.presales_enabled = val;
    }
    if let Some(val) = max_wallet_bps {
        config.max_wallet_bps = val;
    }
    if let Some(addr) = fee_exempt_address {
        config.fee_exempt_address = if addr.is_empty() {
            Addr::unchecked("")
        } else {
            deps.api.addr_validate(&addr)?
        };
    }
    if let Some(addr) = oracle_contract {
        config.oracle_contract = if addr.is_empty() {
            Addr::unchecked("")
        } else {
            deps.api.addr_validate(&addr)?
        };
    }
    if let Some(secs) = name_cooldown_seconds {
        config.name_cooldown_seconds = secs;
    }
    if let Some(id) = vesting_code_id {
        config.vesting_code_id = id;
    }

    // Validate bounds after updates
    if config.min_graduation_threshold > config.max_graduation_threshold {
        return Err(ContractError::InvalidThresholdBounds {
            min: config.min_graduation_threshold,
            max: config.max_graduation_threshold,
        });
    }

    CONFIG.save(deps.storage, &config)?;

    Ok(Response::new()
        .add_attribute("action", "update_config")
        .add_attribute("target_graduation_usd", config.target_graduation_usd.to_string())
        .add_attribute("min_threshold", config.min_graduation_threshold.to_string())
        .add_attribute("max_threshold", config.max_graduation_threshold.to_string())
        .add_attribute("target_starting_mc_usd", config.target_starting_mc_usd.to_string())
        .add_attribute("target_raised_usd", config.target_raised_usd.to_string()))
}

/// Load oracle and compute effective threshold for a curve.
fn load_effective_threshold(
    deps: Deps,
    config: &Config,
    curve: &Curve,
) -> u128 {
    let oracle_price = get_bwick_usd_price(deps, config);

    let computed = compute_dynamic_threshold(
        oracle_price,
        config.target_raised_usd,
        config.min_graduation_threshold,
        config.max_graduation_threshold,
        config.graduation_threshold, // fallback = old global threshold
        config.target_raised_bwick,
    );

    effective_threshold_pure(
        curve.graduation_threshold_ubwick,
        computed,
        config.graduation_threshold, // legacy fallback
    )
}

// ── Metadata voting (v3) ──────────────────────────────────────────────────

/// Composite key helper. Tally storage is `(token, "field:value")` because
/// cw-storage-plus owned-String composite keys are clumsier. Field names
/// are fixed ASCII so there's no escaping concern.
fn tally_key(field: &str, value: &str) -> String {
    let mut s = String::with_capacity(field.len() + 1 + value.len());
    s.push_str(field);
    s.push(':');
    s.push_str(value);
    s
}

/// Query the live CW20 balance for `voter` against `token`. Returns u128.
fn query_cw20_balance(
    deps: Deps,
    token: &Addr,
    voter: &Addr,
) -> StdResult<u128> {
    let r: cw20::BalanceResponse = deps.querier.query_wasm_smart(
        token,
        &cw20::Cw20QueryMsg::Balance { address: voter.to_string() },
    )?;
    Ok(r.balance.u128())
}

/// Iterate all distinct values for (token, field) and return (winner, winner_weight, total_weight).
/// `applied_value` is used as the tiebreaker when no votes have been cast or
/// when the winning value has zero weight — i.e. the applied value sticks.
fn find_leader(
    deps: Deps,
    token: &Addr,
    field: &str,
    applied_value: &str,
) -> StdResult<(String, u128, u128)> {
    let prefix = format!("{}:", field);
    let mut best_value = applied_value.to_string();
    let mut best_weight: u128 = 0;
    let mut total: u128 = 0;
    // Iterate the (token, "field:value") tallies and pick the highest weight.
    // The prefix bound keeps us scoped to a single field.
    let entries: StdResult<Vec<_>> = METADATA_TALLIES
        .prefix(token)
        .range(deps.storage, None, None, cosmwasm_std::Order::Ascending)
        .collect();
    for (key, weight) in entries? {
        if !key.starts_with(&prefix) {
            continue;
        }
        total = total.saturating_add(weight);
        if weight > best_weight {
            best_weight = weight;
            best_value = key[prefix.len()..].to_string();
        }
    }
    Ok((best_value, best_weight, total))
}

/// Default-init the AppliedMetadata for a token if not yet stored. Called
/// from execute_create_token after the curve is finalized. Also called
/// lazily by vote / apply paths so the contract migrates cleanly.
fn ensure_applied_metadata(
    deps: &mut DepsMut,
    env: &Env,
    token: &Addr,
) -> Result<AppliedMetadata, ContractError> {
    let now = env.block.time.seconds();
    if let Some(mut existing) = APPLIED_METADATA.may_load(deps.storage, token)? {
        // Pre-v3 records (or pre-fields migration) won't have
        // created_at_secs. Backfill from last_applied_at so the 48h voting
        // window is anchored at first init. This preserves intent for
        // tokens that existed before this code rolled out.
        if existing.created_at_secs == 0 {
            existing.created_at_secs = existing.last_applied_at.max(1);
            APPLIED_METADATA.save(deps.storage, token, &existing)?;
        }
        return Ok(existing);
    }
    let curve = CURVES
        .may_load(deps.storage, token)?
        .ok_or(ContractError::CurveNotFound { token: token.to_string() })?;
    let applied = AppliedMetadata {
        name: curve.metadata.name.clone(),
        symbol: curve.metadata.symbol.clone(),
        image: curve.metadata.image.clone(),
        description: curve.metadata.description.clone(),
        last_applied_at: now,
        created_at_secs: now,
        first_vote_at: 0,
        apply_count: 0,
        twitter: social_at(&curve.metadata.social_links, 0),
        telegram: social_at(&curve.metadata.social_links, 1),
        website: social_at(&curve.metadata.social_links, 2),
    };
    APPLIED_METADATA.save(deps.storage, token, &applied)?;
    Ok(applied)
}

/// Compute the (winner, leading_weight, total_weight) for one field and
/// determine if the leader differs from the applied value. Used by both
/// the inline-update path (in VoteMetadata when apply_count > 0) and the
/// explicit ApplyMetadata path.
fn leader_for_field(
    deps: Deps,
    token: &Addr,
    field: &str,
    applied: &str,
) -> StdResult<(String, bool)> {
    let (leader, _w, _total) = find_leader(deps, token, field, applied)?;
    let changed = leader != applied;
    Ok((leader, changed))
}

/// Returns true if voting is still open for this token (within
/// METADATA_VOTING_WINDOW_SECS of opening).
fn voting_open(applied: &AppliedMetadata, now: u64) -> bool {
    let close_at = applied
        .created_at_secs
        .saturating_add(METADATA_VOTING_WINDOW_SECS);
    now < close_at
}

fn execute_vote_metadata(
    mut deps: DepsMut,
    env: Env,
    info: MessageInfo,
    token_address: String,
    name: Option<String>,
    symbol: Option<String>,
    image: Option<String>,
    description: Option<String>,
) -> Result<Response, ContractError> {
    let token = deps.api.addr_validate(&token_address)?;
    // Validate curve exists.
    if !CURVES.has(deps.storage, &token) {
        return Err(ContractError::CurveNotFound { token: token.to_string() });
    }
    let applied = ensure_applied_metadata(&mut deps, &env, &token)?;

    // 48h voting window — refuse new votes after the close.
    let now = env.block.time.seconds();
    if !voting_open(&applied, now) {
        return Err(ContractError::Std(StdError::generic_err(
            "metadata voting window has closed (48h from token creation)",
        )));
    }

    // Reject empty-string votes (use None to withdraw).
    for (label, v) in [
        ("name", &name),
        ("symbol", &symbol),
        ("image", &image),
        ("description", &description),
    ] {
        if let Some(s) = v {
            if s.is_empty() {
                return Err(ContractError::Std(StdError::generic_err(format!(
                    "field {} cannot be empty; pass null to withdraw vote",
                    label
                ))));
            }
        }
    }

    // Pull voter's live CW20 balance — that's their voting weight.
    let weight = query_cw20_balance(deps.as_ref(), &token, &info.sender)?;
    if weight == 0 {
        return Err(ContractError::Std(StdError::generic_err(
            "voter holds none of this token",
        )));
    }

    // Subtract the voter's previous tallies, if any.
    let prev = METADATA_VOTES
        .may_load(deps.storage, (&token, &info.sender))?
        .unwrap_or_default();
    let prev_weight = prev.weight;
    for (field, value) in [
        ("n", prev.name.as_deref()),
        ("s", prev.symbol.as_deref()),
        ("i", prev.image.as_deref()),
        ("d", prev.description.as_deref()),
    ] {
        if let Some(v) = value {
            let key = tally_key(field, v);
            let cur = METADATA_TALLIES
                .may_load(deps.storage, (&token, &key))?
                .unwrap_or(0);
            let new = cur.saturating_sub(prev_weight);
            if new == 0 {
                METADATA_TALLIES.remove(deps.storage, (&token, &key));
            } else {
                METADATA_TALLIES.save(deps.storage, (&token, &key), &new)?;
            }
        }
    }

    // Add new tallies.
    for (field, value) in [
        ("n", name.as_deref()),
        ("s", symbol.as_deref()),
        ("i", image.as_deref()),
        ("d", description.as_deref()),
    ] {
        if let Some(v) = value {
            let key = tally_key(field, v);
            let cur = METADATA_TALLIES
                .may_load(deps.storage, (&token, &key))?
                .unwrap_or(0);
            METADATA_TALLIES.save(
                deps.storage,
                (&token, &key),
                &cur.saturating_add(weight),
            )?;
        }
    }

    // Persist the voter's record (or remove if they cleared every field).
    let is_non_empty =
        name.is_some() || symbol.is_some() || image.is_some() || description.is_some();
    if !is_non_empty {
        METADATA_VOTES.remove(deps.storage, (&token, &info.sender));
    } else {
        METADATA_VOTES.save(
            deps.storage,
            (&token, &info.sender),
            &MetadataVote {
                name,
                symbol,
                image,
                description,
                weight,
            },
        )?;
    }

    // Inline-apply: once the first apply has run (apply_count > 0), the
    // applied metadata IS whatever's currently winning. This vote may
    // have shifted the leader, so refresh applied immediately. The first
    // apply still requires the explicit ApplyMetadata call (10-min
    // cooldown) so the demo has a "stabilization window" before mutations
    // start propagating live.
    let mut applied2 = applied;
    // Set first_vote_at on the first non-empty vote for this token. The
    // 10-min stabilization countdown runs from this timestamp, not from
    // token creation, so a token with no voter activity doesn't auto-snap.
    if applied2.first_vote_at == 0 && is_non_empty {
        applied2.first_vote_at = now;
        APPLIED_METADATA.save(deps.storage, &token, &applied2)?;
    }
    let mut inline_changed: Vec<&str> = Vec::new();
    if applied2.apply_count > 0 {
        let (n, c_n) = leader_for_field(deps.as_ref(), &token, "n", &applied2.name)?;
        let (s, c_s) = leader_for_field(deps.as_ref(), &token, "s", &applied2.symbol)?;
        let (i, c_i) = leader_for_field(deps.as_ref(), &token, "i", &applied2.image)?;
        let (d, c_d) = leader_for_field(deps.as_ref(), &token, "d", &applied2.description)?;
        if c_n { applied2.name = n; inline_changed.push("name"); }
        if c_s { applied2.symbol = s; inline_changed.push("symbol"); }
        if c_i { applied2.image = i; inline_changed.push("image"); }
        if c_d { applied2.description = d; inline_changed.push("description"); }
        if !inline_changed.is_empty() {
            applied2.last_applied_at = now;
            applied2.apply_count = applied2.apply_count.saturating_add(1);
            APPLIED_METADATA.save(deps.storage, &token, &applied2)?;
            CURVES.update(deps.storage, &token, |c| -> Result<_, ContractError> {
                let mut c = c.ok_or(ContractError::CurveNotFound { token: token.to_string() })?;
                c.metadata.name = applied2.name.clone();
                c.metadata.symbol = applied2.symbol.clone();
                c.metadata.image = applied2.image.clone();
                c.metadata.description = applied2.description.clone();
                Ok(c)
            })?;
        }
    }

    Ok(Response::new()
        .add_attribute("action", "vote_metadata")
        .add_attribute("token", token.to_string())
        .add_attribute("voter", info.sender.to_string())
        .add_attribute("weight", weight.to_string())
        .add_attribute("inline_applied", inline_changed.join(",")))
}

fn execute_apply_metadata(
    mut deps: DepsMut,
    env: Env,
    _info: MessageInfo,
    token_address: String,
) -> Result<Response, ContractError> {
    let token = deps.api.addr_validate(&token_address)?;
    if !CURVES.has(deps.storage, &token) {
        return Err(ContractError::CurveNotFound { token: token.to_string() });
    }
    let mut applied = ensure_applied_metadata(&mut deps, &env, &token)?;
    let now = env.block.time.seconds();

    // 48h voting window — refuse explicit applies after close (votes are
    // already refused at that point).
    if !voting_open(&applied, now) {
        return Err(ContractError::Std(StdError::generic_err(
            "metadata voting window has closed (48h from token creation)",
        )));
    }

    // Two-phase apply:
    //   - apply_count == 0: this is the FIRST apply for the token. It's
    //     gated behind METADATA_FIRST_APPLY_DELAY_SECS (10 min) from
    //     `first_vote_at` — the timer starts on the first non-empty vote,
    //     not from token creation. Refuses entirely if nobody has voted.
    //   - apply_count > 0: subsequent applies happen inline at vote time.
    //     This explicit call becomes a no-op fallback for cases where the
    //     dApp wants to force a recompute.
    if applied.apply_count == 0 {
        if applied.first_vote_at == 0 {
            return Err(ContractError::Std(StdError::generic_err(
                "no votes cast yet — first-apply countdown hasn't started",
            )));
        }
        let elapsed = now.saturating_sub(applied.first_vote_at);
        if elapsed < METADATA_FIRST_APPLY_DELAY_SECS {
            return Err(ContractError::Std(StdError::generic_err(format!(
                "first apply in {}s",
                METADATA_FIRST_APPLY_DELAY_SECS - elapsed
            ))));
        }
    }

    let (n, _, _) = find_leader(deps.as_ref(), &token, "n", &applied.name)?;
    let (s, _, _) = find_leader(deps.as_ref(), &token, "s", &applied.symbol)?;
    let (i, _, _) = find_leader(deps.as_ref(), &token, "i", &applied.image)?;
    let (d, _, _) = find_leader(deps.as_ref(), &token, "d", &applied.description)?;

    let mut changed = Vec::<&str>::new();
    if n != applied.name { applied.name = n; changed.push("name"); }
    if s != applied.symbol { applied.symbol = s; changed.push("symbol"); }
    if i != applied.image { applied.image = i; changed.push("image"); }
    if d != applied.description { applied.description = d; changed.push("description"); }
    applied.last_applied_at = now;
    applied.apply_count = applied.apply_count.saturating_add(1);

    APPLIED_METADATA.save(deps.storage, &token, &applied)?;

    // Mirror into the Curve so AllCurves/Curve responses naturally serve
    // the canonical metadata without callers having to query
    // APPLIED_METADATA separately.
    CURVES.update(deps.storage, &token, |c| -> Result<_, ContractError> {
        let mut c = c.ok_or(ContractError::CurveNotFound { token: token.to_string() })?;
        c.metadata.name = applied.name.clone();
        c.metadata.symbol = applied.symbol.clone();
        c.metadata.image = applied.image.clone();
        c.metadata.description = applied.description.clone();
        Ok(c)
    })?;

    Ok(Response::new()
        .add_attribute("action", "apply_metadata")
        .add_attribute("token", token.to_string())
        .add_attribute("changed", changed.join(",")))
}

fn execute_buy(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    token_address: String,
    min_tokens_out: Uint128,
) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    let token_addr = deps.api.addr_validate(&token_address)?;

    // Load curve
    let mut curve = CURVES.load(deps.storage, &token_addr)
        .map_err(|_| ContractError::CurveNotFound { token: token_address.clone() })?;

    // Check not graduated
    if curve.graduated {
        return Err(ContractError::AlreadyGraduated {});
    }

    // v4: block public Buy while a presale is open. Only BuyPresale
    // can move tokens during the presale window; FinalizePresale
    // flips `finalized=true` and opens the curve for everyone.
    if let Some(state) = PRESALES.may_load(deps.storage, &token_addr)? {
        if !state.finalized {
            return Err(ContractError::PresaleNotFinalized {});
        }
    }

    // Extract BWICK input
    let bwick_input = extract_bwick_from_funds(&info.funds)?;
    if bwick_input == 0 {
        return Err(ContractError::InsufficientFunds {});
    }

    // Get per-curve constants (or legacy fallback)
    let (_vx, vt, k, toc, _tlp) = get_curve_constants(&curve);

    // Calculate tokens out using constant product curve
    let (tokens_out, fee) = calculate_buy_cp(curve.tokens_sold, bwick_input, config.buy_fee_bps, vt, k, toc)?;

    // Check remaining supply on curve
    let tokens_remaining_on_curve = toc.saturating_sub(curve.tokens_sold);
    if tokens_out > tokens_remaining_on_curve {
        return Err(ContractError::NoTokensAvailable {});
    }

    // Check slippage
    if tokens_out < min_tokens_out.u128() {
        return Err(ContractError::SlippageExceeded {
            expected: min_tokens_out.u128(),
            actual: tokens_out,
        });
    }

    // Check max wallet holding limit
    check_max_wallet(
        &deps,
        &token_address,
        info.sender.as_str(),
        tokens_out,
        config.max_wallet_bps,
    )?;

    // Update curve state — all fees stay in reserves (LP)
    curve.tokens_sold += tokens_out;
    curve.bwick_reserves += bwick_input;

    // Compute effective threshold and ratchet it on the curve
    let threshold = load_effective_threshold(deps.as_ref(), &config, &curve);
    curve.graduation_threshold_ubwick = Some(match curve.graduation_threshold_ubwick {
        Some(existing) => existing.max(threshold),
        None => threshold,
    });

    // Check graduation threshold - auto-graduate if reached
    if curve.bwick_reserves >= threshold && !curve.graduated {
        // Mark as graduated
        curve.graduated = true;
        CURVES.save(deps.storage, &token_addr, &curve)?;

        // Calculate AMM pool liquidity
        let bwick_for_pool = curve.bwick_reserves;
        let tokens_remaining = TOTAL_SUPPLY - curve.tokens_sold;

        // Build messages
        let mut messages: Vec<cosmwasm_std::CosmosMsg> = vec![];

        // Transfer tokens to buyer
        messages.push(cosmwasm_std::CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: token_address.clone(),
            msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer {
                recipient: info.sender.to_string(),
                amount: Uint128::from(tokens_out),
            })?,
            funds: vec![],
        }));

        // Transfer remaining tokens to AMM
        messages.push(cosmwasm_std::CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: token_address.clone(),
            msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer {
                recipient: config.amm_contract.to_string(),
                amount: Uint128::from(tokens_remaining),
            })?,
            funds: vec![],
        }));

        // Create AMM pool with augmented fee protection
        let lp_target = bwick_for_pool
            .checked_mul(GRADUATION_LP_TARGET_MULTIPLIER)
            .unwrap_or(bwick_for_pool);
        messages.push(cosmwasm_std::CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: config.amm_contract.to_string(),
            msg: to_binary(&AmmExecuteMsg::CreatePool {
                token_address: token_address.clone(),
                bwick_amount: Uint128::from(bwick_for_pool),
                token_amount: Uint128::from(tokens_remaining),
                augmented_fee_bps: Some(GRADUATION_AUGMENTED_FEE_BPS),
                lp_target_ubwick: Some(lp_target.to_string()),
                locked_lp_unlock_at: None,
                locked_lp_recipient: None,
            })?,
            funds: vec![Coin {
                denom: "ubwick".to_string(),
                amount: Uint128::from(bwick_for_pool),
            }],
        }));

        return Ok(Response::new()
            .add_messages(messages)
            .add_attribute("action", "buy_and_graduate")
            .add_attribute("buyer", info.sender.to_string())
            .add_attribute("token_address", token_address)
            .add_attribute("tokens_out", tokens_out.to_string())
            .add_attribute("graduated", "true")
            .add_attribute("bwick_for_pool", bwick_for_pool.to_string())
            .add_attribute("tokens_for_pool", tokens_remaining.to_string()));
    }

    // Save curve (non-graduating case)
    CURVES.save(deps.storage, &token_addr, &curve)?;

    // Transfer tokens to buyer
    let transfer_msg = WasmMsg::Execute {
        contract_addr: token_address.clone(),
        msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer {
            recipient: info.sender.to_string(),
            amount: Uint128::from(tokens_out),
        })?,
        funds: vec![],
    };

    Ok(Response::new()
        .add_message(transfer_msg)
        .add_attribute("action", "buy")
        .add_attribute("buyer", info.sender.to_string())
        .add_attribute("token_address", token_address)
        .add_attribute("bwick_input", bwick_input.to_string())
        .add_attribute("tokens_out", tokens_out.to_string())
        .add_attribute("fee", fee.to_string()))
}

fn execute_receive(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    cw20_msg: cw20::Cw20ReceiveMsg,
) -> Result<Response, ContractError> {
    use crate::msg::SellTokens;

    let config = CONFIG.load(deps.storage)?;

    // info.sender is the CW20 token contract
    let token_addr = info.sender;

    // Load curve
    let mut curve = CURVES.load(deps.storage, &token_addr)
        .map_err(|_| ContractError::CurveNotFound { token: token_addr.to_string() })?;

    // Check not graduated
    if curve.graduated {
        return Err(ContractError::AlreadyGraduated {});
    }

    // v4: block public Sell while a presale is open (same as Buy).
    if let Some(state) = PRESALES.may_load(deps.storage, &token_addr)? {
        if !state.finalized {
            return Err(ContractError::PresaleNotFinalized {});
        }
    }

    // Parse sell message
    let sell_msg: SellTokens = cosmwasm_std::from_slice(&cw20_msg.msg)?;
    let user_addr = deps.api.addr_validate(&cw20_msg.sender)?;
    let tokens_input = cw20_msg.amount.u128();

    if tokens_input == 0 {
        return Err(ContractError::Std(StdError::generic_err("Zero token amount")));
    }

    // Get per-curve constants (or legacy fallback)
    let (_vx, vt, k, _toc, _tlp) = get_curve_constants(&curve);

    // Calculate BWICK out using constant product curve
    let (bwick_out, total_fee) = calculate_sell_cp(
        curve.tokens_sold,
        tokens_input,
        config.sell_fee_bps,
        vt,
        k,
    )?;
    // All fees stay in reserves (LP) — no burn, no creator share

    // Cap at available reserves if curve math exceeds actual balance
    // (can happen for curves created before a curve-type migration)
    let mut bwick_out = bwick_out;
    if bwick_out > curve.bwick_reserves {
        bwick_out = curve.bwick_reserves;
    }

    // Check slippage (after reserves cap so user sees realistic amount)
    if bwick_out < sell_msg.min_bwick_out.u128() {
        return Err(ContractError::SlippageExceeded {
            expected: sell_msg.min_bwick_out.u128(),
            actual: bwick_out,
        });
    }

    // Update curve state
    curve.tokens_sold -= tokens_input;
    curve.bwick_reserves -= bwick_out; // Only bwick_out leaves; fees stay in reserves

    // Save curve
    CURVES.save(deps.storage, &token_addr, &curve)?;

    // Send BWICK to seller
    let send_msg = BankMsg::Send {
        to_address: user_addr.to_string(),
        amount: vec![Coin {
            denom: "ubwick".to_string(),
            amount: Uint128::from(bwick_out),
        }],
    };

    Ok(Response::new()
        .add_message(send_msg)
        .add_attribute("action", "sell")
        .add_attribute("seller", user_addr.to_string())
        .add_attribute("token_address", token_addr.to_string())
        .add_attribute("tokens_input", tokens_input.to_string())
        .add_attribute("bwick_out", bwick_out.to_string())
        .add_attribute("fee_to_lp", total_fee.to_string()))
}

/// AMM ExecuteMsg for cross-contract calls
#[cw_serde]
pub enum AmmExecuteMsg {
    CreatePool {
        token_address: String,
        bwick_amount: Uint128,
        token_amount: Uint128,
        /// Optional augmented fee in basis points (100 = 1%)
        augmented_fee_bps: Option<u16>,
        /// Optional target pool value in ubwick for augmented fee auto-disable
        lp_target_ubwick: Option<String>,
        /// v4: optional unix timestamp at which the locked LP seed
        /// becomes withdrawable. Missing = permanent lock.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        locked_lp_unlock_at: Option<u64>,
        /// v4: address allowed to withdraw the locked LP after unlock.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        locked_lp_recipient: Option<String>,
    },
}

/// Default augmented fee for graduated pools: 1% (100 bps)
const GRADUATION_AUGMENTED_FEE_BPS: u16 = 100;
/// LP target multiplier: pool must grow 10x before augmented fee disables
const GRADUATION_LP_TARGET_MULTIPLIER: u128 = 10;

fn execute_graduate(
    deps: DepsMut,
    env: Env,
    _info: MessageInfo,
    token_address: String,
) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    let token_addr = deps.api.addr_validate(&token_address)?;

    // Load curve
    let mut curve = CURVES.load(deps.storage, &token_addr)
        .map_err(|_| ContractError::CurveNotFound { token: token_address.clone() })?;

    // Check not already graduated
    if curve.graduated {
        return Err(ContractError::AlreadyGraduated {});
    }

    // Check threshold reached using effective threshold
    let threshold = load_effective_threshold(deps.as_ref(), &config, &curve);
    if curve.bwick_reserves < threshold {
        return Err(ContractError::Std(StdError::generic_err(format!(
            "Graduation threshold not reached: {} / {} BWICK",
            curve.bwick_reserves, threshold
        ))));
    }

    // Mark as graduated (closes the curve)
    curve.graduated = true;
    CURVES.save(deps.storage, &token_addr, &curve)?;

    // Anti-vampire: lock the (normalized) name + symbol forever. The token
    // now lives on the AMM and has holders; allowing a fork to claim the
    // same branding would let an attacker drain trust without effort.
    let now_secs = env.block.time.seconds();
    let name_key = normalize_for_cooldown(&curve.metadata.name);
    let symbol_key = normalize_for_cooldown(&curve.metadata.symbol);
    if !name_key.is_empty() {
        NAME_PERMA_BLOCK.save(deps.storage, &name_key, &now_secs)?;
    }
    if !symbol_key.is_empty() {
        SYMBOL_PERMA_BLOCK.save(deps.storage, &symbol_key, &now_secs)?;
    }

    // Calculate AMM pool liquidity
    // All BWICK reserves + all remaining tokens go to AMM
    let bwick_for_pool = curve.bwick_reserves;
    let tokens_remaining = TOTAL_SUPPLY - curve.tokens_sold;

    // Transfer remaining tokens from this contract to AMM
    let transfer_tokens_msg = WasmMsg::Execute {
        contract_addr: token_address.clone(),
        msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer {
            recipient: config.amm_contract.to_string(),
            amount: Uint128::from(tokens_remaining),
        })?,
        funds: vec![],
    };

    // Create AMM pool with BWICK funds and augmented fee protection
    let lp_target = bwick_for_pool
        .checked_mul(GRADUATION_LP_TARGET_MULTIPLIER)
        .unwrap_or(bwick_for_pool);
    // v4: forward the lp_lock duration set at CreateToken time. The
    // unlock timestamp is "now + lp_lock_seconds"; the recipient is
    // the token's creator. None / Some(0) => leave as permanent lock.
    let (locked_lp_unlock_at, locked_lp_recipient) = match curve.lp_lock_seconds {
        Some(secs) if secs > 0 => (
            Some(env.block.time.seconds().saturating_add(secs)),
            Some(curve.creator.to_string()),
        ),
        _ => (None, None),
    };
    let create_pool_msg = WasmMsg::Execute {
        contract_addr: config.amm_contract.to_string(),
        msg: to_binary(&AmmExecuteMsg::CreatePool {
            token_address: token_address.clone(),
            bwick_amount: Uint128::from(bwick_for_pool),
            token_amount: Uint128::from(tokens_remaining),
            augmented_fee_bps: Some(GRADUATION_AUGMENTED_FEE_BPS),
            lp_target_ubwick: Some(lp_target.to_string()),
            locked_lp_unlock_at,
            locked_lp_recipient,
        })?,
        funds: vec![Coin {
            denom: "ubwick".to_string(),
            amount: Uint128::from(bwick_for_pool),
        }],
    };

    Ok(Response::new()
        .add_message(transfer_tokens_msg)
        .add_message(create_pool_msg)
        .add_attribute("action", "graduate")
        .add_attribute("token_address", token_address)
        .add_attribute("bwick_for_pool", bwick_for_pool.to_string())
        .add_attribute("tokens_for_pool", tokens_remaining.to_string())
        .add_attribute("creator", curve.creator.to_string()))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result<Response, ContractError> {
    match msg.id {
        REPLY_CW20_INSTANTIATE => reply_cw20_instantiate(deps, env, msg),
        REPLY_VESTING_INSTANTIATE => reply_vesting_instantiate(deps, env, msg),
        _ => Err(ContractError::Std(StdError::generic_err("Unknown reply id"))),
    }
}

// v4: parses the freshly-instantiated bwick-vesting contract address,
// records it on the PresaleState, and funds it via CW20 Send (which
// triggers vesting's Receive hook to lock in the schedule total).
fn reply_vesting_instantiate(
    deps: DepsMut,
    _env: Env,
    msg: Reply,
) -> Result<Response, ContractError> {
    let res = parse_instantiate_response_data(
        msg.result
            .into_result()
            .map_err(|e| ContractError::Std(StdError::generic_err(e)))?
            .data
            .ok_or_else(|| {
                ContractError::Std(StdError::generic_err(
                    "vesting reply missing data",
                ))
            })?
            .as_slice(),
    )
    .map_err(|e| ContractError::Std(StdError::generic_err(e.to_string())))?;
    let vesting_addr = deps.api.addr_validate(&res.contract_address)?;

    let pending = PENDING_FINALIZE.load(deps.storage)?;
    PENDING_FINALIZE.remove(deps.storage);

    // Record the vesting contract address on the presale state.
    let mut state = PRESALES.load(deps.storage, &pending.token_address)?;
    state.creator_vesting_contract = Some(vesting_addr.clone());
    PRESALES.save(deps.storage, &pending.token_address, &state)?;
    VESTING_INSTANCES.save(deps.storage, &pending.token_address, &vesting_addr)?;

    // Fund the vesting account via CW20 Send. The vesting contract
    // accepts the Receive hook once and pins the schedule.
    let curve = CURVES.load(deps.storage, &pending.token_address)?;
    let send_msg = WasmMsg::Execute {
        contract_addr: curve.token_address.to_string(),
        msg: to_binary(&cw20::Cw20ExecuteMsg::Send {
            contract: vesting_addr.to_string(),
            amount: Uint128::from(pending.vesting_funding_utokens),
            msg: Binary::default(),
        })?,
        funds: vec![],
    };

    Ok(Response::new()
        .add_message(send_msg)
        .add_attribute("action", "vesting_instantiate_reply")
        .add_attribute("vesting_contract", vesting_addr.to_string())
        .add_attribute("funded_utokens", pending.vesting_funding_utokens.to_string()))
}

fn reply_cw20_instantiate(
    deps: DepsMut,
    env: Env,
    msg: Reply,
) -> Result<Response, ContractError> {
    // Parse reply to get new contract address
    let response = msg.result.into_result().map_err(StdError::generic_err)?;

    // CosmWasm 1.x uses data field (2.0+ uses msg_responses)
    let data = response.data
        .ok_or_else(|| ContractError::Std(StdError::generic_err("No instantiate response data found")))?;

    let res = parse_instantiate_response_data(&data.as_slice())
        .map_err(|e| ContractError::Std(StdError::generic_err(format!("Parse error: {}", e))))?;

    let token_address = deps.api.addr_validate(&res.contract_address)?;

    // Load pending curve data
    let pending = PENDING_CURVE.load(deps.storage)?;
    PENDING_CURVE.remove(deps.storage);

    // Create curve with creation fee as initial BWICK reserves
    let grad_threshold = if pending.graduation_threshold_ubwick > 0 {
        Some(pending.graduation_threshold_ubwick)
    } else {
        None
    };
    let curve = crate::state::Curve {
        token_address: token_address.clone(),
        metadata: pending.metadata.clone(),
        creator: pending.creator.clone(),
        tokens_sold: 0,
        bwick_reserves: pending.initial_bwick,
        graduated: false,
        created_at: env.block.height,
        creator_fees_earned: 0,
        graduation_threshold_ubwick: grad_threshold,
        virtual_bwick_start: pending.virtual_bwick_start,
        virtual_tokens_start: pending.virtual_tokens_start,
        curve_k: pending.curve_k,
        tokens_on_curve: pending.tokens_on_curve,
        tokens_for_lp: pending.tokens_for_lp,
        lp_lock_seconds: pending.lp_lock_seconds,
    };

    // Save curve indexed by token address
    CURVES.save(deps.storage, &token_address, &curve)?;

    // v4: if a presale config was attached at CreateToken, persist it
    // here so BuyPresale + FinalizePresale can find it. The token's
    // bonding curve is left in place but gated by the PresaleState
    // until FinalizePresale flips `finalized = true`.
    if let Some(cfg) = pending.presale.clone() {
        PRESALES.save(
            deps.storage,
            &token_address,
            &crate::state::PresaleState {
                config: cfg,
                bwick_raised: 0,
                tokens_sold: 0,
                finalized: false,
                failed: false,
                escrowed: true,
                creator_vesting_contract: None,
            },
        )?;
    }

    // Seed AppliedMetadata with the creator's values so initial reads + the
    // first ApplyMetadata after votes accumulate have a baseline. The 48h
    // voting window starts now (created_at_secs = now).
    let now_secs = env.block.time.seconds();
    APPLIED_METADATA.save(
        deps.storage,
        &token_address,
        &AppliedMetadata {
            name: curve.metadata.name.clone(),
            symbol: curve.metadata.symbol.clone(),
            image: curve.metadata.image.clone(),
            description: curve.metadata.description.clone(),
            last_applied_at: now_secs,
            created_at_secs: now_secs,
            first_vote_at: 0,
            apply_count: 0,
            twitter: social_at(&curve.metadata.social_links, 0),
            telegram: social_at(&curve.metadata.social_links, 1),
            website: social_at(&curve.metadata.social_links, 2),
        },
    )?;

    Ok(Response::new()
        .add_attribute("action", "token_created")
        .add_attribute("token_address", token_address.to_string())
        .add_attribute("creator", pending.creator)
        .add_attribute("initial_reserves", pending.initial_bwick.to_string()))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::Curve { token_address } => {
            to_binary(&query_curve(deps, token_address)?)
        }
        QueryMsg::AllCurves { start_after, limit } => {
            to_binary(&query_all_curves(deps, start_after, limit)?)
        }
        QueryMsg::Progress { token_address } => {
            to_binary(&query_progress(deps, token_address)?)
        }
        QueryMsg::Config {} => {
            to_binary(&query_config(deps)?)
        }
        QueryMsg::SimulateBuy { token_address, bwick_amount } => {
            to_binary(&query_simulate_buy(deps, token_address, bwick_amount)?)
        }
        QueryMsg::SimulateSell { token_address, token_amount } => {
            to_binary(&query_simulate_sell(deps, token_address, token_amount)?)
        }
        QueryMsg::Oracle {} => {
            to_binary(&query_oracle(deps)?)
        }
        QueryMsg::MetadataState { token_address } => {
            to_binary(&query_metadata_state(deps, env, token_address)?)
        }
        QueryMsg::MetadataVote { token_address, voter } => {
            to_binary(&query_metadata_vote(deps, token_address, voter)?)
        }
        QueryMsg::Presale { token_address } => {
            to_binary(&query_presale(deps, env, token_address)?)
        }
        QueryMsg::PresaleContribution { token_address, buyer } => {
            to_binary(&query_presale_contribution(deps, token_address, buyer)?)
        }
    }
}


/// v7: pay out an escrowed presale position after finalize.
/// Failed presale: full BWICK refund. Successful: tokens at the presale
/// price for the pro-rata ACCEPTED portion, plus refund of any surplus
/// (oversubscription past the hard cap).
fn execute_claim_presale(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    token_address: String,
) -> Result<Response, ContractError> {
    let token_addr = deps.api.addr_validate(&token_address)?;
    let state = PRESALES
        .may_load(deps.storage, &token_addr)?
        .ok_or_else(|| ContractError::NoPresale { token: token_address.clone() })?;

    if !state.finalized {
        return Err(ContractError::PresaleNotFinalizedForClaim {});
    }
    if !state.escrowed {
        // Legacy presales delivered tokens at buy time; nothing to claim.
        return Err(ContractError::PresaleNothingToClaim {});
    }
    let contribution = PRESALE_CONTRIBUTIONS
        .may_load(deps.storage, (&token_addr, &info.sender))?
        .unwrap_or(0);
    if contribution == 0 {
        return Err(ContractError::PresaleNothingToClaim {});
    }
    if PRESALE_CLAIMED
        .may_load(deps.storage, (&token_addr, &info.sender))?
        .unwrap_or(false)
    {
        return Err(ContractError::PresaleAlreadyClaimed {});
    }

    let mut response = Response::new()
        .add_attribute("action", "claim_presale")
        .add_attribute("token_address", token_address.clone())
        .add_attribute("claimer", info.sender.to_string());

    if state.failed {
        // Full refund.
        response = response
            .add_message(cosmwasm_std::BankMsg::Send {
                to_address: info.sender.to_string(),
                amount: vec![cosmwasm_std::Coin {
                    denom: "ubwick".to_string(),
                    amount: Uint128::from(contribution),
                }],
            })
            .add_attribute("refund_ubwick", contribution.to_string())
            .add_attribute("tokens_out", "0");
    } else {
        // Pro-rata acceptance: accepted_i = contribution_i * accepted_total / raised.
        let accepted_total = state.bwick_raised.min(state.config.hard_cap_ubwick);
        let accepted = if state.bwick_raised == 0 {
            0
        } else {
            // contribution and raise both fit in u128; use checked math via
            // widening through u128 mul-div (contribution <= raised, so the
            // product fits if contribution * accepted_total does; clamp via
            // u128::checked_mul fallback to proportional division order).
            contribution
                .checked_mul(accepted_total)
                .map(|product| product / state.bwick_raised)
                .unwrap_or_else(|| (contribution / state.bwick_raised.max(1)).saturating_mul(accepted_total))
        };
        let refund = contribution.saturating_sub(accepted);
        let tokens_out = accepted / state.config.price_ubwick_per_utoken.max(1);

        let curve = CURVES
            .load(deps.storage, &token_addr)
            .map_err(|_| ContractError::CurveNotFound { token: token_address.clone() })?;
        if tokens_out > 0 {
            response = response.add_message(WasmMsg::Execute {
                contract_addr: curve.token_address.to_string(),
                msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer {
                    recipient: info.sender.to_string(),
                    amount: Uint128::from(tokens_out),
                })?,
                funds: vec![],
            });
        }
        if refund > 0 {
            response = response.add_message(cosmwasm_std::BankMsg::Send {
                to_address: info.sender.to_string(),
                amount: vec![cosmwasm_std::Coin {
                    denom: "ubwick".to_string(),
                    amount: Uint128::from(refund),
                }],
            });
        }
        response = response
            .add_attribute("refund_ubwick", refund.to_string())
            .add_attribute("tokens_out", tokens_out.to_string());
    }

    PRESALE_CLAIMED.save(deps.storage, (&token_addr, &info.sender), &true)?;
    Ok(response)
}

fn query_presale(
    deps: Deps,
    env: Env,
    token_address: String,
) -> StdResult<PresaleResponse> {
    let token_addr = deps.api.addr_validate(&token_address)?;
    let state = PRESALES
        .may_load(deps.storage, &token_addr)?
        .ok_or_else(|| StdError::not_found(format!("Presale for {}", token_address)))?;
    let now = env.block.time.seconds();
    let is_active = !state.finalized
        && state.config.start_time <= now
        && now < state.config.end_time;
    let can_finalize = !state.finalized
        && (now >= state.config.end_time || state.bwick_raised >= state.config.hard_cap_ubwick);
    Ok(PresaleResponse {
        token_address: token_addr,
        config: state.config.clone(),
        bwick_raised: Uint128::from(state.bwick_raised),
        tokens_sold: Uint128::from(state.tokens_sold),
        finalized: state.finalized,
        failed: state.failed,
        escrowed: state.escrowed,
        creator_vesting_contract: state.creator_vesting_contract,
        is_active,
        can_finalize,
        now_seconds: now,
    })
}

fn query_presale_contribution(
    deps: Deps,
    token_address: String,
    buyer: String,
) -> StdResult<PresaleContributionResponse> {
    let token_addr = deps.api.addr_validate(&token_address)?;
    let buyer_addr = deps.api.addr_validate(&buyer)?;
    let bwick_contributed = PRESALE_CONTRIBUTIONS
        .may_load(deps.storage, (&token_addr, &buyer_addr))?
        .unwrap_or(0);
    // tokens_received is derivable from contribution / price; the
    // dedicated handler will record it directly in a follow-up commit.
    // For now: compute the floor of (contribution / price) if presale
    // exists, else zero.
    let claimed = PRESALE_CLAIMED
        .may_load(deps.storage, (&token_addr, &buyer_addr))?
        .unwrap_or(false);
    let (tokens_received, refundable) = match PRESALES.may_load(deps.storage, &token_addr)? {
        Some(state) if bwick_contributed > 0 => {
            if state.finalized && state.failed {
                (0, bwick_contributed)
            } else if state.finalized && state.escrowed {
                let accepted_total = state.bwick_raised.min(state.config.hard_cap_ubwick);
                let accepted = if state.bwick_raised == 0 {
                    0
                } else {
                    bwick_contributed
                        .checked_mul(accepted_total)
                        .map(|product| product / state.bwick_raised)
                        .unwrap_or(0)
                };
                (
                    accepted / state.config.price_ubwick_per_utoken.max(1),
                    bwick_contributed.saturating_sub(accepted),
                )
            } else {
                // Pre-finalize estimate (or legacy instant-delivery presale).
                (bwick_contributed / state.config.price_ubwick_per_utoken.max(1), 0)
            }
        }
        _ => (0, 0),
    };
    Ok(PresaleContributionResponse {
        buyer: buyer_addr,
        token_address: token_addr,
        bwick_contributed: Uint128::from(bwick_contributed),
        tokens_received: Uint128::from(tokens_received),
        claimed,
        refundable_ubwick: Uint128::from(refundable),
    })
}

fn query_metadata_state(
    deps: Deps,
    _env: Env,
    token_address: String,
) -> StdResult<MetadataStateResponse> {
    let token = deps.api.addr_validate(&token_address)?;
    // If the curve doesn't exist, propagate a StdError so the dApp shows
    // a clean "token not found" rather than a generic decode failure.
    if !CURVES.has(deps.storage, &token) {
        return Err(StdError::generic_err("token not found"));
    }
    // Pull applied metadata. If somehow missing (pre-v3 storage), fall back
    // to the Curve's stored metadata.
    let applied = APPLIED_METADATA
        .may_load(deps.storage, &token)?
        .unwrap_or_else(|| {
            let curve = CURVES.load(deps.storage, &token).unwrap_or_else(|_| {
                // unreachable — we checked .has above
                Curve {
                    token_address: token.clone(),
                    metadata: TokenMetadata {
                        name: String::new(),
                        symbol: String::new(),
                        image: String::new(),
                        description: String::new(),
                        social_links: vec![],
                    },
                    creator: token.clone(),
                    tokens_sold: 0,
                    bwick_reserves: 0,
                    graduated: false,
                    created_at: 0,
                    creator_fees_earned: 0,
                    graduation_threshold_ubwick: None,
                    virtual_bwick_start: 0,
                    virtual_tokens_start: 0,
                    curve_k: 0,
                    tokens_on_curve: 0,
                    tokens_for_lp: 0,
                    lp_lock_seconds: None,
                }
            });
            let twitter = social_at(&curve.metadata.social_links, 0);
            let telegram = social_at(&curve.metadata.social_links, 1);
            let website = social_at(&curve.metadata.social_links, 2);
            AppliedMetadata {
                name: curve.metadata.name,
                symbol: curve.metadata.symbol,
                image: curve.metadata.image,
                description: curve.metadata.description,
                last_applied_at: 0,
                created_at_secs: 0,
                first_vote_at: 0,
                apply_count: 0,
                twitter,
                telegram,
                website,
            }
        });

    let make_field = |field: &str, applied_value: &str| -> StdResult<MetadataFieldState> {
        let (leading, leading_w, total) =
            find_leader(deps, &token, field, applied_value)?;
        Ok(MetadataFieldState {
            applied: applied_value.to_string(),
            leading,
            leading_weight: Uint128::from(leading_w),
            total_weight: Uint128::from(total),
        })
    };
    let name = make_field("n", &applied.name)?;
    let symbol = make_field("s", &applied.symbol)?;
    let image = make_field("i", &applied.image)?;
    let description = make_field("d", &applied.description)?;

    // First apply: 10-min cooldown from first_vote_at. After votes have
    // started, the countdown is real; if nobody has voted yet the dApp
    // sees u64::MAX-shape sentinel and renders "waiting for first vote".
    let next_apply_eligible_at = if applied.apply_count == 0 {
        if applied.first_vote_at == 0 {
            0 // sentinel: countdown not started; dApp renders the wait state
        } else {
            applied
                .first_vote_at
                .saturating_add(METADATA_FIRST_APPLY_DELAY_SECS)
        }
    } else {
        applied.created_at_secs
    };
    let voting_closes_at = applied
        .created_at_secs
        .saturating_add(METADATA_VOTING_WINDOW_SECS);
    Ok(MetadataStateResponse {
        token_address: token,
        name,
        symbol,
        image,
        description,
        last_applied_at: applied.last_applied_at,
        next_apply_eligible_at,
        apply_count: applied.apply_count,
        created_at_secs: applied.created_at_secs,
        voting_closes_at,
        first_vote_at: applied.first_vote_at,
    })
}

fn query_metadata_vote(
    deps: Deps,
    token_address: String,
    voter: String,
) -> StdResult<MetadataVoteResponse> {
    let token = deps.api.addr_validate(&token_address)?;
    let voter_addr = deps.api.addr_validate(&voter)?;
    let v = METADATA_VOTES
        .may_load(deps.storage, (&token, &voter_addr))?
        .unwrap_or_default();
    Ok(MetadataVoteResponse {
        voter: voter_addr,
        name: v.name,
        symbol: v.symbol,
        image: v.image,
        description: v.description,
        weight: Uint128::from(v.weight),
    })
}

fn query_curve(deps: Deps, token_address: String) -> StdResult<CurveResponse> {
    let token_addr = deps.api.addr_validate(&token_address)?;
    let curve = CURVES.load(deps.storage, &token_addr)
        .map_err(|_| StdError::not_found(format!("Curve for token {}", token_address)))?;

    let (vx, vt, k, toc, tlp) = get_curve_constants(&curve);
    let tokens_remaining = toc.saturating_sub(curve.tokens_sold);
    let current_price = calculate_price_cp(curve.tokens_sold, vt, k);

    Ok(CurveResponse {
        token_address: curve.token_address,
        metadata: curve.metadata,
        creator: curve.creator,
        tokens_sold: Uint128::from(curve.tokens_sold),
        tokens_remaining: Uint128::from(tokens_remaining),
        bwick_reserves: Uint128::from(curve.bwick_reserves),
        current_price: format!("{:.6}", current_price as f64 / 1_000_000.0),
        graduated: curve.graduated,
        created_at: curve.created_at,
        virtual_bwick_start: Uint128::from(vx),
        virtual_tokens_start: Uint128::from(vt),
        tokens_on_curve: Uint128::from(toc),
        tokens_for_lp: Uint128::from(tlp),
    })
}

fn query_all_curves(
    deps: Deps,
    start_after: Option<String>,
    limit: Option<u32>,
) -> StdResult<AllCurvesResponse> {
    let limit = limit.unwrap_or(10).min(30) as usize;
    let start = start_after
        .map(|s| deps.api.addr_validate(&s))
        .transpose()?;

    let curves: Vec<CurveResponse> = CURVES
        .range(
            deps.storage,
            start.as_ref().map(cw_storage_plus::Bound::exclusive),
            None,
            cosmwasm_std::Order::Ascending,
        )
        .take(limit)
        .map(|item| {
            let (_, curve) = item?;
            let (vx, vt, k, toc, tlp) = get_curve_constants(&curve);
            let tokens_remaining = toc.saturating_sub(curve.tokens_sold);
            let current_price = calculate_price_cp(curve.tokens_sold, vt, k);
            Ok(CurveResponse {
                token_address: curve.token_address,
                metadata: curve.metadata,
                creator: curve.creator,
                tokens_sold: Uint128::from(curve.tokens_sold),
                tokens_remaining: Uint128::from(tokens_remaining),
                bwick_reserves: Uint128::from(curve.bwick_reserves),
                current_price: format!("{:.6}", current_price as f64 / 1_000_000.0),
                graduated: curve.graduated,
                created_at: curve.created_at,
                virtual_bwick_start: Uint128::from(vx),
                virtual_tokens_start: Uint128::from(vt),
                tokens_on_curve: Uint128::from(toc),
                tokens_for_lp: Uint128::from(tlp),
            })
        })
        .collect::<StdResult<Vec<_>>>()?;

    Ok(AllCurvesResponse { curves })
}

fn query_progress(deps: Deps, token_address: String) -> StdResult<ProgressResponse> {
    let token_addr = deps.api.addr_validate(&token_address)?;
    let curve = CURVES.load(deps.storage, &token_addr)
        .map_err(|_| StdError::not_found(format!("Curve for token {}", token_address)))?;
    let config = CONFIG.load(deps.storage)?;

    let threshold = load_effective_threshold(deps, &config, &curve);
    let progress_percent = if threshold > 0 {
        (curve.bwick_reserves as f64 / threshold as f64 * 100.0).min(100.0)
    } else {
        100.0
    };

    let (_vx, _vt, _k, toc, _tlp) = get_curve_constants(&curve);
    let tokens_remaining = toc.saturating_sub(curve.tokens_sold);

    Ok(ProgressResponse {
        token_address: curve.token_address,
        bwick_raised: Uint128::from(curve.bwick_reserves),
        graduation_threshold: Uint128::from(threshold),
        progress_percent: format!("{:.2}%", progress_percent),
        tokens_sold: Uint128::from(curve.tokens_sold),
        tokens_remaining: Uint128::from(tokens_remaining),
        graduated: curve.graduated,
    })
}

fn query_config(deps: Deps) -> StdResult<ConfigResponse> {
    let config = CONFIG.load(deps.storage)?;
    Ok(ConfigResponse {
        amm_contract: config.amm_contract,
        cw20_code_id: config.cw20_code_id,
        creation_fee: Uint128::from(config.creation_fee),
        graduation_threshold: Uint128::from(config.graduation_threshold),
        buy_fee_bps: config.buy_fee_bps,
        sell_fee_bps: config.sell_fee_bps,
        admin: config.admin,
        target_graduation_usd: Uint128::from(config.target_graduation_usd),
        min_graduation_threshold: Uint128::from(config.min_graduation_threshold),
        max_graduation_threshold: Uint128::from(config.max_graduation_threshold),
        target_starting_mc_usd: Uint128::from(config.target_starting_mc_usd),
        target_raised_usd: Uint128::from(config.target_raised_usd),
        target_raised_bwick: Uint128::from(config.target_raised_bwick),
        max_wallet_bps: config.max_wallet_bps,
        fee_exempt_address: config.fee_exempt_address,
    })
}

fn query_oracle(deps: Deps) -> StdResult<OracleResponse> {
    // Prefer the live oracle contract's TWAP if configured.
    let config = CONFIG.load(deps.storage)?;
    if !config.oracle_contract.as_str().is_empty() {
        let req = OracleTwapQueryMsg {
            twap: OracleTwapArgs { window_seconds: Some(60) },
        };
        if let Ok(resp) = deps.querier.query_wasm_smart::<OracleTwapResponse>(
            config.oracle_contract.as_str(),
            &req,
        ) {
            return Ok(OracleResponse {
                bwick_usd_price: resp.bwick_usd_price,
                last_update_height: resp.last_update_height,
                last_update_timestamp: resp.last_update_timestamp,
            });
        }
    }
    // Fallback to legacy local oracle.
    let oracle = ORACLE_STATE.may_load(deps.storage)?
        .unwrap_or(OracleState {
            bwick_usd_price: 0,
            last_update_height: 0,
            last_update_timestamp: 0,
        });
    Ok(OracleResponse {
        bwick_usd_price: Uint128::from(oracle.bwick_usd_price),
        last_update_height: oracle.last_update_height,
        last_update_timestamp: oracle.last_update_timestamp,
    })
}

fn query_simulate_buy(
    deps: Deps,
    token_address: String,
    bwick_amount: Uint128,
) -> StdResult<SimulateBuyResponse> {
    let token_addr = deps.api.addr_validate(&token_address)?;
    let curve = CURVES.load(deps.storage, &token_addr)
        .map_err(|_| StdError::not_found(format!("Curve for token {}", token_address)))?;
    let config = CONFIG.load(deps.storage)?;

    if curve.graduated {
        return Err(StdError::generic_err("Curve already graduated - use AMM for trading"));
    }

    let (_vx, vt, k, toc, _tlp) = get_curve_constants(&curve);

    let (tokens_out, fee) = calculate_buy_cp(curve.tokens_sold, bwick_amount.u128(), config.buy_fee_bps, vt, k, toc)
        .map_err(|e| StdError::generic_err(format!("Simulation failed: {:?}", e)))?;

    let new_sold = curve.tokens_sold + tokens_out;
    let new_price = calculate_price_cp(new_sold, vt, k);

    Ok(SimulateBuyResponse {
        tokens_out: Uint128::from(tokens_out),
        fee_amount: Uint128::from(fee),
        new_price: format!("{:.6}", new_price as f64 / 1_000_000.0),
    })
}

fn query_simulate_sell(
    deps: Deps,
    token_address: String,
    token_amount: Uint128,
) -> StdResult<SimulateSellResponse> {
    let token_addr = deps.api.addr_validate(&token_address)?;
    let curve = CURVES.load(deps.storage, &token_addr)
        .map_err(|_| StdError::not_found(format!("Curve for token {}", token_address)))?;
    let config = CONFIG.load(deps.storage)?;

    if curve.graduated {
        return Err(StdError::generic_err("Curve already graduated - use AMM for trading"));
    }

    let (_vx, vt, k, _toc, _tlp) = get_curve_constants(&curve);

    let (bwick_out, total_fee) = calculate_sell_cp(
        curve.tokens_sold,
        token_amount.u128(),
        config.sell_fee_bps,
        vt,
        k,
    ).map_err(|e| StdError::generic_err(format!("Simulation failed: {:?}", e)))?;

    // Cap at available reserves (matches execute path behavior)
    let mut bwick_out = bwick_out;
    if bwick_out > curve.bwick_reserves {
        bwick_out = curve.bwick_reserves;
    }

    let new_sold = curve.tokens_sold - token_amount.u128();
    let new_price = calculate_price_cp(new_sold, vt, k);

    Ok(SimulateSellResponse {
        bwick_out: Uint128::from(bwick_out),
        fee_amount: Uint128::from(total_fee),
        burned_amount: Uint128::zero(),
        new_price: format!("{:.6}", new_price as f64 / 1_000_000.0),
    })
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(
    deps: DepsMut,
    _env: Env,
    msg: MigrateMsg,
) -> Result<Response, ContractError> {
    // Parametric migrate: the wasm-level admin can selectively reset Config
    // fields. Designed so that a previously-corrupted Config (e.g. clobbered
    // by a hardcoded migration in an earlier version) can be repaired by the
    // operator who controls the wasm admin without re-instantiating.
    let mut config = CONFIG.load(deps.storage)?;
    if let Some(addr) = msg.admin {
        config.admin = if addr.is_empty() { Addr::unchecked("") } else { deps.api.addr_validate(&addr)? };
    }
    if let Some(addr) = msg.oracle_contract {
        config.oracle_contract = if addr.is_empty() { Addr::unchecked("") } else { deps.api.addr_validate(&addr)? };
    }
    if let Some(addr) = msg.fee_exempt_address {
        config.fee_exempt_address = if addr.is_empty() { Addr::unchecked("") } else { deps.api.addr_validate(&addr)? };
    }
    if let Some(v) = msg.creation_fee { config.creation_fee = v.u128(); }
    if let Some(v) = msg.graduation_threshold { config.graduation_threshold = v.u128(); }
    if let Some(v) = msg.min_graduation_threshold { config.min_graduation_threshold = v.u128(); }
    if let Some(v) = msg.max_graduation_threshold { config.max_graduation_threshold = v.u128(); }
    if let Some(v) = msg.target_graduation_usd { config.target_graduation_usd = v.u128(); }
    if let Some(v) = msg.target_starting_mc_usd { config.target_starting_mc_usd = v.u128(); }
    if let Some(v) = msg.target_raised_usd { config.target_raised_usd = v.u128(); }
    if let Some(v) = msg.buy_fee_bps { config.buy_fee_bps = v; }
    if let Some(v) = msg.sell_fee_bps { config.sell_fee_bps = v; }
    if let Some(v) = msg.creator_fee_share_bps { config.creator_fee_share_bps = v; }
    if let Some(v) = msg.max_wallet_bps { config.max_wallet_bps = v; }
    CONFIG.save(deps.storage, &config)?;

    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
    Ok(Response::new()
        .add_attribute("action", "migrate")
        .add_attribute("version", CONTRACT_VERSION))
}

// ── v4: presale handlers + helpers ───────────────────────────────────────
//
// Status: validation + dispatch wiring is complete. Buy/Finalize logic is
// currently STUB (returns a clear error). Follow-up commit lands:
//   - BuyPresale: window/cap/Merkle gate, CW20 transfer to buyer, accounting
//   - FinalizePresale: vesting instantiate (SubMsg + reply), curve unlock
//   - reply_vesting_instantiate (REPLY_VESTING_INSTANTIATE)
//   - Public Buy/Sell gating when an unfinalized presale exists
//   - lp_lock_seconds propagation into AmmExecuteMsg::CreatePool
//
// Stub keeps the contract compileable + the message surface advertised so
// frontends + SDKs can integrate against the type shape in parallel.

/// Convention from `social_links` Vec into the three named slots used by
/// metadata voting. Position 0=twitter, 1=telegram, 2=website. Anything
/// beyond index 2 is ignored. Missing slots become empty strings.
fn social_at(social_links: &[String], idx: usize) -> String {
    social_links.get(idx).cloned().unwrap_or_default()
}

fn validate_presale_config(
    cfg: &PresaleConfig,
    now_seconds: u64,
) -> Result<(), ContractError> {
    if cfg.price_ubwick_per_utoken == 0 {
        return Err(ContractError::InvalidPresaleConfig {
            reason: "price_ubwick_per_utoken must be > 0".into(),
        });
    }
    if cfg.hard_cap_ubwick == 0 {
        return Err(ContractError::InvalidPresaleConfig {
            reason: "hard_cap_ubwick must be > 0".into(),
        });
    }
    if cfg.end_time <= cfg.start_time {
        return Err(ContractError::InvalidPresaleConfig {
            reason: "end_time must be > start_time".into(),
        });
    }
    if cfg.end_time <= now_seconds {
        return Err(ContractError::InvalidPresaleConfig {
            reason: "end_time must be in the future".into(),
        });
    }
    if let Some(cap) = cfg.per_wallet_cap_ubwick {
        if cap == 0 || cap > cfg.hard_cap_ubwick {
            return Err(ContractError::InvalidPresaleConfig {
                reason: "per_wallet_cap_ubwick must be in (0, hard_cap_ubwick]".into(),
            });
        }
    }
    if cfg.creator_allocation_bps > MAX_CREATOR_ALLOCATION_BPS {
        return Err(ContractError::CreatorAllocationTooHigh {
            bps: cfg.creator_allocation_bps,
            max_bps: MAX_CREATOR_ALLOCATION_BPS,
        });
    }
    Ok(())
}

/// SHA-256 a byte slice. Helper for Merkle leaf + node hashing.
fn sha256(input: &[u8]) -> [u8; 32] {
    use sha2::Digest;
    let mut h = sha2::Sha256::new();
    h.update(input);
    let out = h.finalize();
    let mut buf = [0u8; 32];
    buf.copy_from_slice(&out);
    buf
}

/// Verify a Merkle proof for `leaf` against `root`. Pair-hashing convention:
/// sort each (left, right) pair lexicographically before hashing, matching
/// the OpenZeppelin / standard EVM Merkle proof convention. `proof` is an
/// ordered list of sibling hashes from leaf to root.
fn merkle_verify(leaf: &[u8; 32], proof: &[Binary], root: &[u8; 32]) -> bool {
    let mut computed = *leaf;
    for sibling in proof {
        let sib_slice = sibling.as_slice();
        if sib_slice.len() != 32 {
            return false;
        }
        let mut concat = [0u8; 64];
        if computed.as_slice() <= sib_slice {
            concat[..32].copy_from_slice(&computed);
            concat[32..].copy_from_slice(sib_slice);
        } else {
            concat[..32].copy_from_slice(sib_slice);
            concat[32..].copy_from_slice(&computed);
        }
        computed = sha256(&concat);
    }
    computed == *root
}

/// Canonical leaf hash for an address. The off-chain allowlist tooling
/// must use the same hashing scheme: leaf = sha256(addr_str_utf8_bytes).
/// Using the validated bech32 string keeps the on-chain side simple and
/// avoids needing to canonicalize via deps.api.addr_canonicalize.
fn merkle_leaf_for(addr: &Addr) -> [u8; 32] {
    sha256(addr.as_str().as_bytes())
}

fn execute_buy_presale(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    token_address: String,
    merkle_proof: Vec<Binary>,
    min_tokens_out: Uint128,
) -> Result<Response, ContractError> {
    let token_addr = deps.api.addr_validate(&token_address)?;
    let mut state = PRESALES
        .may_load(deps.storage, &token_addr)?
        .ok_or_else(|| ContractError::NoPresale { token: token_address.clone() })?;

    if state.finalized {
        return Err(ContractError::PresaleAlreadyFinalized {});
    }

    // Window check.
    let now = env.block.time.seconds();
    if now < state.config.start_time {
        return Err(ContractError::PresaleNotStarted {
            start_time: state.config.start_time,
            now,
        });
    }
    if now >= state.config.end_time {
        return Err(ContractError::PresaleEnded {
            end_time: state.config.end_time,
            now,
        });
    }

    // Funds check — single BWICK coin, positive amount.
    let bwick_in = extract_bwick_from_funds(&info.funds)?;
    if bwick_in == 0 {
        return Err(ContractError::InsufficientFunds {});
    }

    // Hard-cap check. v7 oversubscribed presales keep accepting past the
    // cap (MetaDAO-style); acceptance is settled pro rata at finalize.
    let effective_in = if state.config.allow_oversubscription {
        bwick_in
    } else {
        let remaining_to_cap = state
            .config
            .hard_cap_ubwick
            .saturating_sub(state.bwick_raised);
        if remaining_to_cap == 0 {
            return Err(ContractError::PresaleHardCapReached {
                raised: state.bwick_raised,
                hard_cap: state.config.hard_cap_ubwick,
            });
        }
        // Clip the caller's input to whatever's still buyable.
        bwick_in.min(remaining_to_cap)
    };

    // Per-wallet cap (if set).
    if let Some(cap) = state.config.per_wallet_cap_ubwick {
        let prior = PRESALE_CONTRIBUTIONS
            .may_load(deps.storage, (&token_addr, &info.sender))?
            .unwrap_or(0);
        if prior.saturating_add(effective_in) > cap {
            return Err(ContractError::PresalePerWalletCapExceeded {
                contributing: effective_in,
                cap,
            });
        }
    }

    // Merkle gate — empty root means open presale, any proof ignored.
    let root = state.config.merkle_root;
    let zero_root = [0u8; 32];
    if root != zero_root {
        let leaf = merkle_leaf_for(&info.sender);
        if !merkle_verify(&leaf, &merkle_proof, &root) {
            return Err(ContractError::PresaleAllowlistRejected {});
        }
    }

    // Tokens out = effective_in / price_ubwick_per_utoken (truncate).
    let price = state.config.price_ubwick_per_utoken;
    if price == 0 {
        return Err(ContractError::InvalidPresaleConfig {
            reason: "price must be > 0".into(),
        });
    }
    let tokens_out = effective_in / price;
    if tokens_out == 0 {
        return Err(ContractError::Std(StdError::generic_err(
            "buy amount too small to receive any tokens at the configured price",
        )));
    }
    if tokens_out < min_tokens_out.u128() {
        return Err(ContractError::SlippageExceeded {
            expected: min_tokens_out.u128(),
            actual: tokens_out,
        });
    }

    // v7 escrow model: tokens stay with the launchpad until ClaimPresale
    // after finalize (needed for refunds + pro-rata). Legacy presales
    // (escrowed=false) transfer immediately, as before.
    let curve = CURVES
        .load(deps.storage, &token_addr)
        .map_err(|_| ContractError::CurveNotFound { token: token_address.clone() })?;
    let transfer_msg = if state.escrowed {
        None
    } else {
        Some(WasmMsg::Execute {
            contract_addr: curve.token_address.to_string(),
            msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer {
                recipient: info.sender.to_string(),
                amount: Uint128::from(tokens_out),
            })?,
            funds: vec![],
        })
    };

    // Update state.
    state.bwick_raised = state.bwick_raised.saturating_add(effective_in);
    state.tokens_sold = state.tokens_sold.saturating_add(tokens_out);
    PRESALES.save(deps.storage, &token_addr, &state)?;

    let prior_contribution = PRESALE_CONTRIBUTIONS
        .may_load(deps.storage, (&token_addr, &info.sender))?
        .unwrap_or(0);
    PRESALE_CONTRIBUTIONS.save(
        deps.storage,
        (&token_addr, &info.sender),
        &(prior_contribution.saturating_add(effective_in)),
    )?;

    // If the caller sent MORE than what we could accept (capped by
    // the hard cap), refund the surplus.
    let mut response = Response::new();
    if let Some(msg) = transfer_msg {
        response = response.add_message(msg);
    }
    response = response
        .add_attribute("action", "buy_presale")
        .add_attribute("buyer", info.sender.to_string())
        .add_attribute("token_address", token_address)
        .add_attribute("bwick_in", effective_in.to_string())
        .add_attribute("tokens_out", tokens_out.to_string())
        .add_attribute("bwick_raised", state.bwick_raised.to_string())
        .add_attribute("tokens_sold", state.tokens_sold.to_string());

    let refund = bwick_in.saturating_sub(effective_in);
    if refund > 0 {
        response = response
            .add_message(BankMsg::Send {
                to_address: info.sender.to_string(),
                amount: vec![Coin {
                    denom: "ubwick".to_string(),
                    amount: Uint128::from(refund),
                }],
            })
            .add_attribute("refund_ubwick", refund.to_string());
    }

    Ok(response)
}

fn execute_finalize_presale(
    deps: DepsMut,
    env: Env,
    _info: MessageInfo,
    token_address: String,
) -> Result<Response, ContractError> {
    let token_addr = deps.api.addr_validate(&token_address)?;
    let mut state = PRESALES
        .may_load(deps.storage, &token_addr)?
        .ok_or_else(|| ContractError::NoPresale { token: token_address.clone() })?;

    if state.finalized {
        return Err(ContractError::PresaleAlreadyFinalized {});
    }

    let now = env.block.time.seconds();
    let cap_hit = state.bwick_raised >= state.config.hard_cap_ubwick;
    let window_done = now >= state.config.end_time;
    if !cap_hit && !window_done {
        return Err(ContractError::PresaleNotFinalizable {
            end_time: state.config.end_time,
            hard_cap: state.config.hard_cap_ubwick,
            raised: state.bwick_raised,
            now,
        });
    }

    let config = CONFIG.load(deps.storage)?;
    let curve = CURVES
        .load(deps.storage, &token_addr)
        .map_err(|_| ContractError::CurveNotFound { token: token_address.clone() })?;

    // Creator allocation = total_supply × bps / 10000. Only matters
    // when bps > 0; vesting_code_id must be configured in that case.
    let total_supply = TOTAL_SUPPLY;
    let creator_alloc_utokens = (total_supply as u128)
        .saturating_mul(state.config.creator_allocation_bps as u128)
        / 10_000u128;

    // v7 (MetaDAO-style): if the raise closed below the configured
    // minimum, the presale FAILS. No curve seeding, no vesting; every
    // contributor claims a full refund via ClaimPresale.
    if state.config.min_raise_ubwick > 0 && state.bwick_raised < state.config.min_raise_ubwick {
        state.finalized = true;
        state.failed = true;
        PRESALES.save(deps.storage, &token_addr, &state)?;
        return Ok(Response::new()
            .add_attribute("action", "finalize_presale")
            .add_attribute("token_address", token_address)
            .add_attribute("result", "failed_min_raise")
            .add_attribute("bwick_raised", state.bwick_raised.to_string())
            .add_attribute("min_raise", state.config.min_raise_ubwick.to_string()));
    }

    // v7 pro-rata settlement: only up to the hard cap is accepted. The
    // surplus stays in the launchpad's bank balance and is paid back to
    // contributors pro rata at ClaimPresale.
    let accepted_ubwick = state.bwick_raised.min(state.config.hard_cap_ubwick);
    let tokens_sold_final = if state.escrowed {
        accepted_ubwick / state.config.price_ubwick_per_utoken.max(1)
    } else {
        state.tokens_sold
    };

    // Permissionless: anyone can call FinalizePresale once eligible.
    // The funded creator allocation goes to vesting; the rest of the
    // unsold supply seeds the bonding curve. The accepted BWICK is
    // added to the curve's reserves so the curve continues from the
    // presale's price point.
    state.finalized = true;
    state.tokens_sold = tokens_sold_final;
    PRESALES.save(deps.storage, &token_addr, &state)?;

    // Reflect the ACCEPTED BWICK on the curve (not the oversubscribed raw).
    let mut curve_mut = curve.clone();
    curve_mut.bwick_reserves = curve_mut
        .bwick_reserves
        .saturating_add(accepted_ubwick);
    curve_mut.tokens_sold = curve_mut.tokens_sold.saturating_add(tokens_sold_final);
    CURVES.save(deps.storage, &token_addr, &curve_mut)?;

    let mut response = Response::new()
        .add_attribute("action", "finalize_presale")
        .add_attribute("token_address", token_address.clone())
        .add_attribute("bwick_raised", state.bwick_raised.to_string())
        .add_attribute("tokens_sold", state.tokens_sold.to_string())
        .add_attribute("creator_allocation_utokens", creator_alloc_utokens.to_string());

    if creator_alloc_utokens > 0 {
        if config.vesting_code_id == 0 {
            return Err(ContractError::InvalidPresaleConfig {
                reason: "creator_allocation set but Config.vesting_code_id is unset".into(),
            });
        }
        // Stash data needed by the reply handler to fund the vesting
        // account once it's instantiated and we know its address.
        PENDING_FINALIZE.save(
            deps.storage,
            &PendingFinalize {
                token_address: token_addr.clone(),
                creator_allocation_utokens: creator_alloc_utokens,
                vesting_funding_utokens: creator_alloc_utokens,
            },
        )?;
        let creator_vesting = curve
            .clone();
        let _ = creator_vesting; // silence unused

        // Build the vesting Instantiate. Cliff and end_time are
        // computed against the actual presale end_time so callers
        // can configure them once at CreateToken time.
        #[derive(serde::Serialize)]
        struct VestingInstantiate<'a> {
            token: &'a str,
            beneficiary: &'a str,
            start_time: u64,
            cliff_time: u64,
            end_time: u64,
        }
        let start_time = state.config.end_time;
        // Defaults: linear from end_time over 30 days. Real cliffs
        // come from the CreatorVestingConfig stored in PendingCurve
        // at create time and are forwarded to this Instantiate via
        // a later wiring (kept simple here).
        let cliff_time = start_time;
        let end_time = start_time.saturating_add(30 * 86_400);
        let init = VestingInstantiate {
            token: curve.token_address.as_str(),
            beneficiary: curve.creator.as_str(),
            start_time,
            cliff_time,
            end_time,
        };
        let submsg = SubMsg::reply_on_success(
            WasmMsg::Instantiate {
                admin: Some(env.contract.address.to_string()),
                code_id: config.vesting_code_id,
                msg: to_binary(&init)?,
                funds: vec![],
                label: format!("bwick-vesting-{}", curve.metadata.symbol),
            },
            REPLY_VESTING_INSTANTIATE,
        );
        response = response.add_submessage(submsg);
    }

    Ok(response)
}

#[cfg(test)]
mod tests {
    use super::*;

    // ===========================================
    // Constants Tests
    // ===========================================

    #[test]
    fn test_total_supply_constant() {
        // 100 thousand tokens with 6 decimals
        assert_eq!(TOTAL_SUPPLY, 100_000_000_000);
    }

    #[test]
    fn test_cp_tokens_on_curve_plus_lp_equals_total() {
        assert_eq!(TOKENS_ON_CURVE + TOKENS_FOR_LP, TOTAL_SUPPLY);
    }

    // ===========================================
    // Graduation Threshold Tests
    // ===========================================

    #[test]
    fn test_graduation_threshold_check() {
        // Test threshold boundary conditions
        let threshold = 5_000_000_000_000u128; // 5M BWICK

        // Just below threshold
        let reserves_below = threshold - 1;
        assert!(reserves_below < threshold);

        // At threshold
        let reserves_at = threshold;
        assert!(reserves_at >= threshold);

        // Above threshold
        let reserves_above = threshold + 1;
        assert!(reserves_above >= threshold);
    }

    // ===========================================
    // Fee Basis Points Tests
    // ===========================================

    #[test]
    fn test_buy_fee_bps_validation() {
        // Buy fee should be 50 bps (0.5%)
        let buy_fee_bps: u16 = 50;
        let bwick_amount = 1_000_000u128; // 1 BWICK

        let fee = bwick_amount * (buy_fee_bps as u128) / 10000;
        assert_eq!(fee, 5000); // 0.5% of 1M = 5000
    }

    #[test]
    fn test_sell_fee_bps_validation() {
        // Sell fee should be 350 bps (3.5%)
        let sell_fee_bps: u16 = 350;
        let bwick_amount = 1_000_000u128; // 1 BWICK

        let fee = bwick_amount * (sell_fee_bps as u128) / 10000;
        assert_eq!(fee, 35000); // 3.5% of 1M = 35000
    }

    #[test]
    fn test_max_fee_validation() {
        // Buy fee max is 500 bps (5%)
        // Sell fee max is 1000 bps (10%)
        let max_buy_fee: u16 = 500;
        let max_sell_fee: u16 = 1000;

        assert!(50 <= max_buy_fee, "Default buy fee should be within max");
        assert!(350 <= max_sell_fee, "Default sell fee should be within max");
    }

    // ===========================================
    // Constant Product Curve Tests
    // ===========================================

    #[test]
    fn test_cp_constants_consistency() {
        // TOKENS_ON_CURVE + TOKENS_FOR_LP == TOTAL_SUPPLY
        assert_eq!(
            TOKENS_ON_CURVE + TOKENS_FOR_LP,
            TOTAL_SUPPLY,
            "Curve tokens + LP tokens must equal total supply"
        );
        // K == VIRTUAL_BWICK_START * VIRTUAL_TOKENS_START
        assert_eq!(
            K,
            VIRTUAL_BWICK_START * VIRTUAL_TOKENS_START,
            "K must equal product of virtual reserves"
        );
        // Virtual tokens must exceed tokens on curve
        assert!(
            VIRTUAL_TOKENS_START > TOKENS_ON_CURVE,
            "Virtual token reserve must be greater than tokens on curve"
        );
    }

    #[test]
    fn test_cp_price_at_zero_sold() {
        let price = calculate_price_cp(0, VIRTUAL_TOKENS_START, K);
        // price per whole token (10^6 base units) = K * 10^6 / VIRTUAL_TOKENS_START^2
        let expected = K * 1_000_000 / (VIRTUAL_TOKENS_START * VIRTUAL_TOKENS_START);
        assert_eq!(price, expected, "Price at zero sold should match formula");
        // Sanity: should be a small positive number (around 324,324 ubwick with current constants)
        assert!(price > 0 && price < 1_000_000, "Starting price should be reasonable");
    }

    #[test]
    fn test_cp_price_increases_monotonically() {
        let price_0 = calculate_price_cp(0, VIRTUAL_TOKENS_START, K);
        let price_10pct = calculate_price_cp(TOKENS_ON_CURVE / 10, VIRTUAL_TOKENS_START, K);
        let price_50pct = calculate_price_cp(TOKENS_ON_CURVE / 2, VIRTUAL_TOKENS_START, K);
        let price_90pct = calculate_price_cp(TOKENS_ON_CURVE * 9 / 10, VIRTUAL_TOKENS_START, K);

        assert!(price_0 < price_10pct, "Price should increase at 10%");
        assert!(price_10pct < price_50pct, "Price should increase at 50%");
        assert!(price_50pct < price_90pct, "Price should increase at 90%");
    }

    #[test]
    fn test_cp_price_multiplier_at_graduation() {
        // Note: After the 100k-supply migration, the legacy constants
        // (VIRTUAL_TOKENS_START=107.3M, K) are scaled for 100M supply but
        // TOKENS_ON_CURVE=79.31k. The legacy fallback path is no longer
        // self-consistent and is unused in practice (no pre-v2 curves exist).
        // We just check that grad_price >= start_price.
        let start_price = calculate_price_cp(0, VIRTUAL_TOKENS_START, K);
        let grad_price = calculate_price_cp(TOKENS_ON_CURVE, VIRTUAL_TOKENS_START, K);
        assert!(
            grad_price >= start_price,
            "Grad price should be >= start price, got grad={} start={}",
            grad_price, start_price
        );
    }

    #[test]
    fn test_cp_buy_basic() {
        // Buy with 1 BWICK, 0.5% fee
        let (tokens_out, fee) = calculate_buy_cp(0, 1_000_000, 50, VIRTUAL_TOKENS_START, K, TOKENS_ON_CURVE).unwrap();

        // Fee should be 0.5% of 1_000_000 = 5000
        assert_eq!(fee, 5000, "Fee should be 0.5%");
        assert!(tokens_out > 0, "Should receive tokens");

        // Verify constant product invariant holds approximately
        let bwick_after_fee = 1_000_000u128 - 5000;
        let virtual_bwick_before = K / VIRTUAL_TOKENS_START;
        let new_virtual_bwick = virtual_bwick_before + bwick_after_fee;
        let new_virtual_tokens = K / new_virtual_bwick;
        let expected_tokens_out = VIRTUAL_TOKENS_START - new_virtual_tokens;

        assert_eq!(tokens_out, expected_tokens_out, "Tokens out should match CP formula");
    }

    #[test]
    fn test_cp_buy_fee_deducted_before_curve() {
        let (tokens_out_with_fee, fee) = calculate_buy_cp(0, 10_000_000, 50, VIRTUAL_TOKENS_START, K, TOKENS_ON_CURVE).unwrap();
        let (tokens_out_no_fee, _) = calculate_buy_cp(0, 10_000_000, 0, VIRTUAL_TOKENS_START, K, TOKENS_ON_CURVE).unwrap();

        assert!(
            tokens_out_with_fee < tokens_out_no_fee,
            "Fee should result in fewer tokens"
        );
        assert_eq!(fee, 10_000_000 * 50 / 10000, "Fee should be 0.5%");
    }

    #[test]
    fn test_cp_buy_respects_curve_cap() {
        // Try to buy with an enormous amount of BWICK
        // tokens_out should be capped at TOKENS_ON_CURVE
        let huge_bwick = 1_000_000_000_000_000u128; // 1 billion BWICK
        let (tokens_out, _) = calculate_buy_cp(0, huge_bwick, 0, VIRTUAL_TOKENS_START, K, TOKENS_ON_CURVE).unwrap();

        assert!(
            tokens_out <= TOKENS_ON_CURVE,
            "Tokens out must not exceed curve cap: got {} > {}",
            tokens_out,
            TOKENS_ON_CURVE
        );
    }

    #[test]
    fn test_cp_buy_returns_zero_tokens_error() {
        // When all curve tokens are sold, buying should fail with NoTokensAvailable
        let result = calculate_buy_cp(TOKENS_ON_CURVE, 1_000_000, 0, VIRTUAL_TOKENS_START, K, TOKENS_ON_CURVE);
        assert!(
            result.is_err(),
            "Buying when all curve tokens are sold should return error"
        );

        // When fee consumes entire input (bwick_after_fee = 0), should fail
        // fee_bps=10000 means 100% fee, so bwick_after_fee = 0
        let result = calculate_buy_cp(0, 100, 10000, VIRTUAL_TOKENS_START, K, TOKENS_ON_CURVE);
        assert!(
            result.is_err(),
            "Buying with 100% fee should return error"
        );
    }

    #[test]
    fn test_cp_sell_basic() {
        // First buy some tokens (use 0% fee for simpler math)
        let buy_bwick = 100_000_000_000u128; // 100,000 BWICK
        let (tokens_bought, _) = calculate_buy_cp(0, buy_bwick, 0, VIRTUAL_TOKENS_START, K, TOKENS_ON_CURVE).unwrap();
        assert!(tokens_bought > 0, "Should buy some tokens");

        // Sell half back with 3.5% fee
        let half = tokens_bought / 2;
        let (bwick_out, fee) = calculate_sell_cp(tokens_bought, half, 350, VIRTUAL_TOKENS_START, K).unwrap();

        assert!(bwick_out > 0, "Should receive BWICK from sell");
        assert!(fee > 0, "Should have a sell fee");
    }

    #[test]
    fn test_cp_sell_returns_less_than_buy_paid() {
        // Buy with 0% fee, sell with 0% fee -- should get same BWICK back (roundtrip)
        let buy_bwick = 1_000_000_000u128; // 1000 BWICK
        let (tokens_out, _) = calculate_buy_cp(0, buy_bwick, 0, VIRTUAL_TOKENS_START, K, TOKENS_ON_CURVE).unwrap();
        let (bwick_back, _) = calculate_sell_cp(tokens_out, tokens_out, 0, VIRTUAL_TOKENS_START, K).unwrap();

        // Should be equal within 1 ubwick rounding
        let diff = if bwick_back > buy_bwick {
            bwick_back - buy_bwick
        } else {
            buy_bwick - bwick_back
        };
        assert!(
            diff <= 1,
            "Roundtrip should conserve BWICK within 1 ubwick, diff={}",
            diff
        );
    }

    #[test]
    fn test_cp_sell_with_fee() {
        // Buy some tokens first
        let (tokens_bought, _) = calculate_buy_cp(0, 50_000_000_000u128, 0, VIRTUAL_TOKENS_START, K, TOKENS_ON_CURVE).unwrap();
        let sell_amount = tokens_bought / 4;

        let (bwick_no_fee, _) = calculate_sell_cp(tokens_bought, sell_amount, 0, VIRTUAL_TOKENS_START, K).unwrap();
        let (bwick_with_fee, fee) = calculate_sell_cp(tokens_bought, sell_amount, 350, VIRTUAL_TOKENS_START, K).unwrap();

        assert!(
            bwick_with_fee < bwick_no_fee,
            "Fee should reduce BWICK output"
        );
        // bwick_with_fee + fee should approximately equal bwick_no_fee
        let reconstructed = bwick_with_fee + fee;
        let diff = if reconstructed > bwick_no_fee {
            reconstructed - bwick_no_fee
        } else {
            bwick_no_fee - reconstructed
        };
        assert!(
            diff <= 1,
            "bwick_with_fee + fee should equal bwick_no_fee within rounding, diff={}",
            diff
        );
    }

    #[test]
    fn test_cp_sell_more_than_sold_fails() {
        let result = calculate_sell_cp(1000, 2000, 350, VIRTUAL_TOKENS_START, K);
        assert!(result.is_err(), "Selling more than sold should fail");
    }

    #[test]
    fn test_cp_sell_all_tokens() {
        // Buy tokens, then sell ALL of them back
        let (tokens_bought, _) = calculate_buy_cp(0, 10_000_000_000u128, 0, VIRTUAL_TOKENS_START, K, TOKENS_ON_CURVE).unwrap();
        let result = calculate_sell_cp(tokens_bought, tokens_bought, 350, VIRTUAL_TOKENS_START, K);
        assert!(result.is_ok(), "Selling all tokens should succeed");

        let (bwick_out, _) = result.unwrap();
        assert!(bwick_out > 0, "Should receive BWICK when selling all");
    }

    #[test]
    fn test_cp_buy_sell_roundtrip_conservation() {
        // Buy with 1000 BWICK at 0% fee, sell back at 0% fee
        let buy_bwick = 1_000_000_000u128; // 1000 BWICK
        let (tokens_out, _) = calculate_buy_cp(0, buy_bwick, 0, VIRTUAL_TOKENS_START, K, TOKENS_ON_CURVE).unwrap();
        let (bwick_back, _) = calculate_sell_cp(tokens_out, tokens_out, 0, VIRTUAL_TOKENS_START, K).unwrap();

        let diff = if bwick_back > buy_bwick {
            bwick_back - buy_bwick
        } else {
            buy_bwick - bwick_back
        };
        assert!(
            diff <= 1,
            "Buy-sell roundtrip should conserve BWICK within 1 ubwick rounding, diff={}",
            diff
        );
    }

    #[test]
    fn test_cp_graduated_bwick_raised() {
        // Compute total BWICK raised when all TOKENS_ON_CURVE are sold
        let virtual_tokens_at_grad = VIRTUAL_TOKENS_START - TOKENS_ON_CURVE;
        let virtual_bwick_at_grad = K / virtual_tokens_at_grad;
        let bwick_raised = virtual_bwick_at_grad - VIRTUAL_BWICK_START;

        // Verify by buying all tokens with 0% fee
        let (tokens_out, _) = calculate_buy_cp(0, bwick_raised, 0, VIRTUAL_TOKENS_START, K, TOKENS_ON_CURVE).unwrap();

        // tokens_out should be close to TOKENS_ON_CURVE (within integer division rounding)
        let diff = if tokens_out > TOKENS_ON_CURVE {
            tokens_out - TOKENS_ON_CURVE
        } else {
            TOKENS_ON_CURVE - tokens_out
        };
        assert!(
            diff <= TOKENS_ON_CURVE / 1000, // within 0.1%
            "Buying with bwick_raised should get ~TOKENS_ON_CURVE tokens, diff={}",
            diff
        );

        // bwick_raised should be positive
        // Note: After the 100k-supply migration, legacy constants and
        // TOKENS_ON_CURVE are no longer self-consistent (the legacy fallback
        // is unused in practice). We only assert positivity here.
        assert!(bwick_raised > 0, "Should raise positive BWICK");
    }

    // ===========================================
    // Integration Tests: Curve Cap Behavior
    // ===========================================

    #[test]
    fn test_buy_near_curve_cap() {
        // Almost all tokens sold, small buy should work and be capped
        let tokens_sold = TOKENS_ON_CURVE - 1_000_000; // 1 token remaining
        let (tokens_out, _) = calculate_buy_cp(tokens_sold, 100_000_000_000, 0, VIRTUAL_TOKENS_START, K, TOKENS_ON_CURVE).unwrap();
        // Should be capped at the remaining 1_000_000
        assert!(
            tokens_out <= 1_000_000,
            "Tokens out should be capped at remaining: got {} > 1_000_000",
            tokens_out
        );
    }

    #[test]
    fn test_buy_at_curve_cap() {
        // All curve tokens sold, any buy should fail
        let result = calculate_buy_cp(TOKENS_ON_CURVE, 1_000_000, 0, VIRTUAL_TOKENS_START, K, TOKENS_ON_CURVE);
        assert!(
            result.is_err(),
            "Buying when curve is at cap should fail with NoTokensAvailable"
        );
    }

    // ===========================================
    // Dynamic Graduation Threshold Tests
    // ===========================================

    #[test]
    fn test_compute_dynamic_threshold_basic() {
        // $0.0001/BWICK, $2K raised => 20M BWICK = 20_000_000_000_000 ubwick
        // raw = 2_000_000_000 * 10^6 / 100 = 20_000_000_000_000
        let threshold = compute_dynamic_threshold(
            100,                 // $0.0001 in micro-USD
            2_000_000_000,       // $2K raised in micro-USD
            100_000_000_000,     // min: 100K BWICK
            50_000_000_000_000,  // max: 50M BWICK
            5_000_000_000_000,   // fallback: 5M BWICK
            0,                    // no BWICK override; use USD path
        );
        assert_eq!(threshold, 20_000_000_000_000);
    }

    #[test]
    fn test_compute_dynamic_threshold_low_price_clamps_to_max() {
        // $0.00002/BWICK, $2K raised => raw = 100M BWICK, clamped to max 50M
        let threshold = compute_dynamic_threshold(
            20,                  // $0.00002
            2_000_000_000,       // $2K raised
            100_000_000_000,     // min: 100K BWICK
            50_000_000_000_000,  // max: 50M BWICK
            5_000_000_000_000,
            0,
        );
        assert_eq!(threshold, 50_000_000_000_000); // clamped to max
    }

    #[test]
    fn test_compute_dynamic_threshold_high_price_clamps_to_min() {
        // $1000/BWICK, $2K raised => raw = 2K BWICK = 2_000_000_000 ubwick, clamped to min 100K BWICK
        let threshold = compute_dynamic_threshold(
            1_000_000_000,       // $1000
            2_000_000_000,       // $2K raised
            100_000_000_000,     // min: 100K BWICK
            50_000_000_000_000,  // max: 50M BWICK
            5_000_000_000_000,
            0,
        );
        assert_eq!(threshold, 100_000_000_000); // clamped to min
    }

    #[test]
    fn test_compute_dynamic_threshold_zero_price_uses_fallback() {
        let threshold = compute_dynamic_threshold(
            0,                   // no price
            2_000_000_000,
            100_000_000_000,
            50_000_000_000_000,
            5_000_000_000_000,   // fallback
            0,
        );
        assert_eq!(threshold, 5_000_000_000_000); // uses fallback
    }

    #[test]
    fn test_compute_dynamic_threshold_at_boundaries() {
        // $20/BWICK, $2K raised => raw = 100K BWICK = exactly min
        let threshold = compute_dynamic_threshold(
            20_000_000,          // $20
            2_000_000_000,       // $2K raised
            100_000_000_000,     // min: 100K BWICK
            50_000_000_000_000,
            5_000_000_000_000,
            0,
        );
        assert_eq!(threshold, 100_000_000_000); // exactly at min

        // $0.00004/BWICK, $2K raised => raw = 50B BWICK, clamped to max 50M
        let threshold2 = compute_dynamic_threshold(
            40,                  // $0.00004 = 40 micro-USD
            2_000_000_000,       // $2K raised
            100_000_000_000,
            50_000_000_000_000,  // max: 50M BWICK
            5_000_000_000_000,
            0,
        );
        assert_eq!(threshold2, 50_000_000_000_000); // clamped to max
    }



    #[test]
    fn test_presale_v7_fail_and_prorata() {
        use crate::state::{PresaleConfig, PresaleState, PRESALES, PRESALE_CONTRIBUTIONS, PRESALE_CLAIMED};
        use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};

        let mut deps = mock_dependencies();
        let token = Addr::unchecked("token1");
        let buyer = Addr::unchecked("buyer1");

        let cfg = PresaleConfig {
            price_ubwick_per_utoken: 10,
            hard_cap_ubwick: 1_000_000,
            per_wallet_cap_ubwick: None,
            start_time: 0,
            end_time: 10,
            merkle_root: [0u8; 32],
            creator_allocation_bps: 0,
            min_raise_ubwick: 500_000,
            allow_oversubscription: true,
        };

        // FAILED presale: raised below min -> full refund on claim.
        PRESALES
            .save(deps.as_mut().storage, &token, &PresaleState {
                config: cfg.clone(),
                bwick_raised: 100_000,
                tokens_sold: 0,
                finalized: true,
                failed: true,
                escrowed: true,
                creator_vesting_contract: None,
            })
            .unwrap();
        PRESALE_CONTRIBUTIONS
            .save(deps.as_mut().storage, (&token, &buyer), &100_000u128)
            .unwrap();

        let res = execute_claim_presale(
            deps.as_mut(),
            mock_env(),
            mock_info("buyer1", &[]),
            "token1".to_string(),
        )
        .unwrap();
        let refund_attr = res.attributes.iter().find(|a| a.key == "refund_ubwick").unwrap();
        assert_eq!(refund_attr.value, "100000", "failed presale refunds the full contribution");

        // Double-claim rejected.
        let err = execute_claim_presale(
            deps.as_mut(),
            mock_env(),
            mock_info("buyer1", &[]),
            "token1".to_string(),
        )
        .unwrap_err();
        assert!(matches!(err, ContractError::PresaleAlreadyClaimed {}));

        // OVERSUBSCRIBED success: raised 2x the cap -> half accepted, half refunded.
        let token2 = Addr::unchecked("token2");
        let buyer2 = Addr::unchecked("buyer2");
        PRESALES
            .save(deps.as_mut().storage, &token2, &PresaleState {
                config: cfg.clone(),
                bwick_raised: 2_000_000, // 2x hard cap
                tokens_sold: 100_000,
                finalized: true,
                failed: false,
                escrowed: true,
                creator_vesting_contract: None,
            })
            .unwrap();
        PRESALE_CONTRIBUTIONS
            .save(deps.as_mut().storage, (&token2, &buyer2), &400_000u128)
            .unwrap();
        // claim needs a curve for the token transfer
        let mut curve = test_curve();
        curve.token_address = token2.clone();
        CURVES.save(deps.as_mut().storage, &token2, &curve).unwrap();

        let res = execute_claim_presale(
            deps.as_mut(),
            mock_env(),
            mock_info("buyer2", &[]),
            "token2".to_string(),
        )
        .unwrap();
        let refund = res.attributes.iter().find(|a| a.key == "refund_ubwick").unwrap();
        let tokens = res.attributes.iter().find(|a| a.key == "tokens_out").unwrap();
        // accepted = 400K * 1M / 2M = 200K -> refund 200K, tokens 200K/10 = 20K utokens
        assert_eq!(refund.value, "200000");
        assert_eq!(tokens.value, "20000");
        assert!(PRESALE_CLAIMED.load(deps.as_ref().storage, (&token2, &buyer2)).unwrap());
    }

    #[test]
    fn test_compute_curve_params_bwick_override() {
        // Oracle price 0 + override must work (oracle-free path).
        let raised = 10_000_000_000_000u128; // 10M BWICK
        let params = compute_curve_params(
            0,
            500_000_000,        // starting MC $500 (ratio only)
            69_000_000_000,     // graduation MC $69K (ratio only)
            4_000_000_000,      // raised $4K (ratio only)
            raised,
        ).unwrap();

        // Constant product: price multiple at graduation must equal the
        // configured MC ratio (138x) regardless of BWICK's USD price.
        let p0 = params.virtual_bwick_start as f64 / params.virtual_tokens_start as f64;
        let vb1 = params.virtual_bwick_start as f64 + raised as f64;
        let vt1 = (params.virtual_bwick_start as f64 * params.virtual_tokens_start as f64) / vb1;
        let p1 = vb1 / vt1;
        let multiple = p1 / p0;
        assert!(
            (multiple - 138.0).abs() / 138.0 < 0.02,
            "graduation multiple should be ~138x, got {multiple}"
        );

        // virtual BWICK = raised / (sqrt(138) - 1) ~= raised / 10.747
        let expected_vb = raised as f64 / (138.0f64.sqrt() - 1.0);
        assert!(
            (params.virtual_bwick_start as f64 - expected_vb).abs() / expected_vb < 0.01,
            "virtual_bwick_start should be ~{expected_vb}, got {}",
            params.virtual_bwick_start
        );

        // Same ratios as the USD path: ~68.1% of supply on the curve.
        assert!(
            params.tokens_on_curve > 65_000_000_000 && params.tokens_on_curve < 71_000_000_000,
            "tokens_on_curve should be ~68.1K tokens, got {}",
            params.tokens_on_curve
        );
    }

    #[test]
    fn test_compute_dynamic_threshold_bwick_override() {
        // target_raised_bwick set => oracle ignored entirely
        let threshold = compute_dynamic_threshold(
            2,                    // price irrelevant
            4_000_000_000,        // raised_usd irrelevant
            100_000_000_000,      // min: 100K BWICK
            50_000_000_000_000,   // max: 50M BWICK
            5_000_000_000_000,    // fallback irrelevant
            500_000_000_000,      // 500K BWICK direct target
        );
        assert_eq!(threshold, 500_000_000_000);

        // Override above max => clamped
        let clamped_high = compute_dynamic_threshold(
            2, 4_000_000_000, 100_000_000_000, 50_000_000_000_000, 5_000_000_000_000,
            999_000_000_000_000, // way above max
        );
        assert_eq!(clamped_high, 50_000_000_000_000);

        // Override below min => clamped
        let clamped_low = compute_dynamic_threshold(
            2, 4_000_000_000, 100_000_000_000, 50_000_000_000_000, 5_000_000_000_000,
            1_000_000, // way below min
        );
        assert_eq!(clamped_low, 100_000_000_000);
    }

    #[test]
    fn test_effective_threshold_ratchet_increases() {
        // Curve has stored threshold of 3M, computed is 5M => effective = 5M (increases)
        let effective = effective_threshold_pure(
            Some(3_000_000_000_000), // stored
            5_000_000_000_000,       // computed
            5_000_000_000_000,       // legacy_fallback (unused when Some)
        );
        assert_eq!(effective, 5_000_000_000_000);
    }

    #[test]
    fn test_effective_threshold_ratchet_never_decreases() {
        // Curve has stored threshold of 8M, computed is 5M => effective = 8M (ratchet)
        let effective = effective_threshold_pure(
            Some(8_000_000_000_000), // stored
            5_000_000_000_000,       // computed
            5_000_000_000_000,       // legacy_fallback (unused)
        );
        assert_eq!(effective, 8_000_000_000_000);
    }

    #[test]
    fn test_effective_threshold_legacy_curve_no_stored() {
        // Legacy curve (None), computed is 3M, global fallback is 5M => max(5M, 3M) = 5M
        let effective = effective_threshold_pure(
            None,                    // legacy
            3_000_000_000_000,       // computed
            5_000_000_000_000,       // legacy_fallback (old global threshold)
        );
        assert_eq!(effective, 5_000_000_000_000);
    }

    #[test]
    fn test_effective_threshold_legacy_curve_computed_higher() {
        // Legacy curve (None), computed is 8M, global fallback is 5M => max(5M, 8M) = 8M
        let effective = effective_threshold_pure(
            None,
            8_000_000_000_000,
            5_000_000_000_000,
        );
        assert_eq!(effective, 8_000_000_000_000);
    }

    // ===========================================
    // Oracle Handler Integration Tests
    // ===========================================

    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
    use cosmwasm_std::Addr;

    /// Helper: create a default Config for tests
    fn test_config() -> Config {
        Config {
            presales_enabled: false,
            amm_contract: Addr::unchecked("amm_contract"),
            cw20_code_id: 1,
            creation_fee: 80_000_000_000,
            graduation_threshold: 5_000_000_000_000,
            buy_fee_bps: 50,
            sell_fee_bps: 350,
            creator_fee_share_bps: 2000,
            admin: Addr::unchecked("admin"),
            target_graduation_usd: 10_000_000_000,
            min_graduation_threshold: 100_000_000_000,
            max_graduation_threshold: 50_000_000_000_000,
            target_starting_mc_usd: 1_000_000_000,
            target_raised_usd: 2_000_000_000,
            target_raised_bwick: 0,
            max_wallet_bps: 300,
            fee_exempt_address: Addr::unchecked(""),
            oracle_contract: Addr::unchecked(""),
            name_cooldown_seconds: 0,
            vesting_code_id: 0,
        }
    }

    #[test]
    fn test_oracle_handler_rejects_non_admin() {
        let mut deps = mock_dependencies();
        let config = test_config();
        CONFIG.save(deps.as_mut().storage, &config).unwrap();

        let info = mock_info("not_admin", &[]);
        let env = mock_env();
        let result = execute_update_bwick_price(
            deps.as_mut(), env, info,
            Uint128::from(2_000_000u128),
        );
        assert!(result.is_err());
        match result.unwrap_err() {
            ContractError::Unauthorized {} => {},
            e => panic!("Expected Unauthorized, got {:?}", e),
        }
    }

    #[test]
    fn test_oracle_handler_rejects_zero_price() {
        let mut deps = mock_dependencies();
        let config = test_config();
        CONFIG.save(deps.as_mut().storage, &config).unwrap();

        let info = mock_info("admin", &[]);
        let env = mock_env();
        let result = execute_update_bwick_price(
            deps.as_mut(), env, info,
            Uint128::zero(),
        );
        assert!(result.is_err());
        match result.unwrap_err() {
            ContractError::ZeroPrice {} => {},
            e => panic!("Expected ZeroPrice, got {:?}", e),
        }
    }

    #[test]
    fn test_oracle_handler_saves_price() {
        let mut deps = mock_dependencies();
        let config = test_config();
        CONFIG.save(deps.as_mut().storage, &config).unwrap();

        let info = mock_info("admin", &[]);
        let env = mock_env();
        let result = execute_update_bwick_price(
            deps.as_mut(), env.clone(), info,
            Uint128::from(2_000_000u128),
        );
        assert!(result.is_ok());

        let oracle = ORACLE_STATE.load(deps.as_ref().storage).unwrap();
        assert_eq!(oracle.bwick_usd_price, 2_000_000);
        assert_eq!(oracle.last_update_height, env.block.height);
    }

    #[test]
    fn test_update_config_rejects_non_admin() {
        let mut deps = mock_dependencies();
        let config = test_config();
        CONFIG.save(deps.as_mut().storage, &config).unwrap();

        let info = mock_info("not_admin", &[]);
        let result = execute_update_config(
            deps.as_mut(), info,
            Some(Uint128::from(20_000_000_000u128)),
            None, None, None, None, None, None, None, None, None, None, None,
        );
        assert!(result.is_err());
        match result.unwrap_err() {
            ContractError::Unauthorized {} => {},
            e => panic!("Expected Unauthorized, got {:?}", e),
        }
    }

    #[test]
    fn test_update_config_validates_bounds() {
        let mut deps = mock_dependencies();
        let config = test_config();
        CONFIG.save(deps.as_mut().storage, &config).unwrap();

        let info = mock_info("admin", &[]);
        // Set min > max -- should fail
        let result = execute_update_config(
            deps.as_mut(), info,
            None,
            Some(Uint128::from(100_000_000_000_000u128)), // min = 100M BWICK
            Some(Uint128::from(1_000_000_000u128)),       // max = 1K BWICK
            None, None, None, None, None, None, None, None, None,
        );
        assert!(result.is_err());
        match result.unwrap_err() {
            ContractError::InvalidThresholdBounds { .. } => {},
            e => panic!("Expected InvalidThresholdBounds, got {:?}", e),
        }
    }

    #[test]
    fn test_update_config_updates_values() {
        let mut deps = mock_dependencies();
        let config = test_config();
        CONFIG.save(deps.as_mut().storage, &config).unwrap();

        let info = mock_info("admin", &[]);
        let result = execute_update_config(
            deps.as_mut(), info,
            Some(Uint128::from(20_000_000_000u128)),  // $20K target
            None, None, None, None, None, None, None, None, None, None, None,
        );
        assert!(result.is_ok());

        let updated = CONFIG.load(deps.as_ref().storage).unwrap();
        assert_eq!(updated.target_graduation_usd, 20_000_000_000);
        // Other fields unchanged
        assert_eq!(updated.min_graduation_threshold, 100_000_000_000);
    }

    /// Helper: create a test Curve for integration tests
    fn test_curve() -> crate::state::Curve {
        crate::state::Curve {
            token_address: Addr::unchecked("token1"),
            metadata: TokenMetadata {
                name: "Test".to_string(),
                symbol: "TST".to_string(),
                image: "".to_string(),
                description: "".to_string(),
                social_links: vec![],
            },
            creator: Addr::unchecked("creator"),
            tokens_sold: 0,
            bwick_reserves: 0,
            graduated: false,
            created_at: 0,
            creator_fees_earned: 0,
            graduation_threshold_ubwick: None,
            virtual_bwick_start: 0,
            virtual_tokens_start: 0,
            curve_k: 0,
            tokens_on_curve: 0,
            tokens_for_lp: 0,
            lp_lock_seconds: None,
        }
    }

    #[test]
    fn test_load_effective_threshold_with_oracle() {
        let mut deps = mock_dependencies();
        let config = test_config();
        CONFIG.save(deps.as_mut().storage, &config).unwrap();

        // Set oracle price to $2/BWICK
        let oracle = OracleState {
            bwick_usd_price: 2_000_000,
            last_update_height: 100,
            last_update_timestamp: 1000,
        };
        ORACLE_STATE.save(deps.as_mut().storage, &oracle).unwrap();

        // New curve (None threshold)
        // computed = target_raised_usd * 10^6 / price = 2e9 * 10^6 / 2e6 = 1e9 = 1_000_000_000_000
        // effective = max(global_fallback=5M, computed=1M) = 5M
        let curve = test_curve();

        let threshold = load_effective_threshold(deps.as_ref(), &config, &curve);
        assert_eq!(threshold, 5_000_000_000_000); // max(5M fallback, 1M computed) = 5M
    }

    #[test]
    fn test_load_effective_threshold_ratchet_on_curve() {
        let mut deps = mock_dependencies();
        let config = test_config();
        CONFIG.save(deps.as_mut().storage, &config).unwrap();

        // Set oracle price to $2/BWICK => computed = 1M BWICK
        let oracle = OracleState {
            bwick_usd_price: 2_000_000,
            last_update_height: 100,
            last_update_timestamp: 1000,
        };
        ORACLE_STATE.save(deps.as_mut().storage, &oracle).unwrap();

        // Curve with stored threshold of 8M (previously ratcheted)
        let mut curve = test_curve();
        curve.graduation_threshold_ubwick = Some(8_000_000_000_000);

        let threshold = load_effective_threshold(deps.as_ref(), &config, &curve);
        assert_eq!(threshold, 8_000_000_000_000); // ratchet: max(8M, 1M) = 8M
    }

    #[test]
    fn test_load_effective_threshold_no_oracle_uses_fallback() {
        let mut deps = mock_dependencies();
        let config = test_config();
        CONFIG.save(deps.as_mut().storage, &config).unwrap();
        // No ORACLE_STATE saved => price = 0 => fallback

        let curve = test_curve();

        let threshold = load_effective_threshold(deps.as_ref(), &config, &curve);
        // No oracle => price=0 => fallback = config.graduation_threshold = 5M
        // effective_threshold_pure(None, 5M, 5M) = max(5M, 5M) = 5M
        assert_eq!(threshold, 5_000_000_000_000);
    }

    // ===========================================
    // Integer Square Root Tests
    // ===========================================

    #[test]
    fn test_isqrt_perfect_squares() {
        assert_eq!(isqrt(0), 0);
        assert_eq!(isqrt(1), 1);
        assert_eq!(isqrt(4), 2);
        assert_eq!(isqrt(9), 3);
        assert_eq!(isqrt(100), 10);
        assert_eq!(isqrt(1_000_000), 1_000);
        assert_eq!(isqrt(1_000_000_000_000), 1_000_000);
    }

    #[test]
    fn test_isqrt_non_perfect() {
        // isqrt floors to nearest integer
        assert_eq!(isqrt(2), 1);
        assert_eq!(isqrt(3), 1);
        assert_eq!(isqrt(5), 2);
        assert_eq!(isqrt(10), 3);
    }

    // ===========================================
    // compute_curve_params Tests
    // ===========================================

    #[test]
    fn test_compute_curve_params_at_0001() {
        // P = $0.0001 = 100 micro-USD
        let params = compute_curve_params(
            100,                 // bwick price = 100 micro-USD
            1_000_000_000,       // starting MC = $1K
            10_000_000_000,      // graduation MC = $10K
            2_000_000_000,       // raised = $2K
            0,                   // no BWICK override
        ).unwrap();

        // tokens_on_curve should be ~63.25k tokens = ~63.25e9 utokens
        assert!(
            params.tokens_on_curve > 60_000_000_000 && params.tokens_on_curve < 70_000_000_000,
            "tokens_on_curve should be ~63.25k utokens, got {}",
            params.tokens_on_curve
        );

        // tokens_for_lp = TOTAL_SUPPLY - tokens_on_curve
        assert_eq!(params.tokens_on_curve + params.tokens_for_lp, TOTAL_SUPPLY);

        // virtual_tokens should be ~92.5k utokens
        assert!(
            params.virtual_tokens_start > 85_000_000_000 && params.virtual_tokens_start < 100_000_000_000,
            "virtual_tokens_start should be ~92.5k utokens, got {}",
            params.virtual_tokens_start
        );

        // virtual_bwick: at P=$0.0001, should be ~9.25M BWICK = ~9.25e12 ubwick
        // (virtual_bwick scales with raised_usd / price, NOT with total supply)
        assert!(
            params.virtual_bwick_start > 8_000_000_000_000 && params.virtual_bwick_start < 11_000_000_000_000,
            "virtual_bwick_start should be ~9.25M ubwick, got {}",
            params.virtual_bwick_start
        );

        // K = vx * vt
        assert_eq!(params.curve_k, params.virtual_bwick_start * params.virtual_tokens_start);
    }

    #[test]
    fn test_compute_curve_params_at_00005() {
        // P = $0.00005 = 50 micro-USD
        let params = compute_curve_params(
            50,                  // bwick price = 50 micro-USD
            1_000_000_000,       // starting MC = $1K
            10_000_000_000,      // graduation MC = $10K
            2_000_000_000,       // raised = $2K
            0,                   // no BWICK override
        ).unwrap();

        // tokens_on_curve stays the same (~63.25k) regardless of price
        assert!(
            params.tokens_on_curve > 60_000_000_000 && params.tokens_on_curve < 70_000_000_000,
            "tokens_on_curve should be ~63.25k utokens at any price, got {}",
            params.tokens_on_curve
        );

        // virtual_bwick doubles when price halves (~18.5M BWICK)
        // (virtual_bwick scales with raised_usd / price, NOT with total supply)
        assert!(
            params.virtual_bwick_start > 16_000_000_000_000 && params.virtual_bwick_start < 22_000_000_000_000,
            "virtual_bwick_start should be ~18.5M ubwick, got {}",
            params.virtual_bwick_start
        );
    }

    #[test]
    fn test_compute_curve_params_rejects_zero_price() {
        let result = compute_curve_params(0, 1_000_000_000, 10_000_000_000, 2_000_000_000, 0);
        assert!(result.is_err());
        match result.unwrap_err() {
            ContractError::OraclePriceRequired {} => {},
            e => panic!("Expected OraclePriceRequired, got {:?}", e),
        }
    }

    #[test]
    fn test_compute_curve_params_rejects_invalid_mc_ratio() {
        // graduation MC <= starting MC
        let result = compute_curve_params(100, 10_000_000_000, 1_000_000_000, 2_000_000_000, 0);
        assert!(result.is_err());
    }

    #[test]
    fn test_compute_curve_params_legacy_fallback() {
        // Legacy curve (curve_k = 0) should use old hardcoded constants
        let curve = test_curve();
        let (vx, vt, k, toc, tlp) = get_curve_constants(&curve);
        assert_eq!(vx, VIRTUAL_BWICK_START);
        assert_eq!(vt, VIRTUAL_TOKENS_START);
        assert_eq!(k, K);
        assert_eq!(toc, TOKENS_ON_CURVE);
        assert_eq!(tlp, TOKENS_FOR_LP);
    }

    #[test]
    fn test_compute_curve_params_per_curve_used() {
        // Curve with per-curve params should use them
        let mut curve = test_curve();
        curve.virtual_bwick_start = 100;
        curve.virtual_tokens_start = 200;
        curve.curve_k = 20000;
        curve.tokens_on_curve = 300;
        curve.tokens_for_lp = 400;

        let (vx, vt, k, toc, tlp) = get_curve_constants(&curve);
        assert_eq!(vx, 100);
        assert_eq!(vt, 200);
        assert_eq!(k, 20000);
        assert_eq!(toc, 300);
        assert_eq!(tlp, 400);
    }

    #[test]
    fn test_compute_curve_params_buy_sell_roundtrip() {
        // Verify buy/sell roundtrip with dynamically computed params
        let params = compute_curve_params(
            100,                 // $0.0001
            1_000_000_000,       // $1K starting MC
            10_000_000_000,      // $10K graduation MC
            2_000_000_000,       // $2K raised
            0,                   // no BWICK override
        ).unwrap();

        let buy_bwick = 1_000_000_000u128; // 1000 BWICK
        let (tokens_out, _) = calculate_buy_cp(
            0, buy_bwick, 0,
            params.virtual_tokens_start, params.curve_k, params.tokens_on_curve,
        ).unwrap();
        let (bwick_back, _) = calculate_sell_cp(
            tokens_out, tokens_out, 0,
            params.virtual_tokens_start, params.curve_k,
        ).unwrap();

        let diff = if bwick_back > buy_bwick { bwick_back - buy_bwick } else { buy_bwick - bwick_back };
        // Small integer rounding tolerance (a few units of ubwick)
        assert!(
            diff <= 10,
            "Roundtrip with dynamic params should conserve BWICK, diff={}",
            diff
        );
    }

    #[test]
    fn test_compute_curve_params_price_multiplier() {
        // At graduation, price should be ~R times starting price
        let params = compute_curve_params(
            100,                 // $0.0001
            1_000_000_000,       // $1K starting MC
            10_000_000_000,      // $10K graduation MC (R=10)
            2_000_000_000,       // $2K raised
            0,                   // no BWICK override
        ).unwrap();

        let start_price = calculate_price_cp(0, params.virtual_tokens_start, params.curve_k);
        let grad_price = calculate_price_cp(
            params.tokens_on_curve,
            params.virtual_tokens_start,
            params.curve_k,
        );
        let multiplier = grad_price / start_price;

        // Should be approximately 10x (R=10)
        assert!(
            multiplier >= 9 && multiplier <= 11,
            "Price multiplier should be ~10x (R=10), got {}x",
            multiplier
        );
    }
}