MSG Chain 永续合约协议实现指南
本指南详细讲解如何在 MSG Chain (msg-chain-1) 上构建完整的 vAMM 永续合约去中心化交易所。
链参数:chain-id =
msg-chain-1| bech32 前缀 =msg| 精度 = 18 位小数目标读者:Rust/CosmWasm 开发者、DeFi 协议工程师、区块链研究员
目录
1. 概述
1.1 什么是永续合约
永续合约(Perpetual Futures / Perps)是一种没有到期日的衍生品合约,交易者可以持有多头(做多)或空头(做空)头寸,通过资金费率机制锚定现货价格。截至 2026 年,永续合约日均交易量超过 3000 亿美元,是 DeFi 中最大的交易品类。
核心特征:
- 无到期日:与传统期货不同,永续合约永远不会到期结算
- 资金费率:每 8 小时(或更短)多空双方之间支付的费用,用于锚定现货价格
- 杠杆:交易者可以用少量抵押品控制更大价值的头寸
- 清算机制:当保证金比率低于阈值时,头寸被强制平仓
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 |
清算执行、罚没分配 |
设计原则:
- 模块化:每个合约职责单一,可单独升级
- 安全优先:所有关键函数有多重检查
- LP 保护:池价值计算包含未实现盈亏和坏账
- 抗操纵:使用 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, "e, &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)
