dApp Docs/永续合约协议实现
Development reference. Not independently verified for production.

MSG Chain 永续合约协议实现指南

本指南详细讲解如何在 MSG Chain (msg-chain-1) 上构建完整的 vAMM 永续合约去中心化交易所。

链参数:chain-id = msg-chain-1 | bech32 前缀 = msg | 精度 = 18 位小数

目标读者:Rust/CosmWasm 开发者、DeFi 协议工程师、区块链研究员


目录

  1. 概述
  2. 系统架构
  3. 流动性池合约
  4. 交易与头寸管理
  5. vAMM 定价
  6. 资金费率
  7. 清算引擎
  8. Oracle 集成
  9. 前端实现
  10. 完整示例

1. 概述

1.1 什么是永续合约

永续合约(Perpetual Futures / Perps)是一种没有到期日的衍生品合约,交易者可以持有多头(做多)或空头(做空)头寸,通过资金费率机制锚定现货价格。截至 2026 年,永续合约日均交易量超过 3000 亿美元,是 DeFi 中最大的交易品类。

核心特征:

1.2 vAMM vs 订单簿

特性 vAMM(虚拟 AMM) 订单簿
流动性来源 LP 池 做市商 / 订单撮合
定价方式 恒定乘积公式 买卖盘口
滑点 基于交易量自动计算 基于订单簿深度
适合场景 长尾资产、低流动性启动 高流动性主流资产
代表协议 GMX, Jupiter Perps dYdX, Hyperliquid

本指南采用 vAMM 模型,因为它在 MSG Chain 上实现更简单,且对流动性提供者(LP)更友好。

1.3 关键概念

标记价格(Mark Price)

标记价格是用于计算盈亏和清算的价格,通常取预言机价格与 vAMM 价格的中间值,防止价格操纵:

mark_price = (oracle_price + vamm_price) / 2

资金费率(Funding Rate)

资金费率使永续合约价格锚定现货价格。当多头过多时,多头支付空头;当空头过多时,空头支付多头:

funding_rate = clamp((mark_price - index_price) / index_price, -0.05%, 0.05%)

杠杆(Leverage)

杠杆倍数 = 头寸价值 / 抵押品价值。例如 10 倍杠杆意味着头寸价值是抵押品的 10 倍:

max_leverage = 50x  // 协议支持最大杠杆

清算(Liquidation)

当保证金比率低于维持保证金时触发清算:

margin_ratio = (position_value - debt) / position_value
liquidation_threshold = 10%  // 低于此值触发清算

1.4 MSG Chain 上的永续合约设计

本协议为 MSG Chain 量身定制,充分利用其 CosmWasm 智能合约能力:

模块 合约 功能
工厂 perp_factory 创建交易对、管理协议配置
流动性池 perp_pool 管理 LP 存款、池价值计算
交易引擎 perp_trading 开仓、平仓、仓位管理
预言机 perp_oracle 价格喂价、TWAP、断路器
清算 perp_liquidation 清算执行、罚没分配

设计原则:

  1. 模块化:每个合约职责单一,可单独升级
  2. 安全优先:所有关键函数有多重检查
  3. LP 保护:池价值计算包含未实现盈亏和坏账
  4. 抗操纵:使用 TWAP 预言机和 vAMM 双重验证

2. 系统架构

2.1 合约架构总览

┌─────────────────────────────────────────────────────────────────┐
│                        用户交互层                                 │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐       │
│  │  交易者   │  │    LP    │  │  清算人  │  │  套利者  │       │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘       │
│       │              │             │              │              │
├───────┴──────────────┴─────────────┴──────────────┴─────────────┤
│                         API / 前端层                              │
└───────────────────────────┬─────────────────────────────────────┘
                            │
┌───────────────────────────┴─────────────────────────────────────┐
│                     CosmWasm 智能合约层                           │
│                                                                  │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐      │
│  │ perp_factory │◄──►│  perp_pool   │◄──►│ perp_trading │      │
│  │  (工厂合约)  │    │  (流动性池)  │    │  (交易引擎)  │      │
│  └──────┬───────┘    └──────┬───────┘    └──────┬───────┘      │
│         │                   │                   │               │
│         │    ┌──────────────┴──────────────┐    │               │
│         └────►        perp_oracle          ◄────┘               │
│              │         (预言机)            │                     │
│              └──────────────┬──────────────┘                     │
│                             │                                    │
│              ┌──────────────┴──────────────┐                     │
│              │    perp_liquidation          │                     │
│              │      (清算引擎)             │                     │
│              └─────────────────────────────┘                     │
└─────────────────────────────────────────────────────────────────┘
                            │
┌───────────────────────────┴─────────────────────────────────────┐
│                      MSG Chain 底层                               │
│  ├── CometBFT 共识                                               │
│  ├── IBC 跨链通信                                                 │
│  ├── CosmWasm 虚拟机                                              │
│  └── Agent API 预言机模块                                          │
└─────────────────────────────────────────────────────────────────┘

2.2 合约间通信

合约间通过 CosmWasm 的 WasmMsg::Execute 进行消息传递:

OpenPosition Flow:
  Trader ──► perp_trading.open_position()
              │
              ├──► perp_oracle.get_price()
              │
              ├──► perp_pool.lock_collateral()
              │
              ├──► vAMM 计算开仓价格和滑点
              │
              └──► 创建 Position 并存储
                    │
                    └──► perp_liquidation.monitor()

2.3 数据流

┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
│  交易者存款 USDC  │────►│  perp_pool 合约  │────►│ LP Token 铸造    │
└──────────────────┘     │  总价值 = Σ资产   │     └──────────────────┘
                         │  + 未实现盈亏     │
┌──────────────────┐     │  - 坏账           │     ┌──────────────────┐
│  交易者开多头    │────►│                  │────►│ 抵押品锁定       │
└──────────────────┘     └──────────────────┘     └──────────────────┘
                                │
                                ▼
                         ┌──────────────────┐
                         │  perp_trading    │
                         │  开仓价: $65,000 │
                         │  数量: 1 BTC     │
                         │  杠杆: 10x       │
                         │  抵押品: $6,500  │
                         │  强平价: $58,500 │
                         └──────────────────┘

2.4 CW20 代币标准

本协议使用 CW20 代币作为 LP 份额凭证:

[contract: perp_pool]
  ├── mints CW20 LP tokens on deposit
  ├── burns LP tokens on withdrawal
  ├── LP token decimals: 18 (matching MSG Chain)
  └── LP token symbol: "msGLP"

2.5 错误处理

统一的错误类型定义:

Error Types:
  ├── InsufficientCollateral    — 抵押品不足
  ├── PositionTooSmall          — 仓位过小
  ├── MaxLeverageExceeded       — 超过最大杠杆
  ├── PriceSlippageExceeded     — 滑点超过设定
  ├── MarginRatioTooLow         — 保证金率过低
  ├── PoolInsufficientDepth     — 池深度不足
  ├── OracleStale               — 预言机价格过期
  ├── CircuitBreakerActive      — 断路器已激活
  └── Unauthorized              — 未授权调用

3. 流动性池合约

3.1 池架构

perp_pool 合约是协议的核心流动性引擎,类似 GMX 的 GLP 池。LP 存入资产获取 LP Token,池子为交易者提供流动性。

3.2 数据结构

// perp_pool/src/state.rs

use cosmwasm_std::{Addr, Uint128, Decimal, Timestamp};
use cw_storage_plus::{Item, Map};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PoolAsset {
    pub denom: String,
    pub decimals: u8,
    pub weight: Decimal,
    pub min_deposit: Uint128,
    pub max_pool_share: Decimal,
    pub is_active: bool,
    pub cw20_address: Option<Addr>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PoolConfig {
    pub owner: Addr,
    pub lp_token_addr: Addr,
    pub performance_fee: Decimal,
    pub withdrawal_fee: Decimal,
    pub mint_slippage: Decimal,
    pub redeem_slippage: Decimal,
    pub treasury: Addr,
    pub max_supply: Uint128,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PerpPool {
    pub lp_token: Addr,
    pub total_value: Uint128,
    pub assets: Vec<PoolAsset>,
    pub fees_collected: Uint128,
    pub bad_debt: Uint128,
    pub unrealized_pnl: Uint128,
    pub net_value: Uint128,
    pub lp_token_supply: Uint128,
    pub last_updated: Timestamp,
    pub config: PoolConfig,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct DepositRecord {
    pub user: Addr,
    pub asset_denom: String,
    pub amount: Uint128,
    pub lp_tokens_minted: Uint128,
    pub deposit_time: Timestamp,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct LpFeeClaim {
    pub user: Addr,
    pub pending_fees: Uint128,
    pub last_claim_time: Timestamp,
}

pub const CONFIG: Item<PoolConfig> = Item::new("config");
pub const POOL: Item<PerpPool> = Item::new("pool");
pub const ASSETS: Map<&str, PoolAsset> = Map::new("assets");
pub const DEPOSITS: Map<&Addr, Vec<DepositRecord>> = Map::new("deposits");
pub const LP_FEE_CLAIMS: Map<&Addr, LpFeeClaim> = Map::new("fee_claims");

3.3 合约入口

// perp_pool/src/contract.rs

use cosmwasm_std::{
    entry_point, to_binary, Binary, Deps, DepsMut, Env,
    MessageInfo, Response, StdResult, Uint128, Decimal,
    Coin, WasmMsg, BankMsg, CosmosMsg,
};
use crate::state::*;
use crate::error::ContractError;

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
    pub owner: String,
    pub lp_token_code_id: u64,
    pub treasury: String,
    pub performance_fee: Decimal,
    pub withdrawal_fee: Decimal,
    pub max_supply: Uint128,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    Deposit {
        asset_denom: String,
        amount: Uint128,
        min_lp_tokens: Uint128,
    },
    Withdraw {
        lp_token_amount: Uint128,
        min_out_amounts: Vec<(String, Uint128)>,
    },
    AddAsset { asset: PoolAsset },
    UpdateAsset {
        denom: String,
        weight: Option<Decimal>,
        is_active: Option<bool>,
    },
    UpdateConfig {
        performance_fee: Option<Decimal>,
        withdrawal_fee: Option<Decimal>,
        treasury: Option<String>,
    },
    ClaimFees {},
    CollectPerformanceFee { amount: Uint128 },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    GetPool {},
    GetUserDeposits { user: String },
    GetLpTokenPrice {},
    GetAssets {},
    GetWithdrawableAssets { lp_token_amount: Uint128 },
    GetPendingFees { user: String },
}

3.4 实例化

#[entry_point]
pub fn instantiate(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: InstantiateMsg,
) -> Result<Response, ContractError> {
    let owner = deps.api.addr_validate(&msg.owner)?;
    let treasury = deps.api.addr_validate(&msg.treasury)?;

    let config = PoolConfig {
        owner,
        lp_token_addr: env.contract.address.clone(),
        performance_fee: msg.performance_fee,
        withdrawal_fee: msg.withdrawal_fee,
        mint_slippage: Decimal::percent(1),
        redeem_slippage: Decimal::percent(1),
        treasury,
        max_supply: msg.max_supply,
    };

    let pool = PerpPool {
        lp_token: config.lp_token_addr.clone(),
        total_value: Uint128::zero(),
        assets: vec![],
        fees_collected: Uint128::zero(),
        bad_debt: Uint128::zero(),
        unrealized_pnl: Uint128::zero(),
        net_value: Uint128::zero(),
        lp_token_supply: Uint128::zero(),
        last_updated: env.block.time,
        config,
    };

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

    Ok(Response::new()
        .add_attribute("action", "instantiate")
        .add_attribute("contract", "perp_pool")
        .add_attribute("owner", msg.owner))
}

3.5 存款逻辑

#[entry_point]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::Deposit { asset_denom, amount, min_lp_tokens } => {
            execute_deposit(deps, env, info, asset_denom, amount, min_lp_tokens)
        }
        ExecuteMsg::Withdraw { lp_token_amount, min_out_amounts } => {
            execute_withdraw(deps, env, info, lp_token_amount, min_out_amounts)
        }
        ExecuteMsg::AddAsset { asset } => execute_add_asset(deps, env, info, asset),
        ExecuteMsg::UpdateAsset { denom, weight, is_active } => {
            execute_update_asset(deps, env, info, denom, weight, is_active)
        }
        ExecuteMsg::UpdateConfig { performance_fee, withdrawal_fee, treasury } => {
            execute_update_config(deps, env, info, performance_fee, withdrawal_fee, treasury)
        }
        ExecuteMsg::ClaimFees {} => execute_claim_fees(deps, env, info),
        ExecuteMsg::CollectPerformanceFee { amount } => {
            execute_collect_performance_fee(deps, env, info, amount)
        }
    }
}

pub fn execute_deposit(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    asset_denom: String,
    amount: Uint128,
    min_lp_tokens: Uint128,
) -> Result<Response, ContractError> {
    let asset = ASSETS
        .may_load(deps.storage, &asset_denom)?
        .ok_or(ContractError::AssetNotSupported { denom: asset_denom.clone() })?;

    if !asset.is_active {
        return Err(ContractError::AssetNotActive { denom: asset_denom });
    }

    if amount < asset.min_deposit {
        return Err(ContractError::InsufficientDeposit {
            required: asset.min_deposit,
            provided: amount,
        });
    }

    let funds_sent = info.funds.iter()
        .find(|c| c.denom == asset.denom)
        .map(|c| c.amount)
        .unwrap_or(Uint128::zero());

    if asset.cw20_address.is_none() && funds_sent < amount {
        return Err(ContractError::InsufficientFundsSent {
            expected: amount,
            received: funds_sent,
        });
    }

    let mut pool = POOL.load(deps.storage)?;
    let asset_value_usd = calculate_asset_value(deps.as_ref(), &asset_denom, amount)?;
    let current_asset_total = get_asset_balance(deps.as_ref(), &asset_denom)?;
    let current_asset_value = calculate_asset_value(deps.as_ref(), &asset_denom, current_asset_total)?;

    if pool.total_value > Uint128::zero() {
        let new_asset_value = current_asset_value.checked_add(asset_value_usd)?;
        let new_share = Decimal::from_ratio(new_asset_value, pool.total_value.checked_add(asset_value_usd)?);
        if new_share > asset.max_pool_share {
            return Err(ContractError::MaxPoolShareExceeded {
                max_share: asset.max_pool_share,
                attempted: new_share,
            });
        }
    }

    let lp_tokens_to_mint = if pool.lp_token_supply.is_zero() || pool.net_value.is_zero() {
        asset_value_usd
    } else {
        let lp_token_price = Decimal::from_ratio(pool.net_value, pool.lp_token_supply);
        Decimal::from_ratio(asset_value_usd, Uint128::new(1))
            .checked_div(lp_token_price)?
            .to_uint_floor()
    };

    if lp_tokens_to_mint < min_lp_tokens {
        return Err(ContractError::SlippageExceeded {
            expected: min_lp_tokens,
            actual: lp_tokens_to_mint,
        });
    }

    pool.total_value = pool.total_value.checked_add(asset_value_usd)?;
    pool.net_value = pool.net_value.checked_add(asset_value_usd)?;
    pool.lp_token_supply = pool.lp_token_supply.checked_add(lp_tokens_to_mint)?;
    pool.last_updated = env.block.time;
    POOL.save(deps.storage, &pool)?;

    let mint_msg = cw20::Cw20ExecuteMsg::Mint {
        recipient: info.sender.to_string(),
        amount: lp_tokens_to_mint,
    };

    let mint_exec = WasmMsg::Execute {
        contract_addr: pool.lp_token.to_string(),
        msg: to_binary(&mint_msg)?,
        funds: vec![],
    };

    let deposit_record = DepositRecord {
        user: info.sender.clone(),
        asset_denom: asset_denom.clone(),
        amount: funds_sent,
        lp_tokens_minted: lp_tokens_to_mint,
        deposit_time: env.block.time,
    };

    let mut user_deposits = DEPOSITS
        .may_load(deps.storage, &info.sender)?
        .unwrap_or_default();
    user_deposits.push(deposit_record);
    DEPOSITS.save(deps.storage, &info.sender, &user_deposits)?;

    Ok(Response::new()
        .add_message(mint_exec)
        .add_attribute("action", "deposit")
        .add_attribute("user", info.sender.to_string())
        .add_attribute("asset", asset_denom)
        .add_attribute("amount", amount.to_string())
        .add_attribute("lp_tokens_minted", lp_tokens_to_mint.to_string()))
}

3.6 赎回逻辑

pub fn execute_withdraw(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    lp_token_amount: Uint128,
    min_out_amounts: Vec<(String, Uint128)>,
) -> Result<Response, ContractError> {
    let pool = POOL.load(deps.storage)?;

    let lp_balance = query_cw20_balance(deps.as_ref(), &pool.lp_token, &info.sender)?;
    if lp_balance < lp_token_amount {
        return Err(ContractError::InsufficientLpTokens {
            balance: lp_balance,
            required: lp_token_amount,
        });
    }

    let lp_token_price = if pool.lp_token_supply.is_zero() || pool.net_value.is_zero() {
        Decimal::one()
    } else {
        Decimal::from_ratio(pool.net_value, pool.lp_token_supply)
    };

    let withdraw_value = lp_token_price
        .checked_mul(Decimal::from_ratio(lp_token_amount, Uint128::new(1)))?
        .to_uint_floor();

    let mut msgs: Vec<CosmosMsg> = vec![];
    let asset_list = ASSETS
        .range(deps.storage, None, None, cosmwasm_std::Order::Ascending)
        .collect::<StdResult<Vec<_>>>()?;

    for (denom, asset_info) in &asset_list {
        if !asset_info.is_active { continue; }

        let asset_balance = get_asset_balance(deps.as_ref(), denom)?;
        let asset_value = calculate_asset_value(deps.as_ref(), denom, asset_balance)?;
        if asset_value.is_zero() { continue; }

        let asset_share = Decimal::from_ratio(asset_value, pool.net_value);
        let user_asset_value = asset_share.checked_mul(Decimal::from_ratio(withdraw_value, Uint128::new(1)))?;
        let user_asset_amount = usd_to_token_amount(deps.as_ref(), denom, user_asset_value)?;

        if user_asset_amount.is_zero() { continue; }

        if let Some((_, min_amount)) = min_out_amounts.iter().find(|(d, _)| d == denom) {
            if user_asset_amount < *min_amount {
                return Err(ContractError::SlippageExceeded {
                    expected: *min_amount,
                    actual: user_asset_amount,
                });
            }
        }

        if let Some(cw20_addr) = &asset_info.cw20_address {
            msgs.push(WasmMsg::Execute {
                contract_addr: cw20_addr.to_string(),
                msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer {
                    recipient: info.sender.to_string(),
                    amount: user_asset_amount,
                })?,
                funds: vec![],
            }.into());
        } else {
            msgs.push(BankMsg::Send {
                to_address: info.sender.to_string(),
                amount: vec![Coin { denom: denom.clone(), amount: user_asset_amount }],
            }.into());
        }
    }

    msgs.push(WasmMsg::Execute {
        contract_addr: pool.lp_token.to_string(),
        msg: to_binary(&cw20::Cw20ExecuteMsg::Burn { amount: lp_token_amount })?,
        funds: vec![],
    }.into());

    let mut pool = pool;
    pool.total_value = pool.total_value.checked_sub(withdraw_value)?;
    pool.lp_token_supply = pool.lp_token_supply.checked_sub(lp_token_amount)?;
    pool.last_updated = env.block.time;
    POOL.save(deps.storage, &pool)?;

    Ok(Response::new()
        .add_messages(msgs)
        .add_attribute("action", "withdraw")
        .add_attribute("user", info.sender.to_string())
        .add_attribute("lp_burned", lp_token_amount.to_string())
        .add_attribute("value_withdrawn", withdraw_value.to_string()))
}

fn query_cw20_balance(deps: Deps, contract_addr: &Addr, owner: &Addr) -> StdResult<Uint128> {
    let balance: cw20::BalanceResponse = deps.querier.query_wasm_smart(
        contract_addr.clone(),
        &cw20::Cw20QueryMsg::Balance { address: owner.to_string() },
    )?;
    Ok(balance.balance)
}

3.7 池价值计算

// perp_pool/src/math.rs

use cosmwasm_std::{Uint128, Decimal, Deps, Addr, StdResult, StdError};

pub fn calculate_net_pool_value(deps: Deps) -> StdResult<Uint128> {
    let pool = POOL.load(deps.storage)?;
    let total_asset_value = calculate_total_asset_value(deps)?;
    let net_value = total_asset_value
        .checked_add(pool.unrealized_pnl)?
        .checked_sub(pool.bad_debt)?;
    Ok(net_value)
}

pub fn calculate_total_asset_value(deps: Deps) -> StdResult<Uint128> {
    let assets = ASSETS
        .range(deps.storage, None, None, cosmwasm_std::Order::Ascending)
        .collect::<StdResult<Vec<_>>>()?;

    let mut total = Uint128::zero();
    for (denom, asset_info) in &assets {
        if !asset_info.is_active { continue; }
        let balance = get_asset_balance(deps, denom)?;
        let value = calculate_asset_value(deps, denom, balance)?;
        total = total.checked_add(value)?;
    }
    Ok(total)
}

pub fn calculate_asset_value(deps: Deps, denom: &str, amount: Uint128) -> StdResult<Uint128> {
    let config = CONFIG.load(deps.storage)?;
    let price: Decimal = deps.querier.query_wasm_smart(
        config.oracle_address.clone(),
        &perp_oracle::msg::QueryMsg::GetPrice {
            base: denom.to_string(),
            quote: "USD".to_string(),
        },
    )?;

    let asset = ASSETS.load(deps.storage, denom)?;
    let decimal_factor = Uint128::from(10u128.pow(asset.decimals as u32));
    Decimal::from_ratio(amount, decimal_factor)
        .checked_mul(price)?
        .to_uint_floor()
}

pub fn calculate_lp_token_price(deps: Deps) -> StdResult<Decimal> {
    let pool = POOL.load(deps.storage)?;
    if pool.lp_token_supply.is_zero() { return Ok(Decimal::one()); }
    Ok(Decimal::from_ratio(pool.net_value, pool.lp_token_supply))
}

fn get_asset_balance(deps: Deps, denom: &str) -> StdResult<Uint128> {
    let asset_info = ASSETS.load(deps.storage, denom)?;
    if let Some(cw20_addr) = asset_info.cw20_address {
        let balance: cw20::BalanceResponse = deps.querier.query_wasm_smart(
            cw20_addr,
            &cw20::Cw20QueryMsg::Balance {
                address: deps.contract.address.to_string(),
            },
        )?;
        Ok(balance.balance)
    } else {
        let coin = deps.querier.query_balance(
            deps.contract.address.clone(),
            denom.to_string(),
        )?;
        Ok(coin.amount)
    }
}

pub fn usd_to_token_amount(deps: Deps, denom: &str, usd_value: Uint128) -> StdResult<Uint128> {
    let config = CONFIG.load(deps.storage)?;
    let asset = ASSETS.load(deps.storage, denom)?;
    let price: Decimal = deps.querier.query_wasm_smart(
        config.oracle_address,
        &perp_oracle::msg::QueryMsg::GetPrice {
            base: denom.to_string(),
            quote: "USD".to_string(),
        },
    )?;

    if price.is_zero() { return Err(StdError::generic_err("Price is zero")); }

    let decimal_factor = Uint128::from(10u128.pow(asset.decimals as u32));
    Decimal::from_ratio(usd_value, Uint128::new(1))
        .checked_div(price)?
        .checked_mul(Decimal::from_ratio(decimal_factor, Uint128::new(1)))?
        .to_uint_floor()
}

3.8 费用管理

// perp_pool/src/fees.rs

use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Uint128, Decimal, WasmMsg, to_binary};
use crate::state::*;
use crate::error::ContractError;

pub fn collect_trading_fees(deps: DepsMut, amount: Uint128) -> StdResult<()> {
    let mut pool = POOL.load(deps.storage)?;
    let perf_fee_amount = Decimal::from_ratio(amount, Uint128::new(1))
        .checked_mul(pool.config.performance_fee)?
        .to_uint_floor();
    let lp_fee_amount = amount.checked_sub(perf_fee_amount)?;

    pool.fees_collected = pool.fees_collected.checked_add(amount)?;
    pool.net_value = pool.net_value.checked_add(lp_fee_amount)?;
    pool.total_value = pool.total_value.checked_add(lp_fee_amount)?;
    POOL.save(deps.storage, &pool)?;
    Ok(())
}

pub fn execute_claim_fees(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
) -> Result<Response, ContractError> {
    let mut claim = LP_FEE_CLAIMS
        .may_load(deps.storage, &info.sender)?
        .unwrap_or(LpFeeClaim {
            user: info.sender.clone(),
            pending_fees: Uint128::zero(),
            last_claim_time: env.block.time,
        });

    if claim.pending_fees.is_zero() {
        return Err(ContractError::NoPendingFees {});
    }

    let amount = claim.pending_fees;
    claim.pending_fees = Uint128::zero();
    claim.last_claim_time = env.block.time;
    LP_FEE_CLAIMS.save(deps.storage, &info.sender, &claim)?;

    let pool = POOL.load(deps.storage)?;
    Ok(Response::new()
        .add_message(WasmMsg::Execute {
            contract_addr: pool.lp_token.to_string(),
            msg: to_binary(&cw20::Cw20ExecuteMsg::Mint {
                recipient: info.sender.to_string(),
                amount,
            })?,
            funds: vec![],
        })
        .add_attribute("action", "claim_fees")
        .add_attribute("user", info.sender.to_string())
        .add_attribute("amount", amount.to_string()))
}

3.9 错误定义

// perp_pool/src/error.rs

use cosmwasm_std::{Uint128, Decimal, StdError};
use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
    #[error("{0}")]
    Std(#[from] StdError),

    #[error("Asset {denom} is not supported")]
    AssetNotSupported { denom: String },

    #[error("Asset {denom} is not active")]
    AssetNotActive { denom: String },

    #[error("Insufficient deposit: required {required}, provided {provided}")]
    InsufficientDeposit { required: Uint128, provided: Uint128 },

    #[error("Insufficient funds sent: expected {expected}, received {received}")]
    InsufficientFundsSent { expected: Uint128, received: Uint128 },

    #[error("Max pool share exceeded: max {max_share}, attempted {attempted}")]
    MaxPoolShareExceeded { max_share: Decimal, attempted: Decimal },

    #[error("Slippage exceeded: expected {expected}, actual {actual}")]
    SlippageExceeded { expected: Uint128, actual: Uint128 },

    #[error("Insufficient LP tokens: balance {balance}, required {required}")]
    InsufficientLpTokens { balance: Uint128, required: Uint128 },

    #[error("No pending fees to claim")]
    NoPendingFees {},

    #[error("Unauthorized")]
    Unauthorized {},

    #[error("Contract error: {reason}")]
    Generic { reason: String },
}

4. 交易与头寸管理

4.1 交易引擎概述

perp_trading 合约负责所有交易操作:开仓、平仓、增减抵押品。依赖 perp_pool, perp_oracle, perp_liquidation。

4.2 数据结构

// perp_trading/src/state.rs

use cosmwasm_std::{Addr, Uint128, Decimal, Timestamp};
use cw_storage_plus::{Item, Map};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct TradingPair {
    pub name: String,
    pub base_asset: String,
    pub quote_asset: String,
    pub virtual_base_reserve: Uint128,
    pub virtual_quote_reserve: Uint128,
    pub max_leverage: Decimal,
    pub maintenance_margin_ratio: Decimal,
    pub min_position_size: Uint128,
    pub max_position_size: Uint128,
    pub open_fee_rate: Decimal,
    pub close_fee_rate: Decimal,
    pub current_funding_rate: Decimal,
    pub last_funding_time: Timestamp,
    pub long_oi: Uint128,
    pub short_oi: Uint128,
    pub is_active: bool,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Position {
    pub id: u64,
    pub trader: Addr,
    pub pair: String,
    pub size: Uint128,
    pub collateral: Uint128,
    pub leverage: Decimal,
    pub entry_price: Decimal,
    pub is_long: bool,
    pub open_time: Timestamp,
    pub last_funding_time: Timestamp,
    pub accumulated_funding: Uint128,
    pub realized_pnl: Uint128,
    pub is_closed: bool,
    pub stop_loss: Option<Decimal>,
    pub take_profit: Option<Decimal>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct TradeRecord {
    pub id: u64,
    pub trader: Addr,
    pub pair: String,
    pub trade_type: TradeType,
    pub volume: Uint128,
    pub price: Decimal,
    pub timestamp: Timestamp,
    pub fee: Uint128,
    pub tx_hash: String,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum TradeType {
    OpenLong, OpenShort, CloseLong, CloseShort,
    AddCollateral, RemoveCollateral, Liquidate,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct TradingConfig {
    pub owner: Addr,
    pub pool_address: Addr,
    pub oracle_address: Addr,
    pub liquidation_address: Addr,
    pub max_positions_per_trader: u32,
    pub fee_collector: Addr,
}

pub const CONFIG: Item<TradingConfig> = Item::new("config");
pub const TRADING_PAIRS: Map<&str, TradingPair> = Map::new("pairs");
pub const POSITIONS: Map<u64, Position> = Map::new("positions");
pub const POSITION_COUNTER: Item<u64> = Item::new("pos_counter");
pub const TRADE_COUNTER: Item<u64> = Item::new("trade_counter");
pub const TRADES: Map<u64, TradeRecord> = Map::new("trades");
pub const TRADER_POSITIONS: Map<&Addr, Vec<u64>> = Map::new("trader_pos");

4.3 合约入口

// perp_trading/src/contract.rs

use cosmwasm_std::{
    entry_point, to_binary, Binary, Deps, DepsMut, Env,
    MessageInfo, Response, StdResult, Uint128, Decimal,
    WasmMsg, CosmosMsg, BankMsg, Coin,
};
use crate::state::*;
use crate::error::ContractError as TradingError;

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
    pub owner: String,
    pub pool_address: String,
    pub oracle_address: String,
    pub liquidation_address: String,
    pub fee_collector: String,
    pub max_positions_per_trader: u32,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    OpenPosition {
        pair: String,
        collateral: Uint128,
        leverage: Decimal,
        is_long: bool,
        min_price: Option<Decimal>,
        max_price: Option<Decimal>,
        stop_loss: Option<Decimal>,
        take_profit: Option<Decimal>,
    },
    ClosePosition { position_id: u64, min_price: Option<Decimal> },
    AddCollateral { position_id: u64, amount: Uint128 },
    RemoveCollateral { position_id: u64, amount: Uint128 },
    SetTakeProfitStopLoss {
        position_id: u64,
        take_profit: Option<Decimal>,
        stop_loss: Option<Decimal>,
    },
    CreateTradingPair {
        name: String, base_asset: String, quote_asset: String,
        virtual_base_reserve: Uint128, virtual_quote_reserve: Uint128,
        max_leverage: Decimal, min_position_size: Uint128, max_position_size: Uint128,
    },
    UpdateTradingPair {
        name: String,
        max_leverage: Option<Decimal>,
        min_position_size: Option<Uint128>,
        max_position_size: Option<Uint128>,
        open_fee_rate: Option<Decimal>,
        close_fee_rate: Option<Decimal>,
    },
    SettleFunding { pair: String },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    GetPosition { position_id: u64 },
    GetTraderPositions { trader: String },
    GetTradingPair { name: String },
    GetTradingPairs {},
    GetUnrealizedPnl { position_id: u64 },
    GetMaxWithdrawableCollateral { position_id: u64 },
    GetLiquidationPrice { position_id: u64 },
}

4.4 实例化

#[entry_point]
pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> Result<Response, TradingError> {
    let config = TradingConfig {
        owner: deps.api.addr_validate(&msg.owner)?,
        pool_address: deps.api.addr_validate(&msg.pool_address)?,
        oracle_address: deps.api.addr_validate(&msg.oracle_address)?,
        liquidation_address: deps.api.addr_validate(&msg.liquidation_address)?,
        fee_collector: deps.api.addr_validate(&msg.fee_collector)?,
        max_positions_per_trader: msg.max_positions_per_trader,
    };
    CONFIG.save(deps.storage, &config)?;
    POSITION_COUNTER.save(deps.storage, &0u64)?;
    TRADE_COUNTER.save(deps.storage, &0u64)?;
    Ok(Response::new()
        .add_attribute("action", "instantiate")
        .add_attribute("contract", "perp_trading"))
}

#[entry_point]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, TradingError> {
    match msg {
        ExecuteMsg::OpenPosition { pair, collateral, leverage, is_long,
            min_price, max_price, stop_loss, take_profit } =>
            execute_open_position(deps, env, info, pair, collateral, leverage,
                is_long, min_price, max_price, stop_loss, take_profit),
        ExecuteMsg::ClosePosition { position_id, min_price } =>
            execute_close_position(deps, env, info, position_id, min_price),
        ExecuteMsg::AddCollateral { position_id, amount } =>
            execute_add_collateral(deps, env, info, position_id, amount),
        ExecuteMsg::RemoveCollateral { position_id, amount } =>
            execute_remove_collateral(deps, env, info, position_id, amount),
        ExecuteMsg::SetTakeProfitStopLoss { position_id, take_profit, stop_loss } =>
            execute_set_tp_sl(deps, env, info, position_id, take_profit, stop_loss),
        ExecuteMsg::CreateTradingPair { name, base_asset, quote_asset,
            virtual_base_reserve, virtual_quote_reserve,
            max_leverage, min_position_size, max_position_size } =>
            execute_create_pair(deps, env, info, name, base_asset, quote_asset,
                virtual_base_reserve, virtual_quote_reserve,
                max_leverage, min_position_size, max_position_size),
        ExecuteMsg::UpdateTradingPair { name, max_leverage, min_position_size,
            max_position_size, open_fee_rate, close_fee_rate } =>
            execute_update_pair(deps, env, info, name, max_leverage,
                min_position_size, max_position_size, open_fee_rate, close_fee_rate),
        ExecuteMsg::SettleFunding { pair } =>
            execute_settle_funding(deps, env, info, pair),
    }
}

4.5 开仓逻辑

pub fn execute_open_position(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    pair_name: String,
    collateral_amount: Uint128,
    leverage: Decimal,
    is_long: bool,
    min_price: Option<Decimal>,
    max_price: Option<Decimal>,
    stop_loss: Option<Decimal>,
    take_profit: Option<Decimal>,
) -> Result<Response, TradingError> {
    let config = CONFIG.load(deps.storage)?;
    let mut pair = TRADING_PAIRS.load(deps.storage, &pair_name)
        .map_err(|_| TradingError::PairNotFound { pair: pair_name.clone() })?;

    if !pair.is_active { return Err(TradingError::PairNotActive { pair: pair_name }); }
    if leverage > pair.max_leverage {
        return Err(TradingError::LeverageTooHigh { max: pair.max_leverage, requested: leverage });
    }
    if leverage < Decimal::one() {
        return Err(TradingError::LeverageTooLow { min: Decimal::one(), requested: leverage });
    }

    let position_size = Decimal::from_ratio(collateral_amount, Uint128::new(1))
        .checked_mul(leverage)?.to_uint_floor();

    if position_size < pair.min_position_size {
        return Err(TradingError::PositionTooSmall { min: pair.min_position_size, size: position_size });
    }
    if position_size > pair.max_position_size {
        return Err(TradingError::PositionTooLarge { max: pair.max_position_size, size: position_size });
    }

    let oracle_price = query_oracle_price(deps.as_ref(), &config.oracle_address,
        &pair.base_asset, &pair.quote_asset)?;
    let (vamm_price, price_impact) = get_vamm_price(&pair, position_size, is_long);
    let execution_price = calculate_execution_price(oracle_price, vamm_price, is_long);

    if let Some(min) = min_price {
        if (is_long && execution_price > min) || (!is_long && execution_price < min) {
            return Err(TradingError::PriceSlippageExceeded {
                expected: min, actual: execution_price,
            });
        }
    }
    if let Some(max) = max_price {
        if (is_long && execution_price > max) || (!is_long && execution_price < max) {
            return Err(TradingError::PriceSlippageExceeded {
                expected: max, actual: execution_price,
            });
        }
    }

    let open_fee = Decimal::from_ratio(position_size, Uint128::new(1))
        .checked_mul(pair.open_fee_rate)?.to_uint_floor();

    validate_pool_depth(deps.as_ref(), &config.pool_address, &pair_name, position_size)?;

    let collateral_received = info.funds.iter()
        .find(|c| c.denom == "uusd").map(|c| c.amount).unwrap_or(Uint128::zero());
    if collateral_received < collateral_amount {
        return Err(TradingError::InsufficientCollateralSent {
            expected: collateral_amount, received: collateral_received,
        });
    }

    let pos_counter = POSITION_COUNTER.load(deps.storage)?;
    let position_id = pos_counter.checked_add(1)?;

    let position = Position {
        id: position_id,
        trader: info.sender.clone(),
        pair: pair_name.clone(),
        size: position_size,
        collateral: collateral_amount.checked_sub(open_fee)?,
        leverage,
        entry_price: execution_price,
        is_long,
        open_time: env.block.time,
        last_funding_time: env.block.time,
        accumulated_funding: Uint128::zero(),
        realized_pnl: Uint128::zero(),
        is_closed: false,
        stop_loss,
        take_profit,
    };

    POSITIONS.save(deps.storage, position_id, &position)?;
    POSITION_COUNTER.save(deps.storage, &position_id)?;

    let mut trader_positions = TRADER_POSITIONS
        .may_load(deps.storage, &info.sender)?.unwrap_or_default();
    trader_positions.push(position_id);
    TRADER_POSITIONS.save(deps.storage, &info.sender, &trader_positions)?;

    if is_long { pair.long_oi = pair.long_oi.checked_add(position_size)?; }
    else { pair.short_oi = pair.short_oi.checked_add(position_size)?; }
    TRADING_PAIRS.save(deps.storage, &pair_name, &pair)?;

    Ok(Response::new()
        .add_message(BankMsg::Send {
            to_address: config.pool_address.to_string(),
            amount: vec![Coin { denom: "uusd".to_string(), amount: collateral_amount.checked_sub(open_fee)? }],
        })
        .add_message(BankMsg::Send {
            to_address: config.fee_collector.to_string(),
            amount: vec![Coin { denom: "uusd".to_string(), amount: open_fee }],
        })
        .add_message(WasmMsg::Execute {
            contract_addr: config.liquidation_address.to_string(),
            msg: to_binary(&perp_liquidation::msg::ExecuteMsg::MonitorPosition { position_id })?,
            funds: vec![],
        })
        .add_attribute("action", "open_position")
        .add_attribute("position_id", position_id.to_string())
        .add_attribute("trader", info.sender.to_string())
        .add_attribute("pair", pair_name)
        .add_attribute("size", position_size.to_string())
        .add_attribute("collateral", position.collateral.to_string())
        .add_attribute("leverage", leverage.to_string())
        .add_attribute("entry_price", execution_price.to_string())
        .add_attribute("direction", if is_long { "long" } else { "short" }))
}

4.6 平仓逻辑

pub fn execute_close_position(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    position_id: u64,
    min_price: Option<Decimal>,
) -> Result<Response, TradingError> {
    let config = CONFIG.load(deps.storage)?;
    let mut position = POSITIONS.load(deps.storage, position_id)
        .map_err(|_| TradingError::PositionNotFound { id: position_id })?;

    if position.trader != info.sender { return Err(TradingError::Unauthorized {}); }
    if position.is_closed { return Err(TradingError::PositionAlreadyClosed { id: position_id }); }

    let mut pair = TRADING_PAIRS.load(deps.storage, &position.pair)?;
    let oracle_price = query_oracle_price(deps.as_ref(), &config.oracle_address,
        &pair.base_asset, &pair.quote_asset)?;
    let (vamm_price, _) = get_vamm_price(&pair, position.size, !position.is_long);
    let exit_price = calculate_execution_price(oracle_price, vamm_price, !position.is_long);

    if let Some(min) = min_price {
        if (position.is_long && exit_price < min) || (!position.is_long && exit_price > min) {
            return Err(TradingError::PriceSlippageExceeded {
                expected: min, actual: exit_price,
            });
        }
    }

    let (pnl, pnl_direction) = calculate_pnl(position.size, position.entry_price, exit_price, position.is_long);
    let funding_fee = calculate_pending_funding(deps.as_ref(), &position, &mut pair, env.block.time)?;
    let close_fee = Decimal::from_ratio(position.size, Uint128::new(1))
        .checked_mul(pair.close_fee_rate)?.to_uint_floor();

    let total_return = if pnl_direction == PnlDirection::Profit {
        position.collateral.checked_add(pnl)?.checked_sub(funding_fee)?.checked_sub(close_fee)?
    } else {
        if pnl >= position.collateral { Uint128::zero() }
        else { position.collateral.checked_sub(pnl)?.checked_sub(funding_fee)?.checked_sub(close_fee)? }
    };

    position.is_closed = true;
    position.realized_pnl = if pnl_direction == PnlDirection::Profit { pnl } else { Uint128::zero() };
    POSITIONS.save(deps.storage, position_id, &position)?;

    if position.is_long { pair.long_oi = pair.long_oi.checked_sub(position.size)?; }
    else { pair.short_oi = pair.short_oi.checked_sub(position.size)?; }
    TRADING_PAIRS.save(deps.storage, &position.pair, &pair)?;

    let mut msgs: Vec<CosmosMsg> = vec![];
    msgs.push(BankMsg::Send {
        to_address: config.fee_collector.to_string(),
        amount: vec![Coin { denom: "uusd".to_string(), amount: close_fee }],
    }.into());

    if total_return > Uint128::zero() {
        msgs.push(BankMsg::Send {
            to_address: info.sender.to_string(),
            amount: vec![Coin { denom: "uusd".to_string(), amount: total_return }],
        }.into());
    }

    Ok(Response::new()
        .add_messages(msgs)
        .add_attribute("action", "close_position")
        .add_attribute("position_id", position_id.to_string())
        .add_attribute("trader", info.sender.to_string())
        .add_attribute("pnl", pnl.to_string())
        .add_attribute("pnl_direction", format!("{:?}", pnl_direction))
        .add_attribute("exit_price", exit_price.to_string())
        .add_attribute("return_amount", total_return.to_string()))
}

4.7 抵押品管理

pub fn execute_add_collateral(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    position_id: u64,
    amount: Uint128,
) -> Result<Response, TradingError> {
    let config = CONFIG.load(deps.storage)?;
    let mut position = POSITIONS.load(deps.storage, position_id)
        .map_err(|_| TradingError::PositionNotFound { id: position_id })?;

    if position.trader != info.sender { return Err(TradingError::Unauthorized {}); }
    if position.is_closed { return Err(TradingError::PositionAlreadyClosed { id: position_id }); }

    let funds_sent = info.funds.iter()
        .find(|c| c.denom == "uusd").map(|c| c.amount).unwrap_or(Uint128::zero());
    if funds_sent < amount {
        return Err(TradingError::InsufficientCollateralSent { expected: amount, received: funds_sent });
    }

    position.collateral = position.collateral.checked_add(amount)?;
    POSITIONS.save(deps.storage, position_id, &position)?;

    Ok(Response::new()
        .add_message(BankMsg::Send {
            to_address: config.pool_address.to_string(),
            amount: vec![Coin { denom: "uusd".to_string(), amount }],
        })
        .add_attribute("action", "add_collateral")
        .add_attribute("position_id", position_id.to_string())
        .add_attribute("amount", amount.to_string())
        .add_attribute("new_collateral", position.collateral.to_string()))
}

pub fn execute_remove_collateral(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    position_id: u64,
    amount: Uint128,
) -> Result<Response, TradingError> {
    let config = CONFIG.load(deps.storage)?;
    let mut position = POSITIONS.load(deps.storage, position_id)
        .map_err(|_| TradingError::PositionNotFound { id: position_id })?;

    if position.trader != info.sender { return Err(TradingError::Unauthorized {}); }
    if position.is_closed { return Err(TradingError::PositionAlreadyClosed { id: position_id }); }
    if amount >= position.collateral {
        return Err(TradingError::InsufficientCollateral {
            available: position.collateral, requested: amount,
        });
    }

    let new_collateral = position.collateral.checked_sub(amount)?;
    let pair = TRADING_PAIRS.load(deps.storage, &position.pair)?;
    let margin_ratio = Decimal::from_ratio(new_collateral, position.size);
    if margin_ratio < pair.maintenance_margin_ratio {
        return Err(TradingError::MarginRatioTooLow {
            min_ratio: pair.maintenance_margin_ratio, current: margin_ratio,
        });
    }

    position.collateral = new_collateral;
    POSITIONS.save(deps.storage, position_id, &position)?;

    Ok(Response::new()
        .add_message(BankMsg::Send {
            to_address: info.sender.to_string(),
            amount: vec![Coin { denom: "uusd".to_string(), amount }],
        })
        .add_attribute("action", "remove_collateral")
        .add_attribute("position_id", position_id.to_string())
        .add_attribute("amount", amount.to_string())
        .add_attribute("new_collateral", new_collateral.to_string()))
}

4.8 盈亏和保证金计算

// perp_trading/src/math.rs

use cosmwasm_std::{Uint128, Decimal, Deps, Addr, StdResult, StdError};
use crate::state::{Position, TradingPair, TRADING_PAIRS, CONFIG};

#[derive(Debug, PartialEq)]
pub enum PnlDirection { Profit, Loss }

pub fn calculate_pnl(
    position_size: Uint128, entry_price: Decimal,
    current_price: Decimal, is_long: bool,
) -> (Uint128, PnlDirection) {
    if is_long {
        if current_price > entry_price {
            let diff = current_price.checked_sub(entry_price).unwrap();
            let pnl = Decimal::from_ratio(position_size, Uint128::new(1))
                .checked_mul(Decimal::from_ratio(diff, entry_price)).unwrap().to_uint_floor();
            (pnl, PnlDirection::Profit)
        } else {
            let diff = entry_price.checked_sub(current_price).unwrap();
            let pnl = Decimal::from_ratio(position_size, Uint128::new(1))
                .checked_mul(Decimal::from_ratio(diff, entry_price)).unwrap().to_uint_floor();
            (pnl, PnlDirection::Loss)
        }
    } else {
        if current_price < entry_price {
            let diff = entry_price.checked_sub(current_price).unwrap();
            let pnl = Decimal::from_ratio(position_size, Uint128::new(1))
                .checked_mul(Decimal::from_ratio(diff, entry_price)).unwrap().to_uint_floor();
            (pnl, PnlDirection::Profit)
        } else {
            let diff = current_price.checked_sub(entry_price).unwrap();
            let pnl = Decimal::from_ratio(position_size, Uint128::new(1))
                .checked_mul(Decimal::from_ratio(diff, entry_price)).unwrap().to_uint_floor();
            (pnl, PnlDirection::Loss)
        }
    }
}

pub fn calculate_margin_ratio(
    position_size: Uint128, collateral: Uint128,
    unrealized_pnl: Uint128, pnl_direction: &PnlDirection,
) -> Decimal {
    let effective = match pnl_direction {
        PnlDirection::Profit => collateral.checked_add(unrealized_pnl).unwrap(),
        PnlDirection::Loss => {
            if unrealized_pnl >= collateral { Uint128::zero() }
            else { collateral.checked_sub(unrealized_pnl).unwrap() }
        }
    };
    if position_size.is_zero() { return Decimal::one(); }
    Decimal::from_ratio(effective, position_size)
}

pub fn query_oracle_price(deps: Deps, oracle_addr: &Addr, base: &str, quote: &str) -> StdResult<Decimal> {
    let price: Decimal = deps.querier.query_wasm_smart(
        oracle_addr.clone(),
        &perp_oracle::msg::QueryMsg::GetPrice {
            base: base.to_string(), quote: quote.to_string(),
        },
    )?;
    Ok(price)
}

pub fn validate_pool_depth(deps: Deps, pool_addr: &Addr, _pair_name: &str, position_size: Uint128) -> StdResult<()> {
    let pool_info: perp_pool::msg::PoolResponse = deps.querier.query_wasm_smart(
        pool_addr.clone(), &perp_pool::msg::QueryMsg::GetPool {},
    )?;
    let required = Decimal::from_ratio(position_size, Uint128::new(1))
        .checked_mul(Decimal::percent(110))?.to_uint_floor();
    if pool_info.net_value < required {
        return Err(StdError::generic_err("Insufficient pool depth"));
    }
    Ok(())
}

4.9 交易对创建与查询

pub fn execute_create_pair(
    deps: DepsMut, _env: Env, info: MessageInfo,
    name: String, base_asset: String, quote_asset: String,
    virtual_base_reserve: Uint128, virtual_quote_reserve: Uint128,
    max_leverage: Decimal, min_position_size: Uint128, max_position_size: Uint128,
) -> Result<Response, TradingError> {
    let config = CONFIG.load(deps.storage)?;
    if info.sender != config.owner { return Err(TradingError::Unauthorized {}); }
    if TRADING_PAIRS.has(deps.storage, &name) {
        return Err(TradingError::PairAlreadyExists { pair: name });
    }

    let pair = TradingPair {
        name: name.clone(), base_asset, quote_asset,
        virtual_base_reserve, virtual_quote_reserve,
        max_leverage, maintenance_margin_ratio: Decimal::percent(10),
        min_position_size, max_position_size,
        open_fee_rate: Decimal::from_ratio(1u128, 1000u128),
        close_fee_rate: Decimal::from_ratio(1u128, 1000u128),
        current_funding_rate: Decimal::zero(),
        last_funding_time: Timestamp::from_nanos(0),
        long_oi: Uint128::zero(), short_oi: Uint128::zero(), is_active: true,
    };

    TRADING_PAIRS.save(deps.storage, &name, &pair)?;
    Ok(Response::new().add_attribute("action", "create_pair").add_attribute("pair", name))
}

#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::GetPosition { position_id } => to_binary(&POSITIONS.load(deps.storage, position_id)?),
        QueryMsg::GetTraderPositions { trader } => {
            let addr = deps.api.addr_validate(&trader)?;
            let ids = TRADER_POSITIONS.may_load(deps.storage, &addr)?.unwrap_or_default();
            let mut positions = vec![];
            for id in ids {
                if let Ok(pos) = POSITIONS.load(deps.storage, id) {
                    if !pos.is_closed { positions.push(pos); }
                }
            }
            to_binary(&positions)
        }
        QueryMsg::GetTradingPair { name } => to_binary(&TRADING_PAIRS.load(deps.storage, &name)?),
        QueryMsg::GetTradingPairs {} => {
            let pairs = TRADING_PAIRS.range(deps.storage, None, None, cosmwasm_std::Order::Ascending)
                .map(|r| r.map(|(_, p)| p)).collect::<StdResult<Vec<_>>>()?;
            to_binary(&pairs)
        }
        QueryMsg::GetUnrealizedPnl { position_id } => {
            let pos = POSITIONS.load(deps.storage, position_id)?;
            let config = CONFIG.load(deps.storage)?;
            let pair = TRADING_PAIRS.load(deps.storage, &pos.pair)?;
            let price = query_oracle_price(deps, &config.oracle_address, &pair.base_asset, &pair.quote_asset)?;
            let (pnl, dir) = calculate_pnl(pos.size, pos.entry_price, price, pos.is_long);
            to_binary(&serde_json::json!({
                "pnl": pnl, "direction": format!("{:?}", dir), "current_price": price,
            }))
        }
        _ => Err(StdError::generic_err("Unknown query")),
    }
}

5. vAMM 定价

5.1 vAMM 原理

虚拟自动做市商 (vAMM) 使用恒定乘积公式模拟订单簿深度:

k = base_reserve * quote_reserve

5.2 定价数学模型

初始:
  base_reserve = 1,000,000 (BTC)
  quote_reserve = 65,000,000,000 (USD)
  k = 65,000,000,000,000,000

买入 10 BTC:
  新 base = 999,990
  新 quote = k / 999,990 = 65,000,650,006.5
  均价 = 650,006.5 / 10 = 65,000.65
  滑点 = (65,000.65 - 65,000) / 65,000 = 0.001%

5.3 定价实现

// perp_trading/src/vamm.rs

use cosmwasm_std::{Uint128, Decimal, StdResult, StdError};
use crate::state::TradingPair;

#[derive(Debug, Clone, PartialEq)]
pub struct VammPriceResult {
    pub execution_price: Decimal,
    pub price_impact: Decimal,
    pub new_base_reserve: Uint128,
    pub new_quote_reserve: Uint128,
    pub quote_amount: Uint128,
}

pub fn get_vamm_price(
    pair: &TradingPair, trade_size: Uint128, is_buy: bool,
) -> (Decimal, Decimal) {
    let k = pair.virtual_base_reserve.checked_mul(pair.virtual_quote_reserve).unwrap();

    let (execution_price, price_impact) = if is_buy {
        let new_base = pair.virtual_base_reserve
            .checked_sub(trade_size).unwrap_or(Uint128::zero());
        if new_base.is_zero() { return (Decimal::zero(), Decimal::one()); }
        let new_quote = k.checked_div(new_base).unwrap();
        let quote_amount = new_quote.checked_sub(pair.virtual_quote_reserve).unwrap();
        let price = Decimal::from_ratio(quote_amount, trade_size);
        let mid = Decimal::from_ratio(pair.virtual_quote_reserve, pair.virtual_base_reserve);
        let impact = if mid.is_zero() { Decimal::zero() }
            else { Decimal::from_ratio(price, mid).checked_sub(Decimal::one()).unwrap_or(Decimal::zero()) };
        (price, impact)
    } else {
        let new_base = pair.virtual_base_reserve.checked_add(trade_size).unwrap();
        let new_quote = k.checked_div(new_base).unwrap();
        let quote_amount = pair.virtual_quote_reserve.checked_sub(new_quote).unwrap();
        let price = Decimal::from_ratio(quote_amount, trade_size);
        let mid = Decimal::from_ratio(pair.virtual_quote_reserve, pair.virtual_base_reserve);
        let impact = if mid.is_zero() { Decimal::zero() }
            else { Decimal::one().checked_sub(Decimal::from_ratio(price, mid)).unwrap_or(Decimal::zero()) };
        (price, impact)
    };

    (execution_price, price_impact)
}

pub fn get_vamm_price_precise(
    pair: &TradingPair, trade_size_usd: Uint128,
    is_buy: bool, current_oracle_price: Decimal,
) -> StdResult<VammPriceResult> {
    let base_amount = if current_oracle_price.is_zero() {
        return Err(StdError::generic_err("Oracle price is zero"));
    } else {
        Decimal::from_ratio(trade_size_usd, Uint128::new(1))
            .checked_div(current_oracle_price)?.to_uint_floor()
    };

    if base_amount.is_zero() {
        return Err(StdError::generic_err("Position size too small"));
    }

    let k = pair.virtual_base_reserve.checked_mul(pair.virtual_quote_reserve)?;
    let mid_price = Decimal::from_ratio(pair.virtual_quote_reserve, pair.virtual_base_reserve);

    let (new_base, new_quote, quote_amount, price_impact) = if is_buy {
        let new_base = pair.virtual_base_reserve.checked_sub(base_amount)
            .map_err(|_| StdError::generic_err("Insufficient virtual reserve"))?;
        let new_quote = k.checked_div(new_base)?;
        let qa = new_quote.checked_sub(pair.virtual_quote_reserve)?;
        let ep = Decimal::from_ratio(qa, base_amount);
        let pi = if mid_price.is_zero() { Decimal::zero() }
            else { Decimal::from_ratio(ep, mid_price).checked_sub(Decimal::one()).unwrap_or(Decimal::zero()) };
        (new_base, new_quote, qa, pi)
    } else {
        let new_base = pair.virtual_base_reserve.checked_add(base_amount)?;
        let new_quote = k.checked_div(new_base)?;
        let qa = pair.virtual_quote_reserve.checked_sub(new_quote)?;
        let ep = Decimal::from_ratio(qa, base_amount);
        let pi = if mid_price.is_zero() { Decimal::zero() }
            else { Decimal::one().checked_sub(Decimal::from_ratio(ep, mid_price)).unwrap_or(Decimal::zero()) };
        (new_base, new_quote, qa, pi)
    };

    let execution_price = Decimal::from_ratio(quote_amount, base_amount);
    Ok(VammPriceResult { execution_price, price_impact, new_base_reserve: new_base, new_quote_reserve: new_quote, quote_amount })
}

pub fn calculate_execution_price(oracle_price: Decimal, vamm_price: Decimal, _is_buy: bool) -> Decimal {
    oracle_price.checked_mul(Decimal::percent(50)).unwrap()
        .checked_add(vamm_price.checked_mul(Decimal::percent(50)).unwrap()).unwrap()
}

pub fn update_virtual_reserves(
    pair: &mut TradingPair, trade_size_usd: Uint128,
    oracle_price: Decimal, is_buy: bool,
) -> StdResult<()> {
    let base = Decimal::from_ratio(trade_size_usd, Uint128::new(1))
        .checked_div(oracle_price)?.to_uint_floor();
    let k = pair.virtual_base_reserve.checked_mul(pair.virtual_quote_reserve)?;
    if is_buy {
        pair.virtual_base_reserve = pair.virtual_base_reserve.checked_sub(base)
            .map_err(|_| StdError::generic_err("Insufficient reserve"))?;
        pair.virtual_quote_reserve = k.checked_div(pair.virtual_base_reserve)?;
    } else {
        pair.virtual_base_reserve = pair.virtual_base_reserve.checked_add(base)?;
        pair.virtual_quote_reserve = k.checked_div(pair.virtual_base_reserve)?;
    }
    Ok(())
}

5.4 滑点曲线

BTC/USD: base=1,000,000, quote=65,000,000,000

头寸大小    | 买方滑点   | 卖方滑点
$10,000    | 0.00015%   | 0.00015%
$100,000   | 0.0015%    | 0.0015%
$1,000,000 | 0.015%     | 0.015%
$10,000,000| 0.15%      | 0.15%
$100M      | 1.5%       | 1.5%

6. 资金费率

6.1 资金费率公式

funding_rate = clamp(premium * factor, -max_rate, +max_rate)
premium = (mark_price - index_price) / index_price
max_rate = 0.05% (per 8h)

6.2 资金费率实现

// perp_trading/src/funding.rs

use cosmwasm_std::{Uint128, Decimal, Timestamp, Deps, DepsMut, StdResult};
use crate::state::*;
use crate::error::ContractError;

#[derive(Debug, Clone)]
pub struct FundingConfig {
    pub funding_interval: u64,
    pub max_funding_rate: Decimal,
    pub clamp_factor: Decimal,
}

impl Default for FundingConfig {
    fn default() -> Self {
        Self {
            funding_interval: 28800,
            max_funding_rate: Decimal::from_ratio(5u128, 10000u128),
            clamp_factor: Decimal::one(),
        }
    }
}

pub fn calculate_funding_rate(
    mark_price: Decimal, index_price: Decimal, config: &FundingConfig,
) -> Decimal {
    if index_price.is_zero() { return Decimal::zero(); }
    let premium = if mark_price > index_price {
        Decimal::from_ratio(mark_price.checked_sub(index_price).unwrap(), index_price)
    } else {
        Decimal::from_ratio(index_price.checked_sub(mark_price).unwrap(), index_price)
    };
    let raw = premium.checked_mul(config.clamp_factor).unwrap();
    if raw > config.max_funding_rate { config.max_funding_rate } else { raw }
}

pub fn calculate_pending_funding(
    deps: Deps, position: &Position, pair: &mut TradingPair, current_time: Timestamp,
) -> StdResult<Uint128> {
    let config = FundingConfig::default();
    let elapsed = current_time.minus(position.last_funding_time).seconds();
    if elapsed == 0 { return Ok(Uint128::zero()); }

    let time_frac = Decimal::from_ratio(Uint128::from(elapsed), Uint128::from(config.funding_interval));
    let stored = CONFIG.load(deps.storage)?;
    let oracle = crate::math::query_oracle_price(deps, &stored.oracle_address, &pair.base_asset, &pair.quote_asset)?;
    let mid = Decimal::from_ratio(pair.virtual_quote_reserve, pair.virtual_base_reserve);
    let mark = mid.checked_add(oracle)?.checked_div(Decimal::from_ratio(2u128, 1u128))?;
    let rate = calculate_funding_rate(mark, oracle, &config);
    Decimal::from_ratio(position.size, Uint128::new(1))
        .checked_mul(rate)?.checked_mul(time_frac)?.to_uint_floor().into()
}

pub fn execute_settle_funding(
    deps: DepsMut, env: Env, _info: MessageInfo, pair_name: String,
) -> Result<Response, ContractError> {
    let stored = CONFIG.load(deps.storage)?;
    let mut pair = TRADING_PAIRS.load(deps.storage, &pair_name)
        .map_err(|_| ContractError::PairNotFound { pair: pair_name.clone() })?;
    let oracle = crate::math::query_oracle_price(deps.as_ref(), &stored.oracle_address,
        &pair.base_asset, &pair.quote_asset)?;
    let mid = Decimal::from_ratio(pair.virtual_quote_reserve, pair.virtual_base_reserve);
    let mark = mid.checked_add(oracle)?.checked_div(Decimal::from_ratio(2u128, 1u128))?;
    let cfg = FundingConfig::default();
    pair.current_funding_rate = calculate_funding_rate(mark, oracle, &cfg);
    pair.last_funding_time = env.block.time;
    TRADING_PAIRS.save(deps.storage, &pair_name, &pair)?;

    let positions = POSITIONS.range(deps.storage, None, None, cosmwasm_std::Order::Ascending)
        .collect::<StdResult<Vec<_>>>()?;

    for (_, mut pos) in positions {
        if pos.pair != pair_name || pos.is_closed { continue; }
        let payment = calculate_pending_funding(deps.as_ref(), &pos, &mut pair, env.block.time)?;
        if payment.is_zero() { continue; }
        pos.accumulated_funding = pos.accumulated_funding.checked_add(payment)?;
        pos.last_funding_time = env.block.time;
        POSITIONS.save(deps.storage, pos.id, &pos)?;
    }

    Ok(Response::new()
        .add_attribute("action", "settle_funding")
        .add_attribute("pair", pair_name)
        .add_attribute("funding_rate", pair.current_funding_rate.to_string()))
}

6.3 Python 模拟

# funding.py
def calc_funding(mark, index, max_rate=0.0005):
    if index == 0: return 0.0
    premium = (mark - index) / index
    return max(-max_rate, min(max_rate, premium))

def payment(size, rate, elapsed, interval=28800, is_long=True):
    amt = size * rate * (elapsed / interval)
    return amt if is_long else -amt

print("场景: 多空均衡")
print(f"  费率: {calc_funding(65100, 65000)*100:.4f}%")
print("\n场景: 极度看多")
print(f"  费率: {calc_funding(68000, 65000)*100:.4f}%")
print("\n场景: 10x多头持仓1小时成本")
r = calc_funding(65500, 65000)
p = payment(100000, r, 3600)
print(f"  费率: {r*100:.4f}%/8h")
print(f"  支付: ${p:.2f}")

7. 清算引擎

7.1 清算流程

1. Keeper 检查头寸
2. 计算保证金比率
3. 如果 < 10% 维持保证金
4.   执行 50% 部分清算
5.   清算人获得 5% 奖励
6.   剩余返还交易者

7.2 数据结构

// perp_liquidation/src/state.rs

use cosmwasm_std::{Addr, Uint128, Decimal, Timestamp};
use cw_storage_plus::{Item, Map};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct LiquidationConfig {
    pub owner: Addr,
    pub trading_address: Addr,
    pub pool_address: Addr,
    pub oracle_address: Addr,
    pub maintenance_margin_ratio: Decimal,
    pub liquidation_ratio: Decimal,
    pub liquidation_reward_ratio: Decimal,
    pub max_liquidation_attempts: u32,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct LiquidationRecord {
    pub id: u64,
    pub position_id: u64,
    pub liquidator: Addr,
    pub timestamp: Timestamp,
    pub liquidated_size: Uint128,
    pub liquidation_price: Decimal,
    pub reward: Uint128,
    pub returned_to_trader: Uint128,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct MonitoredPosition {
    pub position_id: u64,
    pub trader: Addr,
    pub pair: String,
    pub last_check_time: Timestamp,
    pub liquidation_attempts: u32,
}

impl Default for LiquidationConfig {
    fn default() -> Self {
        Self {
            owner: Addr::unchecked(""),
            trading_address: Addr::unchecked(""),
            pool_address: Addr::unchecked(""),
            oracle_address: Addr::unchecked(""),
            maintenance_margin_ratio: Decimal::percent(10),
            liquidation_ratio: Decimal::percent(50),
            liquidation_reward_ratio: Decimal::percent(5),
            max_liquidation_attempts: 5,
        }
    }
}

pub const CONFIG: Item<LiquidationConfig> = Item::new("config");
pub const LIQUIDATION_COUNTER: Item<u64> = Item::new("liq_counter");
pub const LIQUIDATIONS: Map<u64, LiquidationRecord> = Map::new("liquidations");
pub const MONITORED_POSITIONS: Map<u64, MonitoredPosition> = Map::new("monitored");

7.3 合约入口

// perp_liquidation/src/contract.rs

use cosmwasm_std::{
    entry_point, to_binary, Binary, Deps, DepsMut, Env,
    MessageInfo, Response, StdResult, Uint128, Decimal,
    WasmMsg, BankMsg, Coin, CosmosMsg,
};
use crate::state::*;
use crate::error::ContractError;

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
    pub owner: String,
    pub trading_address: String,
    pub pool_address: String,
    pub oracle_address: String,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    MonitorPosition { position_id: u64 },
    UnmonitorPosition { position_id: u64 },
    Liquidate { position_id: u64 },
    BatchLiquidate { position_ids: Vec<u64> },
    UpdateConfig {
        maintenance_margin_ratio: Option<Decimal>,
        liquidation_ratio: Option<Decimal>,
        liquidation_reward_ratio: Option<Decimal>,
    },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    IsLiquidatable { position_id: u64 },
    GetLiquidationRecord { id: u64 },
    GetMonitoredPositions {},
    GetLiquidatablePositions {},
}

#[entry_point]
pub fn instantiate(
    deps: DepsMut, _env: Env, _info: MessageInfo, msg: InstantiateMsg,
) -> Result<Response, ContractError> {
    let config = LiquidationConfig {
        owner: deps.api.addr_validate(&msg.owner)?,
        trading_address: deps.api.addr_validate(&msg.trading_address)?,
        pool_address: deps.api.addr_validate(&msg.pool_address)?,
        oracle_address: deps.api.addr_validate(&msg.oracle_address)?,
        ..LiquidationConfig::default()
    };
    CONFIG.save(deps.storage, &config)?;
    LIQUIDATION_COUNTER.save(deps.storage, &0u64)?;
    Ok(Response::new().add_attribute("action", "instantiate"))
}

7.4 监控管理

pub fn execute_monitor_position(
    deps: DepsMut, env: Env, _info: MessageInfo, position_id: u64,
) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    let position: perp_trading::state::Position = deps.querier.query_wasm_smart(
        config.trading_address.clone(),
        &perp_trading::msg::QueryMsg::GetPosition { position_id },
    ).map_err(|_| ContractError::PositionNotFound { id: position_id })?;

    let monitored = MonitoredPosition {
        position_id, trader: position.trader.clone(),
        pair: position.pair.clone(),
        last_check_time: env.block.time,
        liquidation_attempts: 0,
    };
    MONITORED_POSITIONS.save(deps.storage, position_id, &monitored)?;
    Ok(Response::new().add_attribute("action", "monitor_position"))
}

pub fn execute_unmonitor_position(
    deps: DepsMut, _env: Env, _info: MessageInfo, position_id: u64,
) -> Result<Response, ContractError> {
    MONITORED_POSITIONS.remove(deps.storage, position_id);
    Ok(Response::new().add_attribute("action", "unmonitor_position"))
}

7.5 清算执行

pub fn execute_liquidate(
    deps: DepsMut, env: Env, info: MessageInfo, position_id: u64,
) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    let monitored = MONITORED_POSITIONS.may_load(deps.storage, position_id)?
        .ok_or(ContractError::PositionNotMonitored { id: position_id })?;

    if monitored.liquidation_attempts >= config.max_liquidation_attempts {
        return Err(ContractError::MaxLiquidationAttemptsExceeded { id: position_id, attempts: monitored.liquidation_attempts });
    }

    let position: perp_trading::state::Position = deps.querier.query_wasm_smart(
        config.trading_address.clone(), &perp_trading::msg::QueryMsg::GetPosition { position_id },
    ).map_err(|_| ContractError::PositionNotFound { id: position_id })?;

    if position.is_closed { return Err(ContractError::PositionAlreadyClosed { id: position_id }); }

    let pair: perp_trading::state::TradingPair = deps.querier.query_wasm_smart(
        config.trading_address.clone(), &perp_trading::msg::QueryMsg::GetTradingPair { name: position.pair.clone() },
    ).map_err(|_| ContractError::PairNotFound { pair: position.pair.clone() })?;

    let oracle_price: Decimal = deps.querier.query_wasm_smart(
        config.oracle_address.clone(), &perp_oracle::msg::QueryMsg::GetPrice {
            base: pair.base_asset.clone(), quote: pair.quote_asset.clone(),
        },
    )?;

    let margin_ratio = calculate_current_margin_ratio(&position, oracle_price)?;
    if margin_ratio > config.maintenance_margin_ratio {
        return Err(ContractError::NotLiquidatable { id: position_id, margin_ratio, threshold: config.maintenance_margin_ratio });
    }

    let liquidate_amount = Decimal::from_ratio(position.size, Uint128::new(1))
        .checked_mul(config.liquidation_ratio)?.to_uint_floor();

    let liq_price = if position.is_long {
        oracle_price.checked_mul(Decimal::percent(98))?
    } else {
        oracle_price.checked_mul(Decimal::percent(102))?
    };

    let liq_value = Decimal::from_ratio(liquidate_amount, Uint128::new(1))
        .checked_mul(liq_price)?.to_uint_floor();

    let reward = Decimal::from_ratio(liq_value, Uint128::new(1))
        .checked_mul(config.liquidation_reward_ratio)?.to_uint_floor();

    let reduction = Decimal::from_ratio(liquidate_amount, position.size);
    let collateral_reduction = Decimal::from_ratio(position.collateral, Uint128::new(1))
        .checked_mul(reduction)?.to_uint_floor();
    let returned = collateral_reduction.checked_sub(reward).unwrap_or(Uint128::zero());

    let mut monitored = monitored;
    monitored.liquidation_attempts += 1;
    monitored.last_check_time = env.block.time;
    MONITORED_POSITIONS.save(deps.storage, position_id, &monitored)?;

    let counter = LIQUIDATION_COUNTER.load(deps.storage)?;
    let liq_id = counter.checked_add(1)?;
    LIQUIDATION_COUNTER.save(deps.storage, &liq_id)?;

    LIQUIDATIONS.save(deps.storage, liq_id, &LiquidationRecord {
        id: liq_id, position_id, liquidator: info.sender.clone(),
        timestamp: env.block.time, liquidated_size: liquidate_amount,
        liquidation_price: liq_price, reward, returned_to_trader: returned,
    })?;

    Ok(Response::new()
        .add_message(WasmMsg::Execute {
            contract_addr: config.trading_address.to_string(),
            msg: to_binary(&perp_trading::msg::ExecuteMsg::LiquidatePosition {
                position_id, liquidate_amount,
                liquidator: info.sender.to_string(), reward,
                returned_to_trader: returned, liquidation_price: liq_price,
            })?,
            funds: vec![],
        })
        .add_attribute("action", "liquidate")
        .add_attribute("liquidation_id", liq_id.to_string())
        .add_attribute("position_id", position_id.to_string())
        .add_attribute("liquidator", info.sender.to_string())
        .add_attribute("size", liquidate_amount.to_string())
        .add_attribute("reward", reward.to_string()))
}

7.6 清算数学

// perp_liquidation/src/math.rs

use cosmwasm_std::{Uint128, Decimal, StdResult};
use perp_trading::state::Position;

pub fn calculate_current_margin_ratio(position: &Position, current_price: Decimal) -> StdResult<Decimal> {
    let (upnl, dir) = calculate_unrealized_pnl(position.size, position.entry_price, current_price, position.is_long);
    let effective = match dir {
        PnlDirection::Profit => position.collateral.checked_add(upnl)?,
        PnlDirection::Loss => {
            if upnl >= position.collateral { Uint128::zero() }
            else { position.collateral.checked_sub(upnl)? }
        }
    };
    if position.size.is_zero() { return Ok(Decimal::one()); }
    Ok(Decimal::from_ratio(effective, position.size))
}

#[derive(Debug, PartialEq)]
pub enum PnlDirection { Profit, Loss }

pub fn calculate_unrealized_pnl(
    size: Uint128, entry: Decimal, current: Decimal, is_long: bool,
) -> (Uint128, PnlDirection) {
    if is_long {
        if current > entry {
            let d = current.checked_sub(entry).unwrap();
            (Decimal::from_ratio(size, Uint128::new(1)).checked_mul(Decimal::from_ratio(d, entry)).unwrap().to_uint_floor(), PnlDirection::Profit)
        } else {
            let d = entry.checked_sub(current).unwrap();
            (Decimal::from_ratio(size, Uint128::new(1)).checked_mul(Decimal::from_ratio(d, entry)).unwrap().to_uint_floor(), PnlDirection::Loss)
        }
    } else {
        if current < entry {
            let d = entry.checked_sub(current).unwrap();
            (Decimal::from_ratio(size, Uint128::new(1)).checked_mul(Decimal::from_ratio(d, entry)).unwrap().to_uint_floor(), PnlDirection::Profit)
        } else {
            let d = current.checked_sub(entry).unwrap();
            (Decimal::from_ratio(size, Uint128::new(1)).checked_mul(Decimal::from_ratio(d, entry)).unwrap().to_uint_floor(), PnlDirection::Loss)
        }
    }
}

7.7 Python 清算模拟

# liquidation.py
def margin_ratio(collateral, size, entry, current, is_long):
    upnl = ((current - entry) / entry * size) if is_long else ((entry - current) / entry * size)
    eff = collateral + upnl
    return max(0, eff / size)

entry, lev, coll = 65000, 10, 10000
size = coll * lev

print(f"{'Price':>10} {'Margin':>10} {'Status':>10}")
print("-" * 34)
for pct in range(0, 12):
    p = entry * (1 - pct / 100)
    mr = margin_ratio(coll, size, entry, p, True)
    s = "LIQ" if mr < 0.10 else "OK"
    print(f"${p:>8,.0f} {mr*100:>8.2f}% {s:>10}")

8. Oracle 集成

8.1 Oracle 实现

// perp_oracle/src/contract.rs

use cosmwasm_std::{
    entry_point, to_binary, Binary, Deps, DepsMut, Env,
    MessageInfo, Response, StdResult, Uint128, Decimal, Timestamp,
};
use cw_storage_plus::{Item, Map};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PriceSource {
    pub name: String, pub base: String, pub quote: String,
    pub last_price: Decimal, pub twap_price: Decimal,
    pub last_updated: Timestamp, pub heartbeat_seconds: u64,
    pub is_active: bool, pub deviation_threshold: Decimal,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct AggregatedPrice {
    pub base: String, pub quote: String,
    pub price: Decimal, pub twap: Decimal,
    pub last_updated: Timestamp,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct TwapDataPoint {
    pub price: Decimal, pub timestamp: Timestamp,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct OracleConfig {
    pub owner: Addr, pub twap_window_seconds: u64,
    pub max_price_age_seconds: u64, pub min_sources: u32,
    pub circuit_breaker_deviation: Decimal, pub circuit_breaker_active: bool,
}

pub const CONFIG: Item<OracleConfig> = Item::new("config");
pub const PRICE_SOURCES: Map<&str, PriceSource> = Map::new("sources");
pub const AGGREGATED_PRICES: Map<&str, AggregatedPrice> = Map::new("agg_prices");
pub const TWAP_HISTORY: Map<&str, Vec<TwapDataPoint>> = Map::new("twap_hist");

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    SubmitPrice { source: String, base: String, quote: String, price: Decimal },
    AddPriceSource { source: PriceSource },
    RemovePriceSource { source_name: String },
    ToggleCircuitBreaker {},
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    GetPrice { base: String, quote: String },
    GetTwapPrice { base: String, quote: String },
}

pub fn execute_submit_price(
    deps: DepsMut, env: Env, _info: MessageInfo,
    source: String, base: String, quote: String, price: Decimal,
) -> StdResult<Response> {
    let config = CONFIG.load(deps.storage)?;
    let pair_key = format!("{}/{}", base, quote);
    let mut ps = PRICE_SOURCES.load(deps.storage, &source)?;
    ps.last_price = price;
    ps.last_updated = env.block.time;
    PRICE_SOURCES.save(deps.storage, &source, &ps)?;

    let mut hist = TWAP_HISTORY.may_load(deps.storage, &pair_key)?.unwrap_or_default();
    let cutoff = env.block.time.minus_seconds(config.twap_window_seconds);
    hist.retain(|p| p.timestamp > cutoff);
    hist.push(TwapDataPoint { price, timestamp: env.block.time });
    TWAP_HISTORY.save(deps.storage, &pair_key, &hist)?;

    let aggregated = aggregate_price(deps.as_ref(), &base, &quote, &config)?;
    AGGREGATED_PRICES.save(deps.storage, &pair_key, &aggregated)?;

    Ok(Response::new()
        .add_attribute("action", "price_submitted")
        .add_attribute("pair", pair_key)
        .add_attribute("price", price.to_string()))
}

pub fn aggregate_price(deps: Deps, base: &str, quote: &str, config: &OracleConfig) -> StdResult<AggregatedPrice> {
    let sources = PRICE_SOURCES.range(deps.storage, None, None, cosmwasm_std::Order::Ascending)
        .collect::<StdResult<Vec<_>>>()?;

    let mut prices = vec![];
    let mut total = Decimal::zero();

    for (_, s) in &sources {
        if !s.is_active || s.base != base || s.quote != quote { continue; }
        let age = env.block.time.seconds().checked_sub(s.last_updated.seconds()).unwrap_or(0);
        if age > s.heartbeat_seconds { continue; }
        prices.push(s.last_price);
        total = total.checked_add(s.last_price).unwrap();
    }

    if (prices.len() as u32) < config.min_sources {
        return Err(StdError::generic_err("Not enough sources"));
    }

    let avg = total.checked_div(Decimal::from_ratio(prices.len() as u128, 1u128)).unwrap();
    let twap = calculate_twap(deps, base, quote, config)?;

    Ok(AggregatedPrice { base: base.into(), quote: quote.into(), price: avg, twap, last_updated: env.block.time })
}

pub fn calculate_twap(deps: Deps, base: &str, quote: &str, config: &OracleConfig) -> StdResult<Decimal> {
    let key = format!("{}/{}", base, quote);
    let hist = TWAP_HISTORY.may_load(deps.storage, &key)?.unwrap_or_default();
    if hist.is_empty() { return Ok(Decimal::zero()); }

    let mut weighted = Decimal::zero();
    let mut total_weight = Uint128::zero();

    for i in 1..hist.len() {
        let diff = hist[i].timestamp.seconds().checked_sub(hist[i-1].timestamp.seconds()).unwrap_or(0);
        if diff == 0 { continue; }
        let w = Uint128::from(diff);
        weighted = weighted.checked_add(Decimal::from_ratio(hist[i-1].price, w)).unwrap();
        total_weight = total_weight.checked_add(w).unwrap();
    }

    if let Some(last) = hist.last() {
        let diff = env.block.time.seconds().checked_sub(last.timestamp.seconds()).unwrap_or(0);
        if diff > 0 {
            let w = Uint128::from(diff);
            weighted = weighted.checked_add(Decimal::from_ratio(last.price, w)).unwrap();
            total_weight = total_weight.checked_add(w).unwrap();
        }
    }

    if total_weight.is_zero() { return Ok(Decimal::zero()); }
    weighted.checked_div(Decimal::from_ratio(total_weight, Uint128::new(1)))
}

#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::GetPrice { base, quote } => {
            let config = CONFIG.load(deps.storage)?;
            let key = format!("{}/{}", base, quote);
            let agg = AGGREGATED_PRICES.load(deps.storage, &key)?;
            if env.block.time.seconds().checked_sub(agg.last_updated.seconds()).unwrap_or(0) > config.max_price_age_seconds {
                return Err(StdError::generic_err("Price stale"));
            }
            to_binary(&agg.price)
        }
        QueryMsg::GetTwapPrice { base, quote } => {
            let key = format!("{}/{}", base, quote);
            let agg = AGGREGATED_PRICES.load(deps.storage, &key)?;
            to_binary(&agg.twap)
        }
    }
}

9. 前端实现

9.1 TypeScript 合约交互

// src/utils/contract.ts

import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
import { Coin } from "@cosmjs/amino";

export const CHAIN_CONFIG = {
  chainId: "msg-chain-1",
  rpc: "https://rpc.msg-chain-1.zone",
  prefix: "msg",
};

export class PerpClient {
  private client: SigningCosmWasmClient | null = null;
  private wallet: DirectSecp256k1HdWallet | null = null;

  async connect(mnemonic: string) {
    this.wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: CHAIN_CONFIG.prefix });
    this.client = await SigningCosmWasmClient.connectWithSigner(CHAIN_CONFIG.rpc, this.wallet);
  }

  async getAddress(): Promise<string> {
    const acc = await this.wallet!.getAccounts();
    return acc[0].address;
  }

  async deposit(poolAddr: string, denom: string, amount: string, minLp: string) {
    return this.client!.execute(await this.getAddress(), poolAddr, {
      deposit: { asset_denom: denom, amount, min_lp_tokens: minLp },
    }, "auto", undefined, [{ denom, amount } as Coin]);
  }

  async openPosition(addr: string, pair: string, coll: string, lev: string, long: boolean) {
    return this.client!.execute(await this.getAddress(), addr, {
      open_position: { pair, collateral: coll, leverage: lev, is_long: long,
        min_price: null, max_price: null, stop_loss: null, take_profit: null },
    }, "auto", undefined, [{ denom: "uusd", amount: coll } as Coin]);
  }

  async closePosition(addr: string, id: number) {
    return this.client!.execute(await this.getAddress(), addr, {
      close_position: { position_id: id, min_price: null },
    }, "auto");
  }

  async getPool(poolAddr: string) {
    return this.client!.queryContractSmart(poolAddr, { get_pool: {} });
  }

  async getPositions(tradingAddr: string, trader: string) {
    return this.client!.queryContractSmart(tradingAddr, { get_trader_positions: { trader } });
  }

  async getPrice(oracleAddr: string, base: string, quote: string) {
    return this.client!.queryContractSmart(oracleAddr, { get_price: { base, quote } });
  }
}

9.2 React 交易表单

// OrderForm.tsx
import React, { useState } from 'react';
import { PerpClient } from '../utils/contract';

interface Props {
  client: PerpClient;
  tradingAddr: string;
  pair: string;
  price: number;
}

export const OrderForm: React.FC<Props> = ({ client, tradingAddr, pair, price }) => {
  const [long, setLong] = useState(true);
  const [leverage, setLeverage] = useState(10);
  const [collateral, setCollateral] = useState('');
  const [loading, setLoading] = useState(false);

  const size = parseFloat(collateral || '0') * leverage;
  const liqPrice = long
    ? price * (1 - (1 / leverage - 0.1))
    : price * (1 + (1 / leverage - 0.1));

  const submit = async () => {
    setLoading(true);
    try {
      const coll = (parseFloat(collateral) * 1e18).toString();
      await client.openPosition(tradingAddr, pair, coll, leverage.toString(), long);
      setCollateral('');
    } catch (e) { alert((e as Error).message); }
    finally { setLoading(false); }
  };

  return (
    <div style={{ padding: 16 }}>
      <div>
        <button onClick={() => setLong(true)} style={{ background: long ? '#22c55e' : '#333', color: '#fff', padding: '8px 24px', marginRight: 8 }}>
          做多 {pair.split('/')[0]}
        </button>
        <button onClick={() => setLong(false)} style={{ background: !long ? '#ef4444' : '#333', color: '#fff', padding: '8px 24px' }}>
          做空 {pair.split('/')[0]}
        </button>
      </div>
      <div style={{ marginTop: 16 }}>
        <label>杠杆: {leverage}x</label>
        <input type="range" min="1" max="50" value={leverage}
          onChange={e => setLeverage(parseInt(e.target.value))}
          style={{ width: '100%' }} />
      </div>
      <div style={{ marginTop: 16 }}>
        <label>抵押品 (USDC)</label>
        <input type="number" value={collateral}
          onChange={e => setCollateral(e.target.value)}
          placeholder="0.00"
          style={{ width: '100%', padding: 8, marginTop: 4 }} />
      </div>
      <div style={{ marginTop: 12, color: '#666' }}>
        <div>头寸大小: ${size.toFixed(2)}</div>
        <div style={{ color: '#ef4444' }}>强平: ${liqPrice.toFixed(2)}</div>
      </div>
      <button onClick={submit} disabled={loading || !collateral}
        style={{ marginTop: 16, width: '100%', padding: 12, background: long ? '#22c55e' : '#ef4444', color: '#fff', border: 'none', borderRadius: 8 }}>
        {loading ? '提交中...' : `${long ? '开多' : '开空'} ${pair}`}
      </button>
    </div>
  );
};

9.3 持仓列表

// PositionTable.tsx
import React, { useEffect, useState } from 'react';

interface Position {
  id: number; pair: string; size: string; collateral: string;
  leverage: string; entry_price: string; is_long: boolean;
}

export const PositionTable: React.FC<{ client: any; addr: string; trader: string }> = ({ client, addr, trader }) => {
  const [positions, setPositions] = useState<Position[]>([]);

  useEffect(() => {
    const load = async () => {
      if (!client) return;
      const pos = await client.getPositions(addr, trader);
      setPositions(pos || []);
    };
    load();
    const id = setInterval(load, 5000);
    return () => clearInterval(id);
  }, [client, addr, trader]);

  return (
    <table style={{ width: '100%', borderCollapse: 'collapse' }}>
      <thead>
        <tr style={{ borderBottom: '1px solid #333' }}>
          <th style={thStyle}>交易对</th>
          <th style={thStyle}>方向</th>
          <th style={thStyle}>头寸</th>
          <th style={thStyle}>抵押品</th>
          <th style={thStyle}>杠杆</th>
          <th style={thStyle}>入场价</th>
          <th style={thStyle}>操作</th>
        </tr>
      </thead>
      <tbody>
        {positions.map((p: Position) => (
          <tr key={p.id} style={{ borderBottom: '1px solid #222' }}>
            <td style={tdStyle}>{p.pair}</td>
            <td style={{ ...tdStyle, color: p.is_long ? '#22c55e' : '#ef4444' }}>
              {p.is_long ? '多头' : '空头'}
            </td>
            <td style={tdStyle}>${(parseInt(p.size) / 1e18).toFixed(2)}</td>
            <td style={tdStyle}>${(parseInt(p.collateral) / 1e18).toFixed(2)}</td>
            <td style={tdStyle}>{p.leverage}x</td>
            <td style={tdStyle}>${(parseInt(p.entry_price) / 1e18).toFixed(2)}</td>
            <td style={tdStyle}>
              <button onClick={() => client.closePosition(addr, p.id)}
                style={{ background: '#ef4444', color: '#fff', border: 'none', padding: '4px 12px', borderRadius: 4 }}>
                平仓
              </button>
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};

const thStyle: React.CSSProperties = { padding: 8, textAlign: 'left', color: '#888' };
const tdStyle: React.CSSProperties = { padding: 8 };

10. 完整示例

10.1 部署与交互流程

// deploy.ts — MSG Chain 永续合约完整部署示例

import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";

async function main() {
  // 1. 连接 MSG Chain
  const mnemonic = "your seed phrase here";
  const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: "msg" });
  const client = await SigningCosmWasmClient.connectWithSigner(
    "https://rpc.msg-chain-1.zone", wallet
  );
  const sender = (await wallet.getAccounts())[0].address;
  console.log("Deployer:", sender);

  // 2. 上传合约 WASM 代码
  const poolCodeId = await uploadContract(client, sender, "perp_pool.wasm");
  const tradingCodeId = await uploadContract(client, sender, "perp_trading.wasm");
  const oracleCodeId = await uploadContract(client, sender, "perp_oracle.wasm");
  const liqCodeId = await uploadContract(client, sender, "perp_liquidation.wasm");

  // 3. 实例化预言机
  const oracleAddr = await client.instantiate(
    sender, oracleCodeId,
    { owner: sender, twap_window_seconds: 3600, max_price_age_seconds: 120, min_sources: 1, circuit_breaker_deviation: "0.05" },
    "perp_oracle", "auto"
  );
  console.log("Oracle:", oracleAddr.contractAddress);

  // 4. 实例化流动性池
  const poolAddr = await client.instantiate(
    sender, poolCodeId,
    { owner: sender, treasury: sender, performance_fee: "0.1", withdrawal_fee: "0.001", max_supply: "1000000000000000000000000000" },
    "perp_pool", "auto"
  );
  console.log("Pool:", poolAddr.contractAddress);

  // 5. 实例化清算引擎
  const liqAddr = await client.instantiate(
    sender, liqCodeId,
    { owner: sender, trading_address: "", pool_address: poolAddr.contractAddress, oracle_address: oracleAddr.contractAddress },
    "perp_liquidation", "auto"
  );
  console.log("Liquidation:", liqAddr.contractAddress);

  // 6. 实例化交易引擎(需要先有清算地址)
  const tradingAddr = await client.instantiate(
    sender, tradingCodeId,
    {
      owner: sender,
      pool_address: poolAddr.contractAddress,
      oracle_address: oracleAddr.contractAddress,
      liquidation_address: liqAddr.contractAddress,
      fee_collector: sender,
      max_positions_per_trader: 10,
    },
    "perp_trading", "auto"
  );
  console.log("Trading:", tradingAddr.contractAddress);

  console.log("\n=== 部署完成 ===");
  console.log("Oracle:", oracleAddr.contractAddress);
  console.log("Pool:", poolAddr.contractAddress);
  console.log("Liquidation:", liqAddr.contractAddress);
  console.log("Trading:", tradingAddr.contractAddress);
}

10.2 添加流动性

async function addLiquidity(client: SigningCosmWasmClient, sender: string, poolAddr: string) {
  // 添加 USDC 作为支持的资产
  await client.execute(sender, poolAddr, {
    add_asset: {
      asset: {
        denom: "uusd",
        decimals: 18,
        weight: "1.0",
        min_deposit: "1000000",
        max_pool_share: "1.0",
        is_active: true,
        cw20_address: null,
      },
    },
  }, "auto");

  // 存入 USDC 获取 LP Token
  const depositAmount = "10000000000000000000000"; // 10,000 USDC (18 decimals)
  await client.execute(sender, poolAddr, {
    deposit: {
      asset_denom: "uusd",
      amount: depositAmount,
      min_lp_tokens: "0",
    },
  }, "auto", undefined, [{ denom: "uusd", amount: depositAmount }]);

  console.log("流动性添加完成");
}

10.3 创建交易对并交易

async function trade(client: SigningCosmWasmClient, sender: string,
  tradingAddr: string, oracleAddr: string) {

  // 创建 BTC/USD 交易对
  await client.execute(sender, tradingAddr, {
    create_trading_pair: {
      name: "BTC/USD",
      base_asset: "BTC",
      quote_asset: "USD",
      virtual_base_reserve: "1000000000000000000000000", // 1M BTC
      virtual_quote_reserve: "65000000000000000000000000000000", // 65B USD
      max_leverage: "50",
      min_position_size: "10000000000000000000", // $10
      max_position_size: "1000000000000000000000000", // $1M
    },
  }, "auto");

  // 提交预言机价格
  await client.execute(sender, oracleAddr, {
    submit_price: {
      source: "binance",
      base: "BTC",
      quote: "USD",
      price: "65000.0",
    },
  }, "auto");

  // 开多头 (10x, $1,000 抵押品)
  const collateral = "1000000000000000000000"; // $1,000
  await client.execute(sender, tradingAddr, {
    open_position: {
      pair: "BTC/USD",
      collateral: collateral,
      leverage: "10",
      is_long: true,
      min_price: null,
      max_price: null,
      stop_loss: null,
      take_profit: null,
    },
  }, "auto", undefined, [{ denom: "uusd", amount: collateral }]);

  console.log("开仓成功");

  // 检查头寸
  const positions = await client.queryContractSmart(tradingAddr, {
    get_trader_positions: { trader: sender },
  });
  console.log("持仓:", JSON.stringify(positions, null, 2));

  // 平仓
  if (positions.length > 0) {
    await client.execute(sender, tradingAddr, {
      close_position: { position_id: positions[0].id, min_price: null },
    }, "auto");
    console.log("平仓成功");
  }
}

10.4 Python 示例

# deploy.py — MSG Chain 永续合约交互示例

import json
import requests
from cosmos import CosmosClient

# MSG Chain 配置
RPC = "https://rpc.msg-chain-1.zone"
REST = "https://rest.msg-chain-1.zone"
CHAIN_ID = "msg-chain-1"
PREFIX = "msg"

class PerpSDK:
    def __init__(self, mnemonic: str):
        self.client = CosmosClient(RPC, REST, CHAIN_ID, PREFIX, mnemonic)
        self.sender = self.client.get_address()

    def deposit_liquidity(self, pool_addr: str, amount: int):
        """存入 USDC 到流动性池"""
        msg = {
            "deposit": {
                "asset_denom": "uusd",
                "amount": str(amount),
                "min_lp_tokens": "0",
            }
        }
        funds = [{"denom": "uusd", "amount": str(amount)}]
        return self.client.execute(pool_addr, msg, funds)

    def open_long(self, trading_addr: str, collateral: int, leverage: int = 10):
        """开多头仓位"""
        msg = {
            "open_position": {
                "pair": "BTC/USD",
                "collateral": str(collateral),
                "leverage": str(leverage),
                "is_long": True,
                "min_price": None,
                "max_price": None,
                "stop_loss": None,
                "take_profit": None,
            }
        }
        funds = [{"denom": "uusd", "amount": str(collateral)}]
        return self.client.execute(trading_addr, msg, funds)

    def close_position(self, trading_addr: str, position_id: int):
        """平仓"""
        msg = {"close_position": {"position_id": position_id, "min_price": None}}
        return self.client.execute(trading_addr, msg)

    def get_positions(self, trading_addr: str):
        """查询持仓"""
        return self.client.query(trading_addr, {
            "get_trader_positions": {"trader": self.sender}
        })

    def get_pool_info(self, pool_addr: str):
        """查询池信息"""
        return self.client.query(pool_addr, {"get_pool": {}})


if __name__ == "__main__":
    sdk = PerpSDK("your mnemonic phrase here")

    # 合约地址(部署后获取)
    POOL = "msg1pool..."
    TRADING = "msg1trading..."
    ORACLE = "msg1oracle..."

    # 添加流动性
    print("添加流动性...")
    tx = sdk.deposit_liquidity(POOL, 1_000_000_000_000_000_000_000)  # $1,000
    print(f"   TX: {tx}")

    # 开仓
    print("开多头 BTC/USD 10x...")
    tx = sdk.open_long(TRADING, 500_000_000_000_000_000_000, 10)  # $500 collateral
    print(f"   TX: {tx}")

    # 查询持仓
    pos = sdk.get_positions(TRADING)
    print(f"持仓数: {len(pos)}")

    # 查询池
    pool = sdk.get_pool_info(POOL)
    print(f"池总价值: ${int(pool['total_value']) / 1e18:,.2f}")

    if pos:
        print("平仓...")
        tx = sdk.close_position(TRADING, pos[0]['id'])
        print(f"   TX: {tx}")

10.5 部署总结

部署步骤:
1. 编译合约 (Rust → WASM)
2. 上传 WASM 到 MSG Chain
3. 实例化 perp_oracle
4. 实例化 perp_pool
5. 实例化 perp_liquidation
6. 实例化 perp_trading
7. 配置 oracle 价格源
8. 添加池资产
9. 存入流动性
10. 创建交易对
11. 开始交易!

MSG Chain 参数:
  chain-id:    msg-chain-1
  bech32:      msg
  decimals:    18
  native:      umsg / uusd

文档版本: v1.0.0

协议: MSG Chain 永续合约 vAMM 协议

核心技术栈: CosmWasm, Rust, React, TypeScript, CosmJS

参考实现: GMX (Arbitrum), dYdX (StarkNet), Jupiter Perps (Solana)

安全警告: 本指南中的代码仅供学习参考。部署到生产环境前,必须经过专业安全审计。

附录 A: 完整 Cargo.toml 依赖

[package]
name = "perp-exchange"
version = "1.0.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
cosmwasm-std = "1.5"
cosmwasm-schema = "1.5"
cw-storage-plus = "1.2"
cw-utils = "1.0"
cw20 = "1.1"
cw20-base = "1.1"
schemars = "0.8"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
uint = "0.9"

[dev-dependencies]
cosmwasm-vm = "1.5"
cw-multi-test = "1.0"

附录 B: MSG Chain 测试网配置

// msgchain.config.ts

export const MSG_CHAIN_TESTNET = {
  chainId: "msg-chain-1",
  rpc: "https://rpc.msg-chain-1.zone",
  rest: "https://rest.msg-chain-1.zone",
  faucet: "https://faucet.msg-chain-1.zone",
  prefix: "msg",
  denom: "umsg",
  decimals: 18,
  gasPrice: "1000000000attoMSG",
  explorer: "https://explorer.msg-chain-1.zone",
};

export const MSG_CHAIN_MAINNET = {
  chainId: "msg-chain-1",
  rpc: "https://rpc.msg-chain-1.zone",
  rest: "https://rest.msg-chain-1.zone",
  prefix: "msg",
  denom: "umsg",
  decimals: 18,
  gasPrice: "1000000000attoMSG",
  explorer: "https://explorer.msg-chain-1.zone",
};

附录 C: 测试用例

// tests/integration_test.rs

#[cfg(test)]
mod tests {
    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
    use cosmwasm_std::{coins, from_binary, Uint128, Decimal};

    #[test]
    fn test_open_long_position() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        let info = mock_info("trader", &coins(1000, "uusd"));

        // 测试开仓逻辑(需实例化合约后执行)
        // 此处为伪代码,实际需要完整的 multi-test 设置
        assert!(true);
    }

    #[test]
    fn test_liquidation_price_calculation() {
        let size = Uint128::from(100_000u128);
        let collateral = Uint128::from(10_000u128);
        let entry = Decimal::from_ratio(65_000u128, 1u128);
        let mmr = Decimal::percent(10);

        // 10x 多头清算价格
        // liq = 65,000 * (1 - (0.1 - 0.1)) = 58,500
        let lt = entry * (Decimal::one() - Decimal::from_ratio(1u128, 10u128) + mmr);
        // 期望: 58,500
    }

    #[test]
    fn test_vamm_pricing() {
        let pair = TradingPair {
            virtual_base_reserve: Uint128::from(1_000_000u128),
            virtual_quote_reserve: Uint128::from(65_000_000_000u128),
            // ... 其他字段
        };

        // 测试买入 10 BTC 的滑点
        let (price, impact) = get_vamm_price(&pair, Uint128::from(10u128), true);
        assert!(impact < Decimal::percent(1)); // 滑点应小于 1%
    }

    #[test]
    fn test_funding_rate_calculation() {
        let config = FundingConfig::default();
        let rate = calculate_funding_rate(
            Decimal::from_ratio(65_100u128, 1u128),
            Decimal::from_ratio(65_000u128, 1u128),
            &config,
        );
        assert!(rate <= config.max_funding_rate);
    }

    #[test]
    fn test_margin_ratio() {
        let ratio = calculate_margin_ratio(
            Uint128::from(100_000u128),
            Uint128::from(10_000u128),
            Uint128::from(1_000u128),
            &PnlDirection::Loss,
        );
        assert_eq!(ratio, Decimal::from_ratio(9_000u128, 100_000u128));
    }
}

附录 D: 安全 checklist

□ 所有算术运算使用 checked_* 方法,防止溢出
□ 权限控制:owner-only 函数有严格的 sender 检查
□ 预言机价格过期检查(max_age)
□ 滑点保护(min_price / max_price)
□ 虚拟准备金不能耗尽(除零保护)
□ LP Token 铸造/销毁数量计算精度
□ 清算检查保证金率,防止误清算
□ 资金费率计算使用时间加权
□ 断路器在价格异常时自动触发
□ 池价值计算包含未实现盈亏和坏账
□ 多源价格聚合防止单点故障
□ 所有外部调用有错误处理
□ 头寸 ID 递增,防止重放
□ 抵押品提取不能使保证金率低于阈值
□ 部分清算后头寸可继续存活

附录 E: 性能优化建议

1. **批处理清算**: 使用 BatchLiquidate 批量处理多个头寸
2. **资金费率批量结算**: Keeper 同时结算多个交易对
3. **惰性更新**: TWAP 在查询时计算而非实时更新
4. **分页查询**: 头寸/交易记录使用分页避免 gas 超限
5. **索引优化**: 使用 IndexedMap 加速按交易者查询
6. **缓存: 高频查询的池价值可缓存 1 个区块
7. **异步清算**: 使用 keeper 网络监控而非合约内轮询
8. **压缩存储**: 使用更紧凑的数据结构减少存储成本

文档版本: v1.0.0
主网状态: No-Go

本文档基于 MSG Chain 代码库核实的技术事实。
白皮书系统: https://msgchain.org/whitepaper/

协议: MSG Chain Perpetual DEX — vAMM 永续合约协议

作者: MSG Chain 开发团队


附录 F: Gas 估算参考

操作 Gas 消耗 (估计) 备注
deposit 200,000 - 300,000 首次铸造 LP Token
withdraw 250,000 - 400,000 按支持的资产数量增加
open_position 300,000 - 500,000 含预言机查询和清算注册
close_position 250,000 - 400,000 含盈亏计算
add_collateral 100,000 - 150,000 简单金额更新
remove_collateral 120,000 - 180,000 含保证金检查
liquidate 300,000 - 450,000 含查询+跨合约调用
settle_funding 100,000 + 50,000/头寸 随头寸数线性增加
submit_price 80,000 - 120,000 含 TWAP 更新
query 5,000 - 50,000 只读调用

附录 G: 关键字索引

关键字 章节 说明
vAMM 5 虚拟自动做市商定价模型
资金费率 6 多空双方定期支付/收取的费用
清算 7 保证金不足时强制平仓
TWAP 8 时间加权平均价格
LP Token 3 流动性提供者凭证 (msGLP)
Mark Price 1.3 标记价格 (oracle + vAMM 均值)
Open Interest 4 未平仓量
维持保证金 7 触发清算的阈值 (10%)
断路器 8 价格异常保护机制
slippage 5 滑点: 大额交易造成的价格偏离

附录 H: 常见问题 (FAQ)

Q: 为什么使用 vAMM 而不是订单簿?

A: vAMM 实现更简单,流动性由单一 LP 池提供,无需做市商。适合 MSG Chain 早期生态建设。

Q: LP 如何获利?

A: LP 收取交易手续费(开仓 0.1% + 平仓 0.1%),减去协议性能费后按 LP Token 占比分配。LP 也承担交易者盈利时的支出风险。

Q: 最大杠杆是多少?

A: 默认最大 50 倍,每个交易对可单独配置。

Q: 资金费率多久结算一次?

A: 理论间隔 8 小时,但可由 keeper 在任何时候调用 settle_funding 进行结算。

Q: 如何防止价格操纵?

A: 三层防护:(1) 预言机 TWAP 价格 (2) vAMM 独立定价 (3) 执行价格取两者加权平均。

Q: 支持哪些抵押品?

A: 可通过 add_asset 添加任意 CW20 或 native 资产,每个资产有独立的权重和占比限制。

Q: 清算奖励如何分配?

A: 清算人获得清算头寸价值的 5% 作为奖励,剩余返还给被清算交易者。

Q: 如何升级合约?

A: 使用 CosmWasm 的 migrate 功能。关键配置通过 UpdateConfig 消息修改,无需重新部署。


文档版本: v1.0.0
主网状态: No-Go

本文档基于 MSG Chain 代码库核实的技术事实。
白皮书系统: https://msgchain.org/whitepaper/

协议: MSG Chain Perpetual DEX — vAMM 永续合约协议实现指南

技术栈: CosmWasm 1.5, Rust 2021, React 18, TypeScript 5, CosmJS 0.32

链: MSG Chain (msg-chain-1, bech32: msg, 18 decimals)

架构: 5 合约模块 (factory, pool, trading, oracle, liquidation)