dApp Docs/收益聚合器(Yield Aggregator)实现
Development reference. Not independently verified for production.

收益聚合器 (Yield Aggregator) 实现指南

MSG Chain 自动复投金库完全实现

主网状态: No-Go


1. 概述

1.1 什么是收益聚合器

收益聚合器(Yield Aggregator)是一种自动化资产管理协议,它自动将用户的资产部署到最优收益策略中,并通过自动复投机制持续 compounding 收益。Yearn Finance 是这一领域的先驱,其 Vault 模型已成为 DeFi 收益聚合的标准架构。

收益聚合器的核心价值在于:

  1. 自动化复投 — 手动领取收益并 reinvest 会产生 Gas 成本和操作复杂度,聚合器自动完成此过程
  2. 策略优化 — 自动将资金分配到最高收益的协议中
  3. Gas 聚合 — 多个用户的收益操作合并执行,分摊 Gas 成本
  4. 降低门槛 — 用户只需存入资产,无需自行管理策略

1.2 核心概念

Vault(金库) — 用户存入资金的核心合约。用户向 Vault 存入基础资产(want token),获得代表其份额的 Vault 份额代币。Vault 负责:

Strategy(策略) — 具体部署资金到外部协议的模块。每个 Strategy 负责:

want token(基础资产) — Vault 接受和管理的底层代币,通常是 LP 代币或生息代币

share token(份额代币) — 代表用户在 Vault 中资金份额的代币,其价值 = 总资产 / 总供应量

1.3 自动复投流程

                    +-----------------------+
                    |      用户 0xABC...     |
                    |  Deposit LP Token     |
                    +-------+---------------+
                            |
                            v
                    +-----------------------+
                    |   Vault 合约           |
                    |   - 铸造份额代币        |
                    |   - 分配资金到策略      |
                    +-------+---------------+
                            |
                    +-------v---------------+
                    |   Strategy 合约         |
                    |   - 存入 AMM 农场       |
                    |   - 定期 Harvest        |
                    |   - 领取奖励 -> Swap -> |
                    |     Re-deposit         |
                    +------------------------+

Harvest 流程:

1. 调用 Strategy.harvest()
2. 从农场领取奖励代币(如 MSG、USDC 等)
3. 通过 DEX Swap 将奖励兑换为 want token(LP Token)
4. 将兑换后的 want token 再存入策略
5. 扣除 Performance Fee -> 发送到 Treasury
6. Vault 总资产增加 -> 份额价格上升

1.4 MSG Chain 特点

MSG Chain 是基于 Cosmos SDK 构建的 EVM 兼容链 + CosmWasm 智能合约平台。

关键参数:

对 Yield Aggregator 的意义:

1.5 架构总览

msg1q...user                    msg1p...treasury
    |                               |
    v                               v
+--------------------------------------------------+
|              Vault 合约 (Vault.rs)                 |
|  +----------+-----------+----------+----------+  |
|  | 存款管理  | 取款管理   | 费用收取  | 策略管理  |  |
|  +----------+-----------+----------+----------+  |
|  want_token: LP_TOKEN_ADDR                        |
|  total_supply: Uint128 (份额总供应量)              |
|  performance_fee: 0.20 (20%)                     |
+------------------+-------------------------------+
                   |
    +--------------+--------------+
    |              |              |
    v              v              v
+---------+  +---------+  +---------+
|Strategy |  |Strategy |  |Strategy |
|  AMM LP |  |Lending  |  | Custom  |
| Farm #1 |  |  Pool   |  |   ...   |
+---------+  +---------+  +---------+
    |              |              |
    v              v              v
+------------------------------------------+
|       外部协议 (MSG Chain DeFi)           |
| AMM DEX | Lending | Staking | ...        |
+------------------------------------------+

2. Vault 合约实现

2.1 依赖配置 (Cargo.toml)

[package]
name = "yield-vault"
version = "1.0.0"
edition = "2021"

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

[features]
default = ["library"]
library = []

[dependencies]
cosmwasm-std = { version = "1.5", features = ["staking"] }
cosmwasm-storage = "1.5"
cosmwasm-schema = "1.5"
cw-storage-plus = "1.2"
cw-utils = "1.0"
cw2 = "1.1"
cw20 = "1.1"
cw20-base = "1.1"
cw-multi-test = "1.2"
schemars = "0.8"
serde = { version = "1.0", default-features = false, features = ["derive"] }
thiserror = "1.0"
uint = "0.9"
rust_decimal = "1.33"

[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
overflow-checks = true
codegen-units = 1

2.2 合约状态定义 (state.rs)

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

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct VaultConfig {
    pub name: String,
    pub version: String,
    pub want_token: Addr,
    pub reward_tokens: Vec<Addr>,
    pub strategies: Vec<Addr>,
    pub total_balance: Uint128,
    pub total_supply: Uint128,
    pub performance_fee: Decimal,
    pub withdrawal_fee: Decimal,
    pub management_fee: Decimal,
    pub treasury: Addr,
    pub owner: Addr,
    pub paused: bool,
    pub share_token: Option<Addr>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct FeeRecord {
    pub timestamp: u64,
    pub perf_fee_amount: Uint128,
    pub mgmt_fee_amount: Uint128,
    pub withdrawal_fee_amount: Uint128,
    pub total_assets_before: Uint128,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct UserInfo {
    pub shares: Uint128,
    pub last_interaction: u64,
    pub accrued_rewards: Uint128,
    pub deposit_time: u64,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PricePerShareSnapshot {
    pub timestamp: u64,
    pub price_per_share: Uint128,
    pub total_balance: Uint128,
    pub total_supply: Uint128,
}

pub const CONFIG: Item<VaultConfig> = Item::new("vault_config");
pub const USER_INFO: Map<&Addr, UserInfo> = Map::new("user_info");
pub const FEE_HISTORY: Map<u64, FeeRecord> = Map::new("fee_history");
pub const PPS_HISTORY: Map<u64, PricePerShareSnapshot> = Map::new("pps_history");
pub const FEE_WHITELIST: Map<&Addr, bool> = Map::new("fee_whitelist");
pub const STRATEGY_WEIGHTS: Map<&Addr, u16> = Map::new("strategy_weights");
pub const USER_COUNT: Item<u64> = Item::new("user_count");
pub const PAUSE_STATE: Item<PauseState> = Item::new("pause_state");

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum PauseState {
    Running,
    Paused,
    EmergencyLocked,
}

2.3 消息定义 (msg.rs)

use cosmwasm_std::{Decimal, Uint128};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
    pub name: String,
    pub version: String,
    pub want_token: String,
    pub reward_tokens: Vec<String>,
    pub performance_fee: Decimal,
    pub withdrawal_fee: Decimal,
    pub management_fee: Decimal,
    pub treasury: String,
    pub owner: String,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    Deposit { amount: Uint128, recipient: Option<String> },
    Withdraw { shares: Uint128 },
    WithdrawAll {},
    Harvest {},
    AddStrategy { strategy: String, weight: Option<u16> },
    RemoveStrategy { strategy: String },
    MigrateStrategy { old_strategy: String, new_strategy: String },
    UpdateFees { performance_fee: Option<Decimal>, withdrawal_fee: Option<Decimal>, management_fee: Option<Decimal> },
    UpdateTreasury { treasury: String },
    TransferOwnership { new_owner: String },
    Pause {},
    Unpause {},
    EmergencyWithdraw { amount: Uint128, recipient: String },
    SetFeeWhitelist { address: String, whitelisted: bool },
    SetStrategyWeights { weights: Vec<(String, u16)> },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    GetConfig {},
    GetUserInfo { address: String },
    GetPricePerShare {},
    GetTvl {},
    GetApr {},
    GetStrategies {},
    SimulateDeposit { amount: Uint128 },
    SimulateWithdraw { shares: Uint128 },
    GetPpsHistory { from: u64, to: Option<u64> },
    GetFeeHistory { from: u64, to: Option<u64> },
    EstimateHarvestGas {},
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ConfigResponse {
    pub name: String,
    pub version: String,
    pub want_token: String,
    pub reward_tokens: Vec<String>,
    pub strategies: Vec<String>,
    pub total_balance: Uint128,
    pub total_supply: Uint128,
    pub performance_fee: Decimal,
    pub withdrawal_fee: Decimal,
    pub management_fee: Decimal,
    pub treasury: String,
    pub owner: String,
    pub paused: bool,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct UserInfoResponse {
    pub address: String,
    pub shares: Uint128,
    pub want_token_balance: Uint128,
    pub share_value: Uint128,
    pub unrealized_gains: Uint128,
    pub last_interaction: u64,
    pub deposit_time: u64,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PricePerShareResponse {
    pub price_per_share: Uint128,
    pub total_balance: Uint128,
    pub total_supply: Uint128,
    pub decimals: u8,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct TvlResponse {
    pub tvl: Uint128,
    pub tvl_usd: Option<String>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct AprResponse {
    pub apr_7d: Decimal,
    pub apr_30d: Decimal,
    pub harvest_apr: Decimal,
    pub net_apr: Decimal,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct SimulationResponse {
    pub shares_received: Uint128,
    pub fee_charged: Uint128,
    pub net_amount: Uint128,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum StrategyExecuteMsg {
    Deposit { amount: Uint128 },
    Withdraw { amount: Uint128, recipient: String },
    Harvest {},
    Migrate { new_strategy: String },
    Pause {},
    Unpause {},
    UpdateConfig { treasury: Option<String> },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum StrategyQueryMsg {
    BalanceOf {},
    WantToken {},
    RewardTokens {},
    IsActive {},
    Health {},
    EstimatedApr {},
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct EstimateHarvestGasResponse {
    pub estimated_gas: u64,
}

2.4 错误类型 (error.rs)

use cosmwasm_std::StdError;
use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
    #[error("{0}")]
    Std(#[from] StdError),
    #[error("合约已暂停: {reason}")]
    Paused { reason: String },
    #[error("未授权: {msg}")]
    Unauthorized { msg: String },
    #[error("无效参数: {msg}")]
    InvalidArgument { msg: String },
    #[error("资金不足: 需要 {required}, 有 {available}")]
    InsufficientFunds { required: Uint128, available: Uint128 },
    #[error("份额计算错误: {msg}")]
    ShareCalculationError { msg: String },
    #[error("策略错误: {msg}")]
    StrategyError { msg: String },
    #[error("策略已存在: {addr}")]
    StrategyAlreadyExists { addr: Addr },
    #[error("策略不存在: {addr}")]
    StrategyNotFound { addr: Addr },
    #[error("滑点保护: 预期至少 {expected}, 实际 {actual}")]
    SlippageProtection { expected: Uint128, actual: Uint128 },
    #[error("零金额操作不被允许")]
    ZeroAmount {},
    #[error("算术溢出")]
    Overflow {},
    #[error("复投频率过高, 请等待")]
    HarvestTooSoon {},
    #[error("迁移进行中, 请稍后")]
    MigrationInProgress {},
    #[error("总供应量为零")]
    ZeroSupply {},
    #[error("重复的用户")]
    DuplicateUser {},
}

2.5 合约主逻辑 (contract.rs) (续)

fn distribute_funds_to_strategies(
    deps: Deps,
    _env: &Env,
    amount: Uint128,
) -> Result<Vec<CosmosMsg>, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    let mut msgs = vec![];
    if config.strategies.is_empty() || amount.is_zero() { return Ok(msgs); }
    if config.strategies.len() == 1 {
        msgs.push(CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: config.strategies[0].to_string(),
            msg: to_json_binary(&StrategyExecuteMsg::Deposit { amount })?,
            funds: vec![],
        }));
        return Ok(msgs);
    }
    let mut total_weight: u128 = 0;
    let mut weights_map = std::collections::HashMap::new();
    for strategy in &config.strategies {
        let w = STRATEGY_WEIGHTS.may_load(deps.storage, strategy)?.unwrap_or(100) as u128;
        total_weight += w;
        weights_map.insert(strategy.clone(), w);
    }
    if total_weight == 0 { return Ok(msgs); }
    let amount_u128: u128 = amount.into();
    let mut allocated: u128 = 0;
    for (i, strategy) in config.strategies.iter().enumerate() {
        let w = weights_map.get(strategy).copied().unwrap_or(100);
        let sa = if i == config.strategies.len() - 1 { amount_u128.saturating_sub(allocated) } else { (amount_u128 * w) / total_weight };
        if sa == 0 { continue; }
        allocated += sa;
        msgs.push(CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: strategy.to_string(),
            msg: to_json_binary(&StrategyExecuteMsg::Deposit { amount: Uint128::from(sa) })?,
            funds: vec![],
        }));
    }
    Ok(msgs)
}

fn withdraw_funds_from_strategies(
    deps: Deps,
    _env: &Env,
    amount: Uint128,
) -> Result<Vec<CosmosMsg>, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    let mut msgs = vec![];
    if config.strategies.is_empty() || amount.is_zero() { return Ok(msgs); }
    let total_balance = config.total_balance;
    if total_balance.is_zero() { return Ok(msgs); }
    for strategy_addr in &config.strategies {
        let strategy_balance: Uint128 = match deps.querier.query_wasm_smart(strategy_addr, &StrategyQueryMsg::BalanceOf {}) {
            Ok(b) => b, Err(_) => continue,
        };
        if strategy_balance.is_zero() { continue; }
        let withdraw_amount = amount.multiply_ratio(strategy_balance, total_balance);
        if withdraw_amount.is_zero() { continue; }
        msgs.push(CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: strategy_addr.to_string(),
            msg: to_json_binary(&StrategyExecuteMsg::Withdraw { amount: withdraw_amount, recipient: _env.contract.address.to_string() })?,
            funds: vec![],
        }));
    }
    Ok(msgs)
}

fn calculate_shares_for_amount(total_supply: Uint128, total_balance: Uint128, amount: Uint128) -> Result<Uint128, ContractError> {
    if total_supply.is_zero() || total_balance.is_zero() { return Ok(amount); }
    let shares = amount.full_mul(total_supply).checked_div(total_balance.into()).map_err(|_| ContractError::Overflow {})?;
    Uint128::try_from(shares).map_err(|_| ContractError::Overflow {})
}

fn calculate_amount_for_shares(total_supply: Uint128, total_balance: Uint128, shares: Uint128) -> Result<Uint128, ContractError> {
    if total_supply.is_zero() { return Err(ContractError::ZeroSupply {}); }
    let amount = shares.full_mul(total_balance).checked_div(total_supply.into()).map_err(|_| ContractError::Overflow {})?;
    Uint128::try_from(amount).map_err(|_| ContractError::Overflow {})
}

fn calculate_price_per_share(total_balance: Uint128, total_supply: Uint128) -> Result<Uint128, ContractError> {
    if total_supply.is_zero() { return Ok(DENOMINATOR); }
    let pps = total_balance.full_mul(DENOMINATOR).checked_div(total_supply.into()).map_err(|_| ContractError::Overflow {})?;
    Uint128::try_from(pps).map_err(|_| ContractError::Overflow {})
}

fn calculate_management_fee(total_balance: Uint128, management_fee: Decimal, last_charge: u64, now: u64) -> Uint128 {
    if total_balance.is_zero() || management_fee.is_zero() { return Uint128::zero(); }
    let elapsed = now.saturating_sub(last_charge);
    if elapsed == 0 { return Uint128::zero(); }
    let annual_fee = total_balance * management_fee;
    annual_fee.multiply_ratio(elapsed, ONE_YEAR_SECONDS)
}

fn calculate_dynamic_withdrawal_fee(amount: Uint128, deposit_time: u64, current_time: u64, base_fee: Decimal) -> Uint128 {
    let holding_period = current_time.saturating_sub(deposit_time);
    const GRACE_PERIOD: u64 = 3 * 24 * 60 * 60;
    let effective_fee = if holding_period >= GRACE_PERIOD { Decimal::zero() } else {
        let remaining = Decimal::from_ratio(GRACE_PERIOD - holding_period, GRACE_PERIOD);
        base_fee * remaining
    };
    amount * effective_fee
}

fn record_pps_snapshot(deps: DepsMut, env: &Env, config: &VaultConfig) -> Result<(), ContractError> {
    let pps = calculate_price_per_share(config.total_balance, config.total_supply)?;
    let snapshot = PricePerShareSnapshot { timestamp: env.block.time.seconds(), price_per_share: pps, total_balance: config.total_balance, total_supply: config.total_supply };
    PPS_HISTORY.save(deps.storage, &env.block.height, &snapshot)?;
    Ok(())
}

fn assert_owner(config: &VaultConfig, info: &MessageInfo) -> Result<(), ContractError> {
    if info.sender != config.owner { return Err(ContractError::Unauthorized { msg: "只有 owner 可以执行此操作".to_string() }); }
    Ok(())
}

fn assert_not_paused(config: &VaultConfig) -> Result<(), ContractError> {
    if config.paused { return Err(ContractError::Paused { reason: "Vault 已暂停".to_string() }); }
    Ok(())
}

#[entry_point]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::GetConfig {} => to_json_binary(&query_config(deps)?),
        QueryMsg::GetUserInfo { address } => to_json_binary(&query_user_info(deps, address)?),
        QueryMsg::GetPricePerShare {} => to_json_binary(&query_price_per_share(deps)?),
        QueryMsg::GetTvl {} => to_json_binary(&query_tvl(deps)?),
        QueryMsg::GetApr {} => to_json_binary(&query_apr(deps, env)?),
        QueryMsg::GetStrategies {} => to_json_binary(&query_strategies(deps)?),
        QueryMsg::SimulateDeposit { amount } => to_json_binary(&simulate_deposit(deps, amount)?),
        QueryMsg::SimulateWithdraw { shares } => to_json_binary(&simulate_withdraw(deps, shares)?),
        QueryMsg::GetPpsHistory { from, to } => to_json_binary(&query_pps_history(deps, from, to)?),
        QueryMsg::GetFeeHistory { from, to } => to_json_binary(&query_fee_history(deps, from, to)?),
        QueryMsg::EstimateHarvestGas {} => to_json_binary(&EstimateHarvestGasResponse { estimated_gas: 500_000u64 }),
    }
}

fn query_config(deps: Deps) -> StdResult<ConfigResponse> {
    let config = CONFIG.load(deps.storage)?;
    Ok(ConfigResponse {
        name: config.name, version: config.version,
        want_token: config.want_token.to_string(),
        reward_tokens: config.reward_tokens.iter().map(|a| a.to_string()).collect(),
        strategies: config.strategies.iter().map(|a| a.to_string()).collect(),
        total_balance: config.total_balance, total_supply: config.total_supply,
        performance_fee: config.performance_fee, withdrawal_fee: config.withdrawal_fee,
        management_fee: config.management_fee,
        treasury: config.treasury.to_string(), owner: config.owner.to_string(), paused: config.paused,
    })
}

fn query_user_info(deps: Deps, address: String) -> StdResult<UserInfoResponse> {
    let addr = deps.api.addr_validate(&address)?;
    let config = CONFIG.load(deps.storage)?;
    let user = USER_INFO.may_load(deps.storage, &addr)?.unwrap_or(UserInfo {
        shares: Uint128::zero(), last_interaction: 0, accrued_rewards: Uint128::zero(), deposit_time: 0,
    });
    let share_value = if config.total_supply.is_zero() { Uint128::zero() } else {
        calculate_amount_for_shares(config.total_supply, config.total_balance, user.shares).unwrap_or(Uint128::zero())
    };
    Ok(UserInfoResponse {
        address: addr.to_string(), shares: user.shares,
        want_token_balance: share_value, share_value,
        unrealized_gains: share_value.saturating_sub(user.shares),
        last_interaction: user.last_interaction, deposit_time: user.deposit_time,
    })
}

fn query_price_per_share(deps: Deps) -> StdResult<PricePerShareResponse> {
    let config = CONFIG.load(deps.storage)?;
    let pps = calculate_price_per_share(config.total_balance, config.total_supply).unwrap_or(DENOMINATOR);
    Ok(PricePerShareResponse { price_per_share: pps, total_balance: config.total_balance, total_supply: config.total_supply, decimals: SHARE_DECIMALS })
}

fn query_tvl(deps: Deps) -> StdResult<TvlResponse> {
    let config = CONFIG.load(deps.storage)?;
    let mut total = config.total_balance;
    for strategy_addr in &config.strategies {
        if let Ok(balance) = deps.querier.query_wasm_smart::<Uint128>(strategy_addr, &StrategyQueryMsg::BalanceOf {}) {
            total += balance;
        }
    }
    Ok(TvlResponse { tvl: total, tvl_usd: None })
}

fn query_apr(deps: Deps, _env: Env) -> StdResult<AprResponse> {
    let config = CONFIG.load(deps.storage)?;
    let now = _env.block.time.seconds();
    let since = now - 30 * 24 * 60 * 60;
    let mut earliest_pps = None;
    let mut latest_pps = None;
    for item in PPS_HISTORY.range(deps.storage, None, None, Order::Ascending) {
        let (_, snapshot) = item?;
        if snapshot.timestamp >= since && earliest_pps.is_none() { earliest_pps = Some(snapshot.price_per_share); }
        latest_pps = Some(snapshot.price_per_share);
    }
    let apr_30d = match (earliest_pps, latest_pps) {
        (Some(old), Some(new)) if old > Uint128::zero() => {
            let growth = Decimal::from_ratio(new - old, old);
            growth * Decimal::percent(100) * Decimal::from_ratio(365u64, 30u64)
        }
        _ => Decimal::zero(),
    };
    let total_fees = config.performance_fee + config.management_fee;
    Ok(AprResponse { apr_7d: apr_30d, apr_30d, harvest_apr: apr_30d, net_apr: apr_30d * (Decimal::one() - total_fees) })
}

fn query_strategies(deps: Deps) -> StdResult<Vec<String>> {
    let config = CONFIG.load(deps.storage)?;
    Ok(config.strategies.iter().map(|a| a.to_string()).collect())
}

fn simulate_deposit(deps: Deps, amount: Uint128) -> StdResult<SimulationResponse> {
    let config = CONFIG.load(deps.storage)?;
    let shares = calculate_shares_for_amount(config.total_supply, config.total_balance, amount).unwrap_or(Uint128::zero());
    Ok(SimulationResponse { shares_received: shares, fee_charged: Uint128::zero(), net_amount: amount })
}

fn simulate_withdraw(deps: Deps, shares: Uint128) -> StdResult<SimulationResponse> {
    let config = CONFIG.load(deps.storage)?;
    let gross = calculate_amount_for_shares(config.total_supply, config.total_balance, shares).unwrap_or(Uint128::zero());
    let fee = gross * config.withdrawal_fee;
    let net = gross.saturating_sub(fee);
    Ok(SimulationResponse { shares_received: net, fee_charged: fee, net_amount: net })
}

fn query_pps_history(deps: Deps, from: u64, to: Option<u64>) -> StdResult<Vec<PricePerShareSnapshot>> {
    let to = to.unwrap_or(u64::MAX);
    let mut history = vec![];
    for item in PPS_HISTORY.range(deps.storage, Some(from.into()), Some(to.into()), Order::Ascending) {
        let (_, snapshot) = item?;
        history.push(snapshot);
    }
    Ok(history)
}

fn query_fee_history(deps: Deps, from: u64, to: Option<u64>) -> StdResult<Vec<FeeRecord>> {
    let to = to.unwrap_or(u64::MAX);
    let mut records = vec![];
    for item in FEE_HISTORY.range(deps.storage, Some(from.into()), Some(to.into()), Order::Ascending) {
        let (_, record) = item?;
        records.push(record);
    }
    Ok(records)
}

2.6 合约文件清单

contracts/yield-vault/
├── Cargo.toml
├── src/
│   ├── contract.rs     # 主入口: instantiate, execute, query
│   ├── error.rs        # 错误类型
│   ├── msg.rs          # 消息结构
│   ├── state.rs        # 状态存储
│   └── lib.rs          # 模块声明
└── examples/
    └── integration.rs  # 集成测试

2.7 lib.rs

pub mod contract;
pub mod error;
pub mod msg;
pub mod state;

2.8 集成测试

#[cfg(test)]
mod tests {
    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
    use cosmwasm_std::{Decimal, Uint128};
    use crate::contract::{execute, instantiate, query};
    use crate::msg::*;

    const WANT_TOKEN: &str = "msg1wanttoken";
    const TREASURY: &str = "msg1treasury";
    const OWNER: &str = "msg1owner";

    #[test]
    fn test_instantiate() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        let msg = InstantiateMsg {
            name: "MSG-USDT LP Vault".to_string(),
            version: "1.0.0".to_string(),
            want_token: WANT_TOKEN.to_string(),
            reward_tokens: vec!["msg1reward".to_string()],
            performance_fee: Decimal::percent(20),
            withdrawal_fee: Decimal::percent(1),
            management_fee: Decimal::percent(2),
            treasury: TREASURY.to_string(),
            owner: OWNER.to_string(),
        };
        let info = mock_info(OWNER, &[]);
        let res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap();
        assert_eq!(res.attributes[0].value, "instantiate");
        let config: ConfigResponse = query(deps.as_ref(), env.clone(), QueryMsg::GetConfig {}).unwrap();
        assert_eq!(config.name, "MSG-USDT LP Vault");
    }

    #[test]
    fn test_price_per_share_initial() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        let init_msg = InstantiateMsg {
            name: "Test Vault".to_string(),
            version: "1.0.0".to_string(),
            want_token: WANT_TOKEN.to_string(),
            reward_tokens: vec![],
            performance_fee: Decimal::percent(20),
            withdrawal_fee: Decimal::percent(0),
            management_fee: Decimal::zero(),
            treasury: TREASURY.to_string(),
            owner: OWNER.to_string(),
        };
        let info = mock_info(OWNER, &[]);
        instantiate(deps.as_mut(), env.clone(), info, init_msg).unwrap();
        let pps: PricePerShareResponse = query(deps.as_ref(), env.clone(), QueryMsg::GetPricePerShare {}).unwrap();
        assert_eq!(pps.price_per_share, Uint128::new(1_000_000_000_000_000_000u128));
    }

    #[test]
    fn test_shares_calculation() {
        let shares = crate::contract::calculate_shares_for_amount(
            Uint128::new(1000), Uint128::new(2000), Uint128::new(100),
        ).unwrap();
        assert_eq!(shares, Uint128::new(50));
    }
}

3. 策略合约实现

3.1 Strategy Traits

use cosmwasm_std::{Addr, Response, Uint128};

pub trait Strategy {
    fn deposit(&self, amount: Uint128) -> Result<Response, StrategyError>;
    fn withdraw(&self, amount: Uint128, recipient: &Addr) -> Result<Response, StrategyError>;
    fn harvest(&self) -> Result<Response, StrategyError>;
    fn balance_of(&self) -> Result<Uint128, StrategyError>;
    fn health_check(&self) -> Result<HealthStatus, StrategyError>;
}

#[derive(Debug, PartialEq)]
pub enum HealthStatus { Healthy, Degraded { reason: String }, Unhealthy { reason: String } }

#[derive(Debug)]
pub enum StrategyError { NotReady, InsufficientBalance, ExternalProtocolError(String), SlippageError, Paused, Custom(String) }

3.2 AMM 农场策略完整实现

use cosmwasm_std::{
    entry_point, to_json_binary, CosmosMsg, Decimal, Deps, DepsMut,
    Env, MessageInfo, Response, StdError, StdResult, Uint128, WasmMsg,
};
use cw20::{Cw20ExecuteMsg, Cw20QueryMsg, BalanceResponse};
use cw_storage_plus::Item;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
pub enum StrategyError {
    #[error("{0}")] Std(#[from] StdError),
    #[error("未授权: {msg}")] Unauthorized { msg: String },
    #[error("策略已暂停")] Paused,
    #[error("外部协议错误: {msg}")] ExternalProtocolError { msg: String },
    #[error("余额不足: 需要 {required}, 有 {available}")] InsufficientBalance { required: Uint128, available: Uint128 },
    #[error("无效配置: {msg}")] InvalidConfig { msg: String },
    #[error("复投失败: {msg}")] HarvestFailed { msg: String },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct StrategyConfig {
    pub vault: Addr, pub want_token: Addr, pub farm_contract: Addr,
    pub reward_tokens: Vec<Addr>, pub router: Addr, pub swap_route: Vec<Addr>,
    pub slippage_tolerance: Decimal, pub performance_fee: Decimal,
    pub treasury: Addr, pub owner: Addr, pub paused: bool,
    pub min_harvest_interval: u64, pub last_harvest: u64,
}

pub const CONFIG: Item<StrategyConfig> = Item::new("config");

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
    pub vault: String, pub want_token: String, pub farm_contract: String,
    pub reward_tokens: Vec<String>, pub router: String, pub swap_route: Vec<String>,
    pub slippage_tolerance: Decimal, pub performance_fee: Decimal,
    pub treasury: String, pub owner: String, pub min_harvest_interval: u64,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    Deposit { amount: Uint128 }, Withdraw { amount: Uint128, recipient: String },
    Harvest {}, Pause {}, Unpause {},
    UpdateConfig { slippage_tolerance: Option<Decimal>, treasury: Option<String> },
    EmergencyWithdraw { recipient: String },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    BalanceOf {}, WantToken {}, FarmContract {}, RewardTokens {}, IsActive {}, Health {}, EstimatedApr {}, Config {},
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct HealthResponse {
    pub status: String, pub details: String, pub farm_balance: Uint128,
    pub idle_balance: Uint128, pub last_harvest: u64,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct EstimatedAprResponse {
    pub gross_apr: Decimal, pub net_apr: Decimal, pub performance_fee: Decimal,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum FarmExecuteMsg { Stake { amount: Uint128 }, Unstake { amount: Uint128 }, ClaimRewards {} }

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum FarmQueryMsg { StakedBalance { address: String }, TotalStaked {}, RewardRate {} }

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum RouterExecuteMsg { SwapExactIn { amount_in: Uint128, route: Vec<Addr>, min_amount_out: Uint128 } }

#[entry_point]
pub fn instantiate(deps: DepsMut, _env: Env, _info: MessageInfo, msg: InstantiateMsg) -> StdResult<Response> {
    let config = StrategyConfig {
        vault: deps.api.addr_validate(&msg.vault)?,
        want_token: deps.api.addr_validate(&msg.want_token)?,
        farm_contract: deps.api.addr_validate(&msg.farm_contract)?,
        reward_tokens: msg.reward_tokens.iter().map(|a| deps.api.addr_validate(a)).collect::<StdResult<Vec<Addr>>>()?,
        router: deps.api.addr_validate(&msg.router)?,
        swap_route: msg.swap_route.iter().map(|a| deps.api.addr_validate(a)).collect::<StdResult<Vec<Addr>>>()?,
        slippage_tolerance: msg.slippage_tolerance, performance_fee: msg.performance_fee,
        treasury: deps.api.addr_validate(&msg.treasury)?,
        owner: deps.api.addr_validate(&msg.owner)?,
        paused: false, min_harvest_interval: msg.min_harvest_interval, last_harvest: 0,
    };
    CONFIG.save(deps.storage, &config)?;
    Ok(Response::new().add_attribute("action", "instantiate").add_attribute("vault", config.vault.to_string()))
}

#[entry_point]
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> Result<Response, StrategyError> {
    match msg {
        ExecuteMsg::Deposit { amount } => exec_deposit(deps, env, info, amount),
        ExecuteMsg::Withdraw { amount, recipient } => exec_withdraw(deps, env, info, amount, recipient),
        ExecuteMsg::Harvest {} => exec_harvest(deps, env, info),
        ExecuteMsg::Pause {} => exec_pause(deps, env, info),
        ExecuteMsg::Unpause {} => exec_unpause(deps, env, info),
        ExecuteMsg::UpdateConfig { slippage_tolerance, treasury } => exec_update_config(deps, env, info, slippage_tolerance, treasury),
        ExecuteMsg::EmergencyWithdraw { recipient } => exec_emergency_withdraw(deps, env, info, recipient),
    }
}

pub fn exec_deposit(deps: DepsMut, _env: Env, info: MessageInfo, amount: Uint128) -> Result<Response, StrategyError> {
    let config = CONFIG.load(deps.storage)?;
    if info.sender != config.vault { return Err(StrategyError::Unauthorized { msg: "only vault".to_string() }); }
    if config.paused { return Err(StrategyError::Paused); }
    if amount.is_zero() { return Err(StrategyError::InvalidConfig { msg: "zero amount".to_string() }); }
    Ok(Response::new()
        .add_message(CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: config.want_token.to_string(),
            msg: to_json_binary(&Cw20ExecuteMsg::IncreaseAllowance { spender: config.farm_contract.to_string(), amount, expires: None })?,
            funds: vec![],
        }))
        .add_message(CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: config.farm_contract.to_string(),
            msg: to_json_binary(&FarmExecuteMsg::Stake { amount })?,
            funds: vec![],
        }))
        .add_attribute("action", "deposit").add_attribute("amount", amount.to_string()))
}

pub fn exec_withdraw(deps: DepsMut, env: Env, info: MessageInfo, amount: Uint128, recipient: String) -> Result<Response, StrategyError> {
    let config = CONFIG.load(deps.storage)?;
    if info.sender != config.vault { return Err(StrategyError::Unauthorized { msg: "only vault".to_string() }); }
    let recipient_addr = deps.api.addr_validate(&recipient)?;
    let farm_balance: BalanceResponse = deps.querier.query_wasm_smart(&config.farm_contract, &FarmQueryMsg::StakedBalance { address: env.contract.address.to_string() })?;
    if amount > farm_balance.balance { return Err(StrategyError::InsufficientBalance { required: amount, available: farm_balance.balance }); }
    Ok(Response::new()
        .add_message(CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: config.farm_contract.to_string(),
            msg: to_json_binary(&FarmExecuteMsg::Unstake { amount })?,
            funds: vec![],
        }))
        .add_message(CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: config.want_token.to_string(),
            msg: to_json_binary(&Cw20ExecuteMsg::Transfer { recipient: recipient_addr.to_string(), amount })?,
            funds: vec![],
        }))
        .add_attribute("action", "withdraw").add_attribute("amount", amount.to_string()))
}

pub fn exec_harvest(deps: DepsMut, env: Env, info: MessageInfo) -> Result<Response, StrategyError> {
    let config = CONFIG.load(deps.storage)?;
    if info.sender != config.vault { return Err(StrategyError::Unauthorized { msg: "only vault".to_string() }); }
    if env.block.time.seconds() - config.last_harvest < config.min_harvest_interval {
        return Err(StrategyError::HarvestFailed { msg: "interval too short".to_string() });
    }
    let mut msgs: Vec<CosmosMsg> = vec![];
    msgs.push(CosmosMsg::Wasm(WasmMsg::Execute {
        contract_addr: config.farm_contract.to_string(),
        msg: to_json_binary(&FarmExecuteMsg::ClaimRewards {})?,
        funds: vec![],
    }));
    for reward_token in &config.reward_tokens {
        let balance: BalanceResponse = deps.querier.query_wasm_smart(reward_token, &Cw20QueryMsg::Balance { address: env.contract.address.to_string() })?;
        if balance.balance.is_zero() { continue; }
        let fee_amount = balance.balance * config.performance_fee;
        let swap_amount = balance.balance.saturating_sub(fee_amount);
        if !fee_amount.is_zero() {
            msgs.push(CosmosMsg::Wasm(WasmMsg::Execute {
                contract_addr: reward_token.to_string(),
                msg: to_json_binary(&Cw20ExecuteMsg::Transfer { recipient: config.treasury.to_string(), amount: fee_amount })?,
                funds: vec![],
            }));
        }
        if !swap_amount.is_zero() && config.swap_route.len() >= 2 {
            msgs.push(CosmosMsg::Wasm(WasmMsg::Execute {
                contract_addr: reward_token.to_string(),
                msg: to_json_binary(&Cw20ExecuteMsg::IncreaseAllowance { spender: config.router.to_string(), amount: swap_amount, expires: None })?,
                funds: vec![],
            }));
            let min_out = swap_amount * (Decimal::one() - config.slippage_tolerance);
            msgs.push(CosmosMsg::Wasm(WasmMsg::Execute {
                contract_addr: config.router.to_string(),
                msg: to_json_binary(&RouterExecuteMsg::SwapExactIn { amount_in: swap_amount, route: config.swap_route.clone(), min_amount_out: min_out })?,
                funds: vec![],
            }));
        }
    }
    let want_balance: BalanceResponse = deps.querier.query_wasm_smart(&config.want_token, &Cw20QueryMsg::Balance { address: env.contract.address.to_string() })?;
    if !want_balance.balance.is_zero() {
        msgs.push(CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: config.want_token.to_string(),
            msg: to_json_binary(&Cw20ExecuteMsg::IncreaseAllowance { spender: config.farm_contract.to_string(), amount: want_balance.balance, expires: None })?,
            funds: vec![],
        }));
        msgs.push(CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: config.farm_contract.to_string(),
            msg: to_json_binary(&FarmExecuteMsg::Stake { amount: want_balance.balance })?,
            funds: vec![],
        }));
    }
    let mut config = config;
    config.last_harvest = env.block.time.seconds();
    CONFIG.save(deps.storage, &config)?;
    Ok(Response::new().add_messages(msgs).add_attribute("action", "harvest").add_attribute("reinvested", want_balance.balance.to_string()))
}

pub fn exec_pause(deps: DepsMut, _env: Env, info: MessageInfo) -> Result<Response, StrategyError> {
    let config = CONFIG.load(deps.storage)?;
    if info.sender != config.owner { return Err(StrategyError::Unauthorized { msg: "only owner".to_string() }); }
    let mut c = config; c.paused = true; CONFIG.save(deps.storage, &c)?;
    Ok(Response::new().add_attribute("action", "pause"))
}

pub fn exec_unpause(deps: DepsMut, _env: Env, info: MessageInfo) -> Result<Response, StrategyError> {
    let config = CONFIG.load(deps.storage)?;
    if info.sender != config.owner { return Err(StrategyError::Unauthorized { msg: "only owner".to_string() }); }
    let mut c = config; c.paused = false; CONFIG.save(deps.storage, &c)?;
    Ok(Response::new().add_attribute("action", "unpause"))
}

pub fn exec_update_config(deps: DepsMut, _env: Env, info: MessageInfo, slippage_tolerance: Option<Decimal>, treasury: Option<String>) -> Result<Response, StrategyError> {
    let config = CONFIG.load(deps.storage)?;
    if info.sender != config.owner { return Err(StrategyError::Unauthorized { msg: "only owner".to_string() }); }
    let mut c = config;
    if let Some(st) = slippage_tolerance { c.slippage_tolerance = st; }
    if let Some(t) = treasury { c.treasury = deps.api.addr_validate(&t)?; }
    CONFIG.save(deps.storage, &c)?;
    Ok(Response::new().add_attribute("action", "update_config"))
}

pub fn exec_emergency_withdraw(deps: DepsMut, env: Env, info: MessageInfo, recipient: String) -> Result<Response, StrategyError> {
    let config = CONFIG.load(deps.storage)?;
    if info.sender != config.owner { return Err(StrategyError::Unauthorized { msg: "only owner".to_string() }); }
    let recipient_addr = deps.api.addr_validate(&recipient)?;
    let farm_balance: BalanceResponse = deps.querier.query_wasm_smart(&config.farm_contract, &FarmQueryMsg::StakedBalance { address: env.contract.address.to_string() })?;
    let mut msgs = vec![];
    if !farm_balance.balance.is_zero() {
        msgs.push(CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: config.farm_contract.to_string(),
            msg: to_json_binary(&FarmExecuteMsg::Unstake { amount: farm_balance.balance })?,
            funds: vec![],
        }));
        msgs.push(CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: config.want_token.to_string(),
            msg: to_json_binary(&Cw20ExecuteMsg::Transfer { recipient: recipient_addr.to_string(), amount: farm_balance.balance })?,
            funds: vec![],
        }));
    }
    Ok(Response::new().add_messages(msgs).add_attribute("action", "emergency_withdraw").add_attribute("amount", farm_balance.balance.to_string()))
}

#[entry_point]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::BalanceOf {} => to_json_binary(&query_balance_of(deps, &env)?),
        QueryMsg::WantToken {} => to_json_binary(&CONFIG.load(deps.storage)?.want_token),
        QueryMsg::FarmContract {} => to_json_binary(&CONFIG.load(deps.storage)?.farm_contract),
        QueryMsg::RewardTokens {} => to_json_binary(&CONFIG.load(deps.storage)?.reward_tokens),
        QueryMsg::IsActive {} => to_json_binary(&!CONFIG.load(deps.storage)?.paused),
        QueryMsg::Health {} => to_json_binary(&query_health(deps, &env)?),
        QueryMsg::EstimatedApr {} => to_json_binary(&query_estimated_apr(deps)?),
        QueryMsg::Config {} => to_json_binary(&CONFIG.load(deps.storage)?),
    }
}

fn query_balance_of(deps: Deps, env: &Env) -> StdResult<Uint128> {
    let config = CONFIG.load(deps.storage)?;
    let farm: BalanceResponse = deps.querier.query_wasm_smart(&config.farm_contract, &FarmQueryMsg::StakedBalance { address: env.contract.address.to_string() })?;
    let idle: BalanceResponse = deps.querier.query_wasm_smart(&config.want_token, &Cw20QueryMsg::Balance { address: env.contract.address.to_string() })?;
    Ok(farm.balance + idle.balance)
}

fn query_health(deps: Deps, env: &Env) -> StdResult<HealthResponse> {
    let config = CONFIG.load(deps.storage)?;
    let farm = deps.querier.query_wasm_smart::<BalanceResponse>(&config.farm_contract, &FarmQueryMsg::StakedBalance { address: env.contract.address.to_string() }).unwrap_or(BalanceResponse { balance: Uint128::zero() });
    let idle = deps.querier.query_wasm_smart::<BalanceResponse>(&config.want_token, &Cw20QueryMsg::Balance { address: env.contract.address.to_string() }).unwrap_or(BalanceResponse { balance: Uint128::zero() });
    Ok(HealthResponse { status: "healthy".to_string(), details: "normal".to_string(), farm_balance: farm.balance, idle_balance: idle.balance, last_harvest: config.last_harvest })
}

fn query_estimated_apr(deps: Deps) -> StdResult<EstimatedAprResponse> {
    let config = CONFIG.load(deps.storage)?;
    let farm_apr: Decimal = deps.querier.query_wasm_smart(&config.farm_contract, &FarmQueryMsg::RewardRate {}).unwrap_or(Decimal::zero());
    Ok(EstimatedAprResponse { gross_apr: farm_apr, net_apr: farm_apr * (Decimal::one() - config.performance_fee), performance_fee: config.performance_fee })
}

4. 自动复投逻辑

4.1 Harvest 调度

Vault.harvest() -> Strategy.harvest() -> Farm.claimRewards()
                                              |
                                              v
                                        奖励代币 (MSG, USDC...)
                                              |
                                              v
                                        DEX Router.swap()
                                              |
                                              v
                                        Want Token (LP Token)
                                              |
                                              v
                                        Farm.stake()  <- reinvest

4.2 激励模型

pub fn execute_harvest_with_incentive(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    let gain_estimate = estimate_harvest_gain(deps.as_ref())?;
    if gain_estimate < MIN_HARVEST_PROFIT {
        return Err(ContractError::InvalidArgument { msg: "收益不足以覆盖 Gas".to_string() });
    }
    let mut response = execute_harvest(deps, env.clone(), info.clone())?;
    let incentive = gain_estimate * Decimal::permille(5);
    if !incentive.is_zero() {
        response = response.add_message(CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: config.want_token.to_string(),
            msg: to_json_binary(&Cw20ExecuteMsg::Transfer { recipient: info.sender.to_string(), amount: incentive })?,
            funds: vec![],
        }));
    }
    Ok(response)
}

4.3 批量 Harvest

pub fn execute_batch_harvest(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    let mut msgs = vec![];
    for strategy in &config.strategies {
        msgs.push(CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: strategy.to_string(),
            msg: to_json_binary(&StrategyExecuteMsg::Harvest {})?,
            funds: vec![],
        }));
    }
    Ok(Response::new().add_messages(msgs).add_attribute("action", "batch_harvest").add_attribute("count", config.strategies.len().to_string()))
}

4.4 Gas 优化策略

// Gas 优化: 使用 Permit 代替 Approve
// 标准 Approve: ~50k gas
// Permit: ~5k gas (链下签名, 链上验证)

// Gas 优化: 合并交易
// Instead of:
//   Tx1: claim rewards (100k)
//   Tx2: swap (150k)
//   Tx3: stake (80k)
//   Total: 330k
// Do:
//   Tx1: claim + swap + stake (250k, saved 80k)

pub fn calculate_harvest_threshold(
    total_balance: Uint128,
    estimated_apr: Decimal,
    gas_cost: Uint128,
) -> Uint128 {
    let hourly_yield = total_balance * estimated_apr / Uint128::from(365 * 24u64);
    hourly_yield.max(gas_cost * Uint128::from(2u64))
}

4.5 Python Harvester 机器人

#!/usr/bin/env python3
import os, time, json, logging, argparse
from datetime import datetime
from cosmoslib import Wallet, Contract, StargateClient

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("harvester")

class HarvesterBot:
    def __init__(self, rpc: str, mnemonic: str, interval: int = 300):
        self.client = StargateClient(rpc)
        self.wallet = Wallet.from_mnemonic(mnemonic)
        self.sleep_interval = interval
        self.vaults = []
        self.stats = {"total_harvests": 0, "total_gas_spent": 0}

    def discover_vaults(self, registry: str):
        logger.info(f"Discovering vaults from {registry}")
        # Query registry to get all vault addresses
        # For demo, use hardcoded list
        pass

    def check_vault(self, addr: str) -> bool:
        try:
            c = Contract(self.client, addr, None)
            params = c.query("get_harvest_params") if hasattr(c, 'query') else {"last_harvest": 0, "min_interval": 3600}
            elapsed = time.time() - params.get("last_harvest", 0)
            if elapsed < params.get("min_interval", 3600):
                return False
            return True
        except Exception as e:
            logger.error(f"Check vault {addr} failed: {e}")
            return False

    def harvest(self, addr: str) -> bool:
        try:
            tx_hash = self.client.execute_contract(
                self.wallet, addr, {"harvest": {}},
                gas_limit=600_000, gas_price="1000000000attoMSG",
            )
            receipt = self.client.wait_for_tx(tx_hash, timeout=30)
            logger.info(f"Harvest success: {tx_hash}, gas: {receipt['gas_used']}")
            self.stats["total_harvests"] += 1
            self.stats["total_gas_spent"] += int(receipt["gas_used"])
            return True
        except Exception as e:
            logger.error(f"Harvest failed: {e}")
            return False

    def run_forever(self):
        logger.info(f"Harvester started, interval: {self.sleep_interval}s")
        while True:
            try:
                for vault in self.vaults:
                    if self.check_vault(vault):
                        self.harvest(vault)
                        time.sleep(5)
            except Exception as e:
                logger.error(f"Cycle error: {e}")
            time.sleep(self.sleep_interval)

5. 费用模型

5.1 费用结构

                    Gross Yield (100%)
                          |
            +-------------+-------------+
            |                           |
        Performance Fee (20%)       Management Fee (2% APR)
            |                           |
            v                           v
        Treasury (50%)              Treasury
        Developer (20%)
        Buyback (20%)
        Insurance (10%)
            |
            v
        Net Yield ~= 78% of Gross

5.2 费用计算公式

/// Performance Fee
pub fn calc_performance_fee(profit: Uint128, rate: Decimal) -> Uint128 {
    if profit.is_zero() || rate.is_zero() { return Uint128::zero(); }
    profit * rate
}

/// 高水位线 High-Water Mark
pub struct HighWaterMark { pub highest_pps: Uint128, pub last_reset: u64 }

pub fn calc_performance_fee_hwm(
    current_pps: Uint128, hwm_pps: Uint128, total_supply: Uint128, fee_rate: Decimal,
) -> (Uint128, Uint128) {
    if current_pps <= hwm_pps { return (Uint128::zero(), hwm_pps); }
    let denominator = Uint128::new(10u128.pow(18));
    let prev_value = hwm_pps * total_supply / denominator;
    let curr_value = current_pps * total_supply / denominator;
    let profit = curr_value - prev_value;
    let fee = profit * fee_rate;
    (fee, current_pps)
}

/// 管理费 (按秒)
pub fn calc_management_fee(balance: Uint128, rate: Decimal, elapsed_secs: u64) -> Uint128 {
    if balance.is_zero() || rate.is_zero() || elapsed_secs == 0 { return Uint128::zero(); }
    let annual = balance * rate;
    annual.multiply_ratio(elapsed_secs, 365 * 24 * 60 * 60)
}

/// 动态取款费 (随时间递减, 3天后归零)
pub fn calc_withdrawal_fee(amount: Uint128, deposit_time: u64, current_time: u64, base_rate: Decimal, grace_days: u64) -> Uint128 {
    let held = current_time.saturating_sub(deposit_time);
    let grace = grace_days * 24 * 60 * 60;
    if held >= grace { return Uint128::zero(); }
    let ratio = Decimal::from_ratio(grace - held, grace);
    amount * base_rate * ratio
}

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

    #[test]
    fn test_performance_fee() {
        let fee = calc_performance_fee(Uint128::new(1000), Decimal::percent(20));
        assert_eq!(fee, Uint128::new(200));
    }

    #[test]
    fn test_management_fee_30d() {
        let balance = Uint128::new(1_000_000_000_000_000_000_000u128);
        let fee = calc_management_fee(balance, Decimal::percent(2), 30 * 86400);
        let expected = balance * Decimal::percent(2) * Decimal::from_ratio(30u64, 365u64);
        assert_eq!(fee, expected);
    }

    #[test]
    fn test_withdrawal_fee_decay() {
        let amount = Uint128::new(1000);
        let now = 100_000_000;
        let fee1 = calc_withdrawal_fee(amount, now, now, Decimal::percent(1), 3);
        assert_eq!(fee1, amount * Decimal::percent(1));
        let fee2 = calc_withdrawal_fee(amount, now, now + 3 * 86400, Decimal::percent(1), 3);
        assert_eq!(fee2, Uint128::zero());
    }
}

6. 风险管理

6.1 多层暂停机制

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum PauseLevel { None, StopDeposits, FullPause, EmergencyLock }

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PauseConfig {
    pub level: PauseLevel, pub reason: String,
    pub paused_by: Addr, pub paused_at: u64,
    pub estimated_duration: Option<u64>,
}

pub const PAUSE_CONFIG: Item<PauseConfig> = Item::new("pause_config");

pub fn assert_operation_permitted(deps: Deps, op: &str) -> Result<(), ContractError> {
    let pc = PAUSE_CONFIG.load(deps.storage)?;
    match pc.level {
        PauseLevel::None => Ok(()),
        PauseLevel::StopDeposits => {
            if op == "deposit" { Err(ContractError::Paused { reason: pc.reason }) } else { Ok(()) }
        }
        PauseLevel::FullPause => Err(ContractError::Paused { reason: pc.reason }),
        PauseLevel::EmergencyLock => {
            if op != "emergency_withdraw" { Err(ContractError::Paused { reason: "emergency lock".into() }) } else { Ok(()) }
        }
    }
}

6.2 策略迁移

pub fn safe_migrate_strategy(deps: DepsMut, env: Env, old_strategy: Addr, new_strategy: Addr) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    let balance_before: Uint128 = deps.querier.query_wasm_smart(&old_strategy, &StrategyQueryMsg::BalanceOf {})?;
    Ok(Response::new()
        .add_message(CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: old_strategy.to_string(),
            msg: to_json_binary(&StrategyExecuteMsg::Withdraw { amount: balance_before, recipient: env.contract.address.to_string() })?,
            funds: vec![],
        }))
        .add_message(CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: config.want_token.to_string(),
            msg: to_json_binary(&Cw20ExecuteMsg::IncreaseAllowance { spender: new_strategy.to_string(), amount: balance_before, expires: None })?,
            funds: vec![],
        }))
        .add_message(CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: new_strategy.to_string(),
            msg: to_json_binary(&StrategyExecuteMsg::Deposit { amount: balance_before })?,
            funds: vec![],
        }))
        .add_attribute("action", "safe_migration")
        .add_attribute("amount", balance_before.to_string()))
}

6.3 Health Check

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct VaultHealthReport {
    pub status: String, pub vault_balance: Uint128, pub total_supply: Uint128,
    pub price_per_share: Uint128, pub strategy_count: usize,
    pub strategies: Vec<StrategyHealthReport>, pub tvl: Uint128,
    pub last_harvest: u64, pub paused: bool, pub errors: Vec<String>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct StrategyHealthReport {
    pub address: String, pub status: String, pub balance: Uint128,
    pub last_harvest: u64, pub error: Option<String>,
}

pub fn perform_health_check(deps: Deps, env: Env) -> StdResult<VaultHealthReport> {
    let config = CONFIG.load(deps.storage)?;
    let mut errors = vec![];
    let mut strategies = vec![];

    for strategy in &config.strategies {
        let health: HealthResponse = match deps.querier.query_wasm_smart(strategy, &StrategyQueryMsg::Health {}) {
            Ok(h) => h,
            Err(e) => { errors.push(format!("策略 {} 无响应: {}", strategy, e)); continue; }
        };
        strategies.push(StrategyHealthReport {
            address: strategy.to_string(), status: health.status,
            balance: health.farm_balance + health.idle_balance,
            last_harvest: health.last_harvest,
            error: if health.status != "healthy" { Some(health.details) } else { None },
        });
    }

    let pps = calculate_price_per_share(config.total_balance, config.total_supply).unwrap_or(Uint128::zero());
    let last_harvest = PPS_HISTORY.range(deps.storage, None, None, Order::Descending).next()
        .map(|r| r.map(|(_, s)| s.timestamp).unwrap_or(0)).unwrap_or(0);
    let status = if errors.is_empty() { "healthy".to_string() } else { "degraded".to_string() };

    Ok(VaultHealthReport {
        status, vault_balance: config.total_balance, total_supply: config.total_supply,
        price_per_share: pps, strategy_count: config.strategies.len(), strategies,
        tvl: config.total_balance, last_harvest, paused: config.paused, errors,
    })
}

6.4 损失核算

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct LossRecord {
    pub timestamp: u64, pub amount: Uint128, pub reason: String,
    pub strategy: Option<Addr>, pub compensated: bool,
}

pub const LOSS_RECORDS: Map<u64, LossRecord> = Map::new("loss_records");
pub const TOTAL_LOSS: Item<Uint128> = Item::new("total_loss");

pub fn report_loss(deps: DepsMut, env: Env, amount: Uint128, reason: String, strategy: Option<Addr>) -> Result<Response, ContractError> {
    let record = LossRecord { timestamp: env.block.time.seconds(), amount, reason, strategy, compensated: false };
    LOSS_RECORDS.save(deps.storage, &record.timestamp, &record)?;
    let mut total = TOTAL_LOSS.load(deps.storage).unwrap_or(Uint128::zero());
    total += amount;
    TOTAL_LOSS.save(deps.storage, &total)?;
    Ok(Response::new().add_attribute("action", "loss_reported").add_attribute("amount", amount.to_string()))
}

6.5 安全清单

// 安全清单:
// 1. 合约审计 (至少 2 家)
// 2. 时间锁管理关键参数变更
// 3. 多签治理 (>= 2/3)
// 4. 紧急暂停机制
// 5. 策略白名单 (仅允许经过审计的策略)
// 6. 最大存款限额
// 7. 速率限制 (单位时间内最大 Harvest 次数)
// 8. 滑点保护
// 9. 高水位线绩效费
// 10. 保险基金

pub const MAX_TVL: Item<Uint128> = Item::new("max_tvl");

pub fn assert_tvl_limit(deps: Deps, additional: Uint128) -> Result<(), ContractError> {
    let max = MAX_TVL.load(deps.storage).unwrap_or(Uint128::MAX);
    let config = CONFIG.load(deps.storage)?;
    let new_tvl = config.total_balance + additional;
    if new_tvl > max {
        return Err(ContractError::InvalidArgument { msg: format!("TVL 超过上限: {}/{}", new_tvl, max) });
    }
    Ok(())
}

7. 前端实现

7.1 Vault Dashboard

import React, { useEffect, useState } from 'react';
import {
  Box, Card, CardContent, Typography, Grid, Button,
  LinearProgress, Tooltip, Chip, Dialog, DialogTitle,
  DialogContent, TextField, CircularProgress, Divider,
} from '@mui/material';
import { useWallet, useContract } from '@msg-chain/react';
import { formatUnits, parseUnits } from 'ethers';

interface VaultData {
  name: string; wantToken: string; totalSupply: string;
  totalBalance: string; pricePerShare: string; tvl: string;
  apr7d: string; apr30d: string; netApr: string;
  performanceFee: string; managementFee: string; withdrawalFee: string;
  paused: boolean; strategies: string[];
}

interface UserData { shares: string; wantTokenBalance: string; shareValue: string; unrealizedGains: string; }

const VAULT_ABI = [
  'function get_config() view returns (tuple)',
  'function get_price_per_share() view returns (tuple)',
  'function get_user_info(address) view returns (tuple)',
  'function get_apr() view returns (tuple)',
  'function get_tvl() view returns (tuple)',
  'function get_strategies() view returns (string[])',
  'function simulate_deposit(uint256) view returns (tuple)',
  'function simulate_withdraw(uint256) view returns (tuple)',
];

export const VaultDashboard: React.FC<{ vaultAddress: string }> = ({ vaultAddress }) => {
  const { account, provider } = useWallet();
  const [vault, setVault] = useState<VaultData | null>(null);
  const [user, setUser] = useState<UserData | null>(null);
  const [loading, setLoading] = useState(true);
  const vaultContract = useContract(vaultAddress, VAULT_ABI);

  useEffect(() => { loadVaultData(); }, [vaultAddress, account]);

  const loadVaultData = async () => {
    try {
      setLoading(true);
      const [config, pps, strategies, apr] = await Promise.all([
        vaultContract.query('get_config', []),
        vaultContract.query('get_price_per_share', []),
        vaultContract.query('get_strategies', []),
        vaultContract.query('get_apr', []),
      ]);
      setVault({
        name: config.name, wantToken: config.want_token,
        totalSupply: config.total_supply, totalBalance: config.total_balance,
        pricePerShare: pps.price_per_share, tvl: config.total_balance,
        apr7d: apr.apr_7d, apr30d: apr.apr_30d, netApr: apr.net_apr,
        performanceFee: config.performance_fee, managementFee: config.management_fee,
        withdrawalFee: config.withdrawal_fee, paused: config.paused, strategies,
      });
      if (account) {
        const userInfo = await vaultContract.query('get_user_info', [account]);
        setUser({
          shares: userInfo.shares, wantTokenBalance: userInfo.want_token_balance,
          shareValue: userInfo.share_value, unrealizedGains: userInfo.unrealized_gains,
        });
      }
    } catch (err) { console.error(err); }
    finally { setLoading(false); }
  };

  const fmt = (a: string, d = 18) => parseFloat(formatUnits(a, d)).toLocaleString(undefined, { maximumFractionDigits: 6 });
  const pct = (d: string) => (parseFloat(d) * 100).toFixed(2) + '%';

  if (loading) return <LinearProgress />;
  if (!vault) return <Typography color="error">无法加载 Vault 数据</Typography>;

  return (
    <Box sx={{ maxWidth: 1200, mx: 'auto', p: 3 }}>
      <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 4 }}>
        <Box>
          <Typography variant="h4">{vault.name}</Typography>
          <Typography variant="body2" color="text.secondary">{vaultAddress}</Typography>
        </Box>
        <Chip label={vault.paused ? '已暂停' : '运行中'} color={vault.paused ? 'error' : 'success'} />
      </Box>

      <Grid container spacing={3} sx={{ mb: 4 }}>
        <Grid item xs={12} sm={6} md={3}>
          <Card><CardContent>
            <Typography variant="overline">TVL</Typography>
            <Typography variant="h5">${fmt(vault.tvl)}</Typography>
          </CardContent></Card>
        </Grid>
        <Grid item xs={12} sm={6} md={3}>
          <Card><CardContent>
            <Typography variant="overline">价格/份额</Typography>
            <Typography variant="h5">{fmt(vault.pricePerShare)}</Typography>
            <Typography variant="caption" color="text.secondary">总供应: {fmt(vault.totalSupply)}</Typography>
          </CardContent></Card>
        </Grid>
        <Grid item xs={12} sm={6} md={3}>
          <Card><CardContent>
            <Typography variant="overline">APR (30d)</Typography>
            <Typography variant="h5" color="success.main">{pct(vault.apr30d)}</Typography>
            <Typography variant="caption">净 APR: {pct(vault.netApr)}</Typography>
          </CardContent></Card>
        </Grid>
        <Grid item xs={12} sm={6} md={3}>
          <Card><CardContent>
            <Typography variant="overline">策略数</Typography>
            <Typography variant="h5">{vault.strategies.length}</Typography>
          </CardContent></Card>
        </Grid>
      </Grid>

      {user && (
        <Card sx={{ mb: 4 }}>
          <CardContent>
            <Typography variant="h6" gutterBottom>我的仓位</Typography>
            <Grid container spacing={2}>
              <Grid item xs={4}><Typography variant="overline">持有份额</Typography><Typography>{fmt(user.shares)}</Typography></Grid>
              <Grid item xs={4}><Typography variant="overline">资产价值</Typography><Typography>{fmt(user.shareValue)}</Typography></Grid>
              <Grid item xs={4}><Typography variant="overline">未实现收益</Typography><Typography color={parseFloat(user.unrealizedGains) >= 0 ? 'success.main' : 'error.main'}>{fmt(user.unrealizedGains)}</Typography></Grid>
            </Grid>
          </CardContent>
        </Card>
      )}

      <Card sx={{ mb: 4 }}>
        <CardContent>
          <Typography variant="h6" gutterBottom>费用结构</Typography>
          <Grid container spacing={2}>
            <Grid item xs={4}><Tooltip title="Harvest 时从利润中扣除"><Typography>绩效费: {pct(vault.performanceFee)}</Typography></Tooltip></Grid>
            <Grid item xs={4}><Tooltip title="按年化比例每日累积"><Typography>管理费: {pct(vault.managementFee)}</Typography></Tooltip></Grid>
            <Grid item xs={4}><Tooltip title="取款时扣除, 3 天后归零"><Typography>取款费: {pct(vault.withdrawalFee)}</Typography></Tooltip></Grid>
          </Grid>
        </CardContent>
      </Card>

      <Box sx={{ display: 'flex', gap: 2, mb: 4 }}>
        <DepositDialog vaultAddress={vaultAddress} onSuccess={loadVaultData} />
        <WithdrawDialog vaultAddress={vaultAddress} onSuccess={loadVaultData} />
      </Box>
    </Box>
  );
};

const DepositDialog: React.FC<{ vaultAddress: string; onSuccess: () => void }> = ({ vaultAddress, onSuccess }) => {
  const { account } = useWallet();
  const [open, setOpen] = useState(false);
  const [amount, setAmount] = useState('');
  const [loading, setLoading] = useState(false);
  const [sim, setSim] = useState<any>(null);
  const vaultContract = useContract(vaultAddress, VAULT_ABI);

  const handleChange = async (e: any) => {
    const val = e.target.value;
    setAmount(val);
    if (val && parseFloat(val) > 0) {
      try { const s = await vaultContract.query('simulate_deposit', [parseUnits(val, 18).toString()]); setSim(s); } catch {}
    }
  };

  const handleDeposit = async () => {
    if (!account || !amount) return;
    setLoading(true);
    try {
      const tx = await vaultContract.execute('deposit', [{ amount: parseUnits(amount, 18).toString(), recipient: null }]);
      await tx.wait();
      onSuccess(); setOpen(false); setAmount('');
    } catch (err: any) { console.error(err); }
    finally { setLoading(false); }
  };

  return (<>
    <Button variant="contained" onClick={() => setOpen(true)}>存入</Button>
    <Dialog open={open} onClose={() => setOpen(false)} maxWidth="sm" fullWidth>
      <DialogTitle>存入资产</DialogTitle>
      <DialogContent>
        <TextField label="存入数量" type="number" fullWidth value={amount} onChange={handleChange} sx={{ my: 2 }} />
        {sim && <Box sx={{ bgcolor: '#f5f5f5', p: 2, borderRadius: 1 }}>
          <Typography variant="body2">将获得: {formatUnits(sim.shares_received, 18)} 份额</Typography>
        </Box>}
        <Button variant="contained" fullWidth onClick={handleDeposit} disabled={loading || !amount} sx={{ mt: 2 }}>
          {loading ? <CircularProgress size={24} /> : '确认存入'}
        </Button>
      </DialogContent>
    </Dialog>
  </>);
};

const WithdrawDialog: React.FC<{ vaultAddress: string; onSuccess: () => void }> = ({ vaultAddress, onSuccess }) => {
  const { account } = useWallet();
  const [open, setOpen] = useState(false);
  const [shares, setShares] = useState('');
  const [loading, setLoading] = useState(false);
  const [sim, setSim] = useState<any>(null);
  const vaultContract = useContract(vaultAddress, VAULT_ABI);

  const handleChange = async (e: any) => {
    const val = e.target.value;
    setShares(val);
    if (val && parseFloat(val) > 0) {
      try { const s = await vaultContract.query('simulate_withdraw', [parseUnits(val, 18).toString()]); setSim(s); } catch {}
    }
  };

  const handleWithdraw = async () => {
    if (!account || !shares) return;
    setLoading(true);
    try {
      const tx = await vaultContract.execute('withdraw', [{ shares: parseUnits(shares, 18).toString() }]);
      await tx.wait();
      onSuccess(); setOpen(false); setShares('');
    } catch (err: any) { console.error(err); }
    finally { setLoading(false); }
  };

  return (<>
    <Button variant="outlined" onClick={() => setOpen(true)}>提取</Button>
    <Dialog open={open} onClose={() => setOpen(false)} maxWidth="sm" fullWidth>
      <DialogTitle>提取资产</DialogTitle>
      <DialogContent>
        <TextField label="提取份额" type="number" fullWidth value={shares} onChange={handleChange} sx={{ my: 2 }} />
        {sim && <Box sx={{ bgcolor: '#f5f5f5', p: 2, borderRadius: 1 }}>
          <Typography variant="body2">将获得: {formatUnits(sim.net_amount, 18)} want token</Typography>
          <Typography variant="body2" color="text.secondary">手续费: {formatUnits(sim.fee_charged, 18)}</Typography>
        </Box>}
        <Button variant="contained" fullWidth onClick={handleWithdraw} disabled={loading || !shares} sx={{ mt: 2 }}>
          {loading ? <CircularProgress size={24} /> : '确认提取'}
        </Button>
      </DialogContent>
    </Dialog>
  </>);
};

7.2 Admin 面板

export const AdminPanel: React.FC<{ vaultAddress: string }> = ({ vaultAddress }) => {
  const { account } = useWallet();
  const vaultContract = useContract(vaultAddress, VAULT_ABI);

  const harvest = async () => {
    await vaultContract.execute('harvest', [{}], { gasLimit: 1_000_000 });
  };
  const pause = async () => { await vaultContract.execute('pause', [{}]); };
  const unpause = async () => { await vaultContract.execute('unpause', [{}]); };

  return (
    <Card sx={{ m: 3 }}>
      <CardContent>
        <Typography variant="h5" gutterBottom>Admin 面板</Typography>
        <Button onClick={harvest} variant="contained" color="warning" sx={{ mr: 2 }}>手动 Harvest</Button>
        <Button onClick={pause} variant="contained" color="error" sx={{ mr: 2 }}>暂停</Button>
        <Button onClick={unpause} variant="outlined">恢复</Button>
      </CardContent>
    </Card>
  );
};

8. 完整示例

8.1 端到端流程 (TypeScript)

import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate';
import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing';
import { calculateFee, GasPrice } from '@cosmjs/stargate';

const MSG_RPC = 'https://rpc.msg-chain-1.com';
const MSG_CHAIN_ID = 'msg-chain-1';

async function initClient(mnemonic: string) {
  const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: 'msg' });
  const [account] = await wallet.getAccounts();
  const client = await SigningCosmWasmClient.connectWithSigner(MSG_RPC, wallet, {
    gasPrice: GasPrice.fromString('1000000000attoMSG'),
  });
  console.log('Wallet:', account.address);
  return { client, wallet, account };
}

async function deployVault(client: SigningCosmWasmClient, sender: string, codeId: number, wantToken: string) {
  const { contractAddress } = await client.instantiate(sender, codeId, {
    name: 'MSG-USDT LP Auto-Vault',
    version: '1.0.0',
    want_token: wantToken,
    reward_tokens: [wantToken],
    performance_fee: '0.2',
    withdrawal_fee: '0.005',
    management_fee: '0.02',
    treasury: sender,
    owner: sender,
  }, 'MSG-USDT Vault', calculateFee(500_000, '1000000000attoMSG'));
  console.log('Vault deployed:', contractAddress);
  return contractAddress;
}

async function deployStrategy(client: SigningCosmWasmClient, sender: string, codeId: number, vaultAddr: string, wantToken: string, farm: string, router: string) {
  const { contractAddress } = await client.instantiate(sender, codeId, {
    vault: vaultAddr, want_token: wantToken, farm_contract: farm,
    reward_tokens: [wantToken], router: router,
    swap_route: [wantToken, wantToken],
    slippage_tolerance: '0.01', performance_fee: '0.2',
    treasury: sender, owner: sender, min_harvest_interval: 3600,
  }, 'LP Strategy', calculateFee(400_000, '1000000000attoMSG'));
  console.log('Strategy deployed:', contractAddress);
  return contractAddress;
}

async function addStrategy(client: SigningCosmWasmClient, sender: string, vaultAddr: string, strategyAddr: string) {
  await client.execute(sender, vaultAddr, { add_strategy: { strategy: strategyAddr, weight: 100 } }, calculateFee(200_000, '1000000000attoMSG'));
  console.log('Strategy added');
}

async function depositWant(client: SigningCosmWasmClient, sender: string, wantToken: string, vaultAddr: string, amount: string) {
  await client.execute(sender, wantToken, { increase_allowance: { spender: vaultAddr, amount, expires: null } }, calculateFee(150_000, '1000000000attoMSG'));
  await client.execute(sender, vaultAddr, { deposit: { amount, recipient: null } }, calculateFee(300_000, '1000000000attoMSG'));
  console.log('Deposit OK');
}

async function harvest(client: SigningCosmWasmClient, sender: string, vaultAddr: string) {
  const tx = await client.execute(sender, vaultAddr, { harvest: {} }, calculateFee(600_000, '1000000000attoMSG'));
  console.log('Harvest TX:', tx.transactionHash);
}

async function queryVault(client: SigningCosmWasmClient, vaultAddr: string) {
  const config = await client.queryContractSmart(vaultAddr, { get_config: {} });
  const pps = await client.queryContractSmart(vaultAddr, { get_price_per_share: {} });
  const tvl = await client.queryContractSmart(vaultAddr, { get_tvl: {} });
  console.log('Config:', config, 'PPS:', pps.price_per_share, 'TVL:', tvl.tvl);
}

async function main() {
  const { client, account } = await initClient(process.env.MNEMONIC!);
  const vaultAddr = await deployVault(client, account.address, Number(process.env.VAULT_CODE_ID), process.env.WANT_TOKEN!);
  const stratAddr = await deployStrategy(client, account.address, Number(process.env.STRATEGY_CODE_ID), vaultAddr, process.env.WANT_TOKEN!, process.env.FARM_CONTRACT!, process.env.ROUTER!);
  await addStrategy(client, account.address, vaultAddr, stratAddr);
  await queryVault(client, vaultAddr);
  await depositWant(client, account.address, process.env.WANT_TOKEN!, vaultAddr, '1000000000000000000000');
  await queryVault(client, vaultAddr);
  await harvest(client, account.address, vaultAddr);
  await queryVault(client, vaultAddr);
  console.log('=== Complete ===');
}

8.2 Python 测试脚本

import asyncio, json
from pathlib import Path
from cosmwasm_client import CosmWasmClient, Wallet

MSG_RPC = "https://rpc.msg-chain-1.com"
VAULT_WASM = Path("artifacts/yield_vault.wasm")
STRATEGY_WASM = Path("artifacts/lp_farm_strategy.wasm")

async def test_full_workflow():
    wallet = Wallet.from_mnemonic(open("test.mnemonic").read().strip())
    wallet.set_prefix("msg")
    client = CosmWasmClient(MSG_RPC, wallet)
    print(f"Deployer: {wallet.address}")

    vault_code_id = await client.upload_wasm(VAULT_WASM)
    print(f"Vault code: {vault_code_id}")
    strat_code_id = await client.upload_wasm(STRATEGY_WASM)
    print(f"Strategy code: {strat_code_id}")

    vault_addr = await client.instantiate(vault_code_id, {
        "name": "Test Vault", "version": "1.0.0",
        "want_token": "msg1testwant", "reward_tokens": ["msg1reward"],
        "performance_fee": "0.2", "withdrawal_fee": "0.005",
        "management_fee": "0.02", "treasury": wallet.address,
        "owner": wallet.address,
    }, "Test Vault")
    print(f"Vault: {vault_addr}")

    strat_addr = await client.instantiate(strat_code_id, {
        "vault": vault_addr, "want_token": "msg1testwant",
        "farm_contract": "msg1testfarm", "reward_tokens": ["msg1reward"],
        "router": "msg1testrouter", "swap_route": ["msg1reward", "msg1testwant"],
        "slippage_tolerance": "0.01", "performance_fee": "0.2",
        "treasury": wallet.address, "owner": wallet.address, "min_harvest_interval": 3600,
    }, "Test Strategy")
    print(f"Strategy: {strat_addr}")

    await client.execute(vault_addr, {"add_strategy": {"strategy": strat_addr, "weight": 100}})
    config = await client.query(vault_addr, {"get_config": {}})
    print(f"Config: {json.dumps(config, indent=2)}")
    pps = await client.query(vault_addr, {"get_price_per_share": {}})
    print(f"PPS: {pps['price_per_share']}")
    print("All tests passed")

asyncio.run(test_full_workflow())

8.3 Bash 部署脚本

#!/bin/bash
set -euo pipefail

MNEMONIC="$1"
WANT_TOKEN="$2"
FARM_CONTRACT="$3"
ROUTER="$4"
MSG_CLI="msg-chain-devkit"
CHAIN_ID="msg-chain-1"
NODE="https://rpc.msg-chain-1.com"

echo "=== 编译合约 ==="
cd contracts/yield-vault && RUSTFLAGS='-C link-arg=-s' cargo wasm
cp target/wasm32-unknown-unknown/release/yield_vault.wasm ../../artifacts/
cd ../..
cd contracts/strategy-lp-farm && RUSTFLAGS='-C link-arg=-s' cargo wasm
cp target/wasm32-unknown-unknown/release/lp_farm_strategy.wasm ../../artifacts/
cd ../..

echo "=== 上传合约 ==="
VAULT_CODE_ID=$($MSG_CLI tx wasm store artifacts/yield_vault.wasm --from "$MNEMONIC" --chain-id "$CHAIN_ID" --node "$NODE" --gas auto --output json | jq -r '.logs[0].events[] | select(.type=="store_code") | .attributes[] | select(.key=="code_id") | .value')
echo "Vault Code ID: $VAULT_CODE_ID"

STRAT_CODE_ID=$($MSG_CLI tx wasm store artifacts/lp_farm_strategy.wasm --from "$MNEMONIC" --chain-id "$CHAIN_ID" --node "$NODE" --gas auto --output json | jq -r '.logs[0].events[] | select(.type=="store_code") | .attributes[] | select(.key=="code_id") | .value')
echo "Strategy Code ID: $STRAT_CODE_ID"

echo "=== 实例化 Vault ==="
VAULT_ADDR=$($MSG_CLI tx wasm instantiate "$VAULT_CODE_ID" '{"name":"MSG-USDT LP Vault","version":"1.0.0","want_token":"'$WANT_TOKEN'","reward_tokens":["'$WANT_TOKEN'"],"performance_fee":"0.2","withdrawal_fee":"0.005","management_fee":"0.02","treasury":"'$($MSG_CLI keys show --address)'","owner":"'$($MSG_CLI keys show --address)'"}' --label "MSG-USDT Vault" --from "$MNEMONIC" --chain-id "$CHAIN_ID" --node "$NODE" --gas auto --output json | jq -r '.logs[0].events[] | select(.type=="instantiate") | .attributes[] | select(.key=="_contract_address") | .value')
echo "Vault: $VAULT_ADDR"

echo "=== 实例化策略 ==="
STRAT_ADDR=$($MSG_CLI tx wasm instantiate "$STRAT_CODE_ID" '{"vault":"'$VAULT_ADDR'","want_token":"'$WANT_TOKEN'","farm_contract":"'$FARM_CONTRACT'","reward_tokens":["'$WANT_TOKEN'"],"router":"'$ROUTER'","swap_route":["'$WANT_TOKEN'","'$WANT_TOKEN'"],"slippage_tolerance":"0.01","performance_fee":"0.2","treasury":"'$($MSG_CLI keys show --address)'","owner":"'$($MSG_CLI keys show --address)'","min_harvest_interval":3600}' --label "LP Strategy" --from "$MNEMONIC" --chain-id "$CHAIN_ID" --node "$NODE" --gas auto --output json | jq -r '.logs[0].events[] | select(.type=="instantiate") | .attributes[] | select(.key=="_contract_address") | .value')
echo "Strategy: $STRAT_ADDR"

echo "=== 添加策略 ==="
$MSG_CLI tx wasm execute "$VAULT_ADDR" '{"add_strategy":{"strategy":"'$STRAT_ADDR'","weight":100}}' --from "$MNEMONIC" --chain-id "$CHAIN_ID" --node "$NODE" --gas auto

echo "=== 部署完成 ==="
echo "Vault:     $VAULT_ADDR"
echo "Strategy:  $STRAT_ADDR"

附录

A. 配额与限制

参数 默认值 说明
最大策略数 10 单个 Vault 可添加的策略上限
最小 Harvest 间隔 1 小时 防止频繁 Harvest
最大 Performance Fee 50% 收入上限
最大 Withdrawal Fee 10% 取款费上限
最大 Management Fee 20% 管理费年化上限
滑点容忍度默认值 1% 可配置
取款费宽限期 3 天 3 天后取款免费
份额精度 18 decimals 与 MSG Chain 一致

B. 参考链接

C. 常见问题

Q: 为什么需要份额代币?
A: 份额代币跟踪用户在 Vault 中的所有权比例。随着 Harvest 产生收益, Vault 总资产增加, 每份额的价值也随之增长。

Q: 如何计算 APR?
A: APR 基于份额价格的历史增长计算。公式为 (当前价格 - 30天前的价格) / 30天前的价格 * (365/30)。

Q: 什么是高水位线 (HWM)?
A: 高水位线机制确保 Performance Fee 只在 Vault 达到历史最高值时收取, 防止对同一部分利润重复收费。

Q: 资金安全如何保障?
A: 通过多层暂停机制、策略白名单、时间锁、多签治理和保险基金共同保障。

Q: 如何运行 Harvester 机器人?
A: 参考第 4.5 节的 Python 脚本, 配置钱包助记词和 RPC 地址后运行即可。


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

D. 前端状态管理 (Zustand Store)

// stores/vaultStore.ts
import { create } from 'zustand';
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate';

interface VaultStore {
  vaults: VaultSummary[];
  selectedVault: string | null;
  loading: boolean;
  error: string | null;
  client: SigningCosmWasmClient | null;
  setClient: (client: SigningCosmWasmClient) => void;
  fetchVaults: (registryAddr: string) => Promise<void>;
  selectVault: (addr: string) => void;
  refreshVault: (addr: string) => Promise<void>;
}

interface VaultSummary {
  address: string; name: string; tvl: string;
  apr: string; userShares: string; pps: string;
}

export const useVaultStore = create<VaultStore>((set, get) => ({
  vaults: [], selectedVault: null, loading: false, error: null, client: null,
  setClient: (client) => set({ client }),
  fetchVaults: async (registryAddr: string) => {
    set({ loading: true, error: null });
    try {
      const client = get().client;
      if (!client) throw new Error('Client not initialized');
      const vaultAddrs: string[] = await client.queryContractSmart(registryAddr, { get_all_vaults: {} });
      const vaults: VaultSummary[] = [];
      for (const addr of vaultAddrs) {
        const [config, pps, apr] = await Promise.all([
          client.queryContractSmart(addr, { get_config: {} }),
          client.queryContractSmart(addr, { get_price_per_share: {} }),
          client.queryContractSmart(addr, { get_apr: {} }),
        ]);
        vaults.push({ address: addr, name: config.name, tvl: config.total_balance, apr: apr.apr_30d, userShares: '0', pps: pps.price_per_share });
      }
      set({ vaults, loading: false });
    } catch (err: any) { set({ error: err.message, loading: false }); }
  },
  selectVault: (addr) => set({ selectedVault: addr }),
  refreshVault: async (addr: string) => {
    try {
      const client = get().client;
      if (!client) return;
      const [config, pps, apr] = await Promise.all([
        client.queryContractSmart(addr, { get_config: {} }),
        client.queryContractSmart(addr, { get_price_per_share: {} }),
        client.queryContractSmart(addr, { get_apr: {} }),
      ]);
      set((state) => ({
        vaults: state.vaults.map((v) => v.address === addr ? { ...v, tvl: config.total_balance, apr: apr.apr_30d, pps: pps.price_per_share } : v),
      }));
    } catch (err: any) { set({ error: err.message }); }
  },
}));

E. Hooks 封装

// hooks/useVault.ts
import { useCallback, useEffect, useState } from 'react';
import { useWallet, useContract } from '@msg-chain/react';

const VAULT_ABI = [
  'function get_config() view returns (tuple)',
  'function get_price_per_share() view returns (tuple)',
  'function get_user_info(address) view returns (tuple)',
  'function get_apr() view returns (tuple)',
  'function get_tvl() view returns (tuple)',
  'function get_strategies() view returns (string[])',
  'function simulate_deposit(uint256) view returns (tuple)',
  'function simulate_withdraw(uint256) view returns (tuple)',
];

export function useVault(vaultAddress: string) {
  const { account } = useWallet();
  const contract = useContract(vaultAddress, VAULT_ABI);
  const [config, setConfig] = useState<any>(null);
  const [userInfo, setUserInfo] = useState<any>(null);
  const [pps, setPps] = useState<any>(null);
  const [apr, setApr] = useState<any>(null);
  const [loading, setLoading] = useState(true);

  const refresh = useCallback(async () => {
    try {
      setLoading(true);
      const [cfg, price, ap] = await Promise.all([
        contract.query('get_config', []),
        contract.query('get_price_per_share', []),
        contract.query('get_apr', []),
      ]);
      setConfig(cfg); setPps(price); setApr(ap);
      if (account) {
        const user = await contract.query('get_user_info', [account]);
        setUserInfo(user);
      }
    } catch (err) { console.error(err); }
    finally { setLoading(false); }
  }, [vaultAddress, account]);

  useEffect(() => { refresh(); }, [refresh]);

  const deposit = useCallback(async (amount: string) => {
    const tx = await contract.execute('deposit', [{ amount, recipient: null }]);
    await tx.wait(); await refresh();
  }, [contract, refresh]);

  const withdraw = useCallback(async (shares: string) => {
    const tx = await contract.execute('withdraw', [{ shares }]);
    await tx.wait(); await refresh();
  }, [contract, refresh]);

  const doHarvest = useCallback(async () => {
    const tx = await contract.execute('harvest', [{}]);
    await tx.wait(); await refresh();
  }, [contract, refresh]);

  return { config, userInfo, pps, apr, loading, deposit, withdraw, doHarvest, refresh };
}

F. Vault 对比表格

export const VaultCompare: React.FC<{ vaultAddresses: string[] }> = ({ vaultAddresses }) => {
  const [vaults, setVaults] = useState<any[]>([]);

  useEffect(() => {
    Promise.all(
      vaultAddresses.map(async (addr) => {
        const c = useContract(addr, VAULT_ABI);
        const [cfg, pps, apr] = await Promise.all([
          c.query('get_config', []), c.query('get_price_per_share', []), c.query('get_apr', []),
        ]);
        return { address: addr, ...cfg, pps: pps.price_per_share, apr: apr.apr_30d, netApr: apr.net_apr };
      })
    ).then(setVaults);
  }, [vaultAddresses]);

  const fmt = (a: string, d = 18) => parseFloat(formatUnits(a, d)).toLocaleString(undefined, { maximumFractionDigits: 4 });
  const pct = (d: string) => (parseFloat(d) * 100).toFixed(2) + '%';

  return (
    <TableContainer component={Paper}>
      <Table>
        <TableHead>
          <TableRow>
            <TableCell>Vault</TableCell>
            <TableCell align="right">TVL</TableCell>
            <TableCell align="right">APR</TableCell>
            <TableCell align="right">净 APR</TableCell>
            <TableCell align="right">份额价格</TableCell>
            <TableCell align="right">绩效费</TableCell>
            <TableCell align="right">管理费</TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {vaults.map((v) => (
            <TableRow key={v.address}>
              <TableCell>{v.name}</TableCell>
              <TableCell align="right">{fmt(v.total_balance)}</TableCell>
              <TableCell align="right">{pct(v.apr)}</TableCell>
              <TableCell align="right">{pct(v.netApr)}</TableCell>
              <TableCell align="right">{fmt(v.pps)}</TableCell>
              <TableCell align="right">{pct(v.performance_fee)}</TableCell>
              <TableCell align="right">{pct(v.management_fee)}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  );
};

G. 监控告警脚本

#!/usr/bin/env python3
"""monitor.py - Vault 健康监控告警"""
import os, time, logging, requests
from datetime import datetime

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("monitor")
WEBHOOK_URL = os.getenv("SLACK_WEBHOOK", "")

def send_alert(msg: str):
    if WEBHOOK_URL:
        requests.post(WEBHOOK_URL, json={"text": f"[Vault Monitor] {msg}"})
    logger.warning(f"ALERT: {msg}")

def check_vault(client, addr: str):
    try:
        config = client.query(addr, {"get_config": {}})
        health = client.query(addr, {"get_health": {}})
        tvl = int(config["total_balance"])
        paused = config["paused"]
        status = health.get("status", "unknown")
        if paused: send_alert(f"Vault {addr[:12]}... 已暂停!")
        if status != "healthy": send_alert(f"Vault {addr[:12]}... 不健康: {status}")
        if tvl == 0: logger.info(f"Vault {addr[:12]}... TVL 为零")
        return {"addr": addr, "tvl": tvl, "paused": paused, "status": status}
    except Exception as e:
        send_alert(f"Vault {addr[:12]}... 检查失败: {e}")
        return None

def main():
    from cosmoslib import StargateClient
    client = StargateClient("https://rpc.msg-chain-1.com")
    vaults = os.getenv("VAULT_ADDRESSES", "").split(",")
    logger.info(f"监控 {len(vaults)} 个 Vault")
    while True:
        for addr in vaults:
            if addr.strip(): check_vault(client, addr.strip())
        time.sleep(int(os.getenv("CHECK_INTERVAL", "300")))

if __name__ == "__main__":
    main()

H. Performance Fee 深度分析

两种绩效费收取模式对比:

模式 A: 在 Strategy Harvest 中直接扣除 (推荐)

模式 B: 在 Vault 层面统一扣除

// 模式 A (Strategy 层):
let fee = reward_balance * config.performance_fee;
let investable = reward_balance - fee;
// fee -> Treasury, investable -> swap -> reinvest

// 模式 B (Vault 层):
let profit = current_balance - balance_before_harvest;
let fee = profit * config.performance_fee;
// 从 Vault 余额中扣除 fee

I. 合约升级

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct MigrateMsg {
    pub new_version: String,
    pub updated_fees: Option<UpdatedFees>,
    pub new_treasury: Option<String>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct UpdatedFees {
    pub performance_fee: Option<Decimal>,
    pub management_fee: Option<Decimal>,
    pub withdrawal_fee: Option<Decimal>,
}

#[entry_point]
pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
    let mut config = CONFIG.load(deps.storage)?;
    config.version = msg.new_version;
    if let Some(fees) = msg.updated_fees {
        if let Some(f) = fees.performance_fee { config.performance_fee = f; }
        if let Some(f) = fees.management_fee { config.management_fee = f; }
        if let Some(f) = fees.withdrawal_fee { config.withdrawal_fee = f; }
    }
    if let Some(t) = msg.new_treasury { config.treasury = deps.api.addr_validate(&t)?; }
    CONFIG.save(deps.storage, &config)?;
    Ok(Response::new().add_attribute("action", "migrate").add_attribute("version", config.version))
}

J. 收益计算器

// calculators/yieldCalculator.ts
export class YieldCalculator {
  static calculateAPR(ppsHistory: { timestamp: number; price: string }[]): number {
    if (ppsHistory.length < 2) return 0;
    const sorted = ppsHistory.sort((a, b) => a.timestamp - b.timestamp);
    const oldest = sorted[0];
    const newest = sorted[sorted.length - 1];
    const daysDiff = (newest.timestamp - oldest.timestamp) / 86400;
    if (daysDiff < 1) return 0;
    const growth = (parseFloat(newest.price) - parseFloat(oldest.price)) / parseFloat(oldest.price);
    return (growth / daysDiff) * 365 * 100;
  }

  static simulateGrowth(
    initialDeposit: number, aprPercent: number, days: number,
    perfFee: number, mgmtFee: number,
  ): { day: number; balance: number; feesPaid: number }[] {
    const results = [];
    const dailyRate = aprPercent / 100 / 365;
    let balance = initialDeposit;
    let totalFees = 0;
    for (let d = 0; d <= days; d++) {
      results.push({ day: d, balance, feesPaid: totalFees });
      const dailyYield = balance * dailyRate;
      const perfFeeAmount = dailyYield * perfFee;
      const mgmtFeeAmount = balance * (mgmtFee / 365);
      balance += dailyYield - perfFeeAmount - mgmtFeeAmount;
      totalFees += perfFeeAmount + mgmtFeeAmount;
    }
    return results;
  }

  static estimateTimeToDouble(aprPercent: number): number {
    return 72 / aprPercent;
  }
}

K. 完整接口参考

Vault Execute 接口

方法 权限 描述
deposit(amount, recipient) 任何人 存入 want token, 获得份额
withdraw(shares) 任何人 提取份额, 获得 want token
withdraw_all() 任何人 提取全部份额
harvest() 任何人 执行 Harvest 操作
add_strategy(strategy, weight) Owner 添加策略
remove_strategy(strategy) Owner 移除策略
migrate_strategy(old, new) Owner 迁移策略
update_fees(perf, mgmt, wd) Owner 更新费用参数
update_treasury(treasury) Owner 更新国库地址
transfer_ownership(new) Owner 转移所有权
pause() Owner 暂停 Vault
unpause() Owner 恢复 Vault
emergency_withdraw(amount, to) Owner 紧急提取
set_strategy_weights(weights) Owner 设置策略权重

Vault Query 接口

方法 描述
get_config() 获取 Vault 配置
get_user_info(address) 获取用户信息
get_price_per_share() 获取份额价格
get_tvl() 获取 TVL
get_apr() 获取 APR 数据
get_strategies() 获取策略列表
simulate_deposit(amount) 模拟存款
simulate_withdraw(shares) 模拟取款
get_pps_history(from, to) 获取份额价格历史
get_fee_history(from, to) 获取费用历史

本文档为 MSG Chain 收益聚合器 (Yield Aggregator) 的完整实现指南。涵盖从合约设计、前端开发到部署运维的全流程。所有代码示例基于 CosmWasm 1.5 + MSG Chain。

链参数: chain-id=msg-chain-1, bech32=msg, decimals=18

L. Gas 成本分析

操作 预估 Gas 成本 (1,000,000,000 attoMSG)
deposit 250,000 - 350,000 0.00625 - 0.00875 MSG
withdraw 200,000 - 300,000 0.005 - 0.0075 MSG
harvest (单策略) 400,000 - 600,000 0.01 - 0.015 MSG
harvest (批量) 300,000 + 150k/策略 0.0075 + 0.00375/策略
add_strategy 150,000 0.00375 MSG
pause/unpause 100,000 0.0025 MSG

M. 策略开发模板

// contracts/strategies/my_strategy/src/contract.rs
// 开发新策略的模板代码

use cosmwasm_std::{entry_point, to_json_binary, CosmosMsg, DepsMut, Env, MessageInfo, Response, StdResult, Uint128, WasmMsg};
use cw_storage_plus::Item;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
    pub vault: Addr,
    pub want_token: Addr,
    // 添加你的协议特定参数
    pub external_protocol: Addr,
}

pub const CONFIG: Item<Config> = Item::new("config");

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    Deposit { amount: Uint128 },
    Withdraw { amount: Uint128, recipient: String },
    Harvest {},
    Pause {},
    Unpause {},
}

#[entry_point]
pub fn instantiate(deps: DepsMut, _env: Env, _info: MessageInfo, msg: InstantiateMsg) -> StdResult<Response> {
    // Step 1: 保存配置
    // Step 2: 验证参数
    // Step 3: 返回 Response
    Ok(Response::new())
}

#[entry_point]
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> Result<Response, StrategyError> {
    match msg {
        ExecuteMsg::Deposit { amount } => {
            // Step 1: 授权外部协议使用 want token
            // Step 2: 调用外部协议的 deposit/stake 方法
            Ok(Response::new())
        }
        ExecuteMsg::Withdraw { amount, recipient } => {
            // Step 1: 从外部协议提取
            // Step 2: 转给 recipient
            Ok(Response::new())
        }
        ExecuteMsg::Harvest {} => {
            // Step 1: 领取奖励
            // Step 2: Swap 奖励到 want token
            // Step 3: Re-deposit
            Ok(Response::new())
        }
        _ => Ok(Response::new()),
    }
}

N. 贡献指南

欢迎为 Yield Aggregator 贡献新的策略! 请遵循以下流程:

  1. Fork 仓库并创建新分支
  2. 在 contracts/strategies/ 下创建新策略目录
  3. 实现 Strategy trait (deposit, withdraw, harvest, balance_of)
  4. 编写单元测试和集成测试
  5. 在测试网上部署并验证
  6. 提交 PR 并附带测试结果

测试要求:

O. 已知问题与解决方案

问题 描述 解决方案
舍入误差 份额计算时的小数取舍 使用 full_mul 高精度计算, 最后一个策略吸收余数
Nonce 冲突 批量交易时 nonce 重复 在交易间添加 1-2 秒延迟
奖励代币精度不一致 不同协议奖励代币 decimals 不同 统一转换为 18 decimals 后再计算
策略存款失败 外部协议暂停或限额 实现失败回退机制, 资金暂留 Vault
Gas 预估不足 Harvest 复杂度过高 设置 1.2x - 1.5x 的 Gas bufffer
多策略平衡 资金在不同策略间再平衡 设定目标权重阈值, 偏差超过 5% 时触发再平衡

P. 常见问题解答 (FAQ)

Q: 为什么需要份额代币?
A: 份额代币跟踪用户在 Vault 中的所有权比例。随着 Harvest 产生收益, Vault 总资产增加, 每份额的价值也随之增长。用户持有的份额数量不变, 但每份额的价值增加。

Q: 如何计算 APR?
A: APR 基于份额价格的历史增长计算。公式为 (当前价格 - 30天前的价格) / 30天前的价格 * (365/30)。AGR 会随市场条件变化。

Q: 什么是高水位线 (HWM)?
A: 高水位线机制确保 Performance Fee 只在 Vault 达到历史最高值时收取, 防止对同一部分利润重复收费。如果 Vault 净值下跌, 需要先回到之前的高点才开始重新计费。

Q: 资金安全如何保障?
A: 通过多层暂停机制、策略白名单、时间锁、多签治理和保险基金共同保障。所有合约代码需经过至少两家审计公司的审计。

Q: 如何运行 Harvester 机器人?
A: 参考第 4.5 节的 Python 脚本, 配置钱包助记词和 RPC 地址后运行。建议使用单独的 Keeper 钱包, 并设置 Gas 限额。

Q: 支持哪些策略类型?
A: 当前支持 AMM LP 农场策略和借贷市场策略。社区可以贡献新的策略类型。

Q: 费用如何收取?
A: 绩效费在每次 Harvest 时从收益中直接扣除 (20%), 管理费按年化 2% 线性累积, 取款费在取款时扣除 (3 天后免费)。

Q: 如何迁移到新策略?
A: Owner 通过 migrate_strategy 函数将资金从旧策略提取并存入新策略。建议在迁移前暂停 Vault, 迁移完成后进行余额验证。

本文档基于 MSG Chain 代码库核实的技术事实。
白皮书系统: https://msgchain.org/whitepaper/
链参数: chain-id=msg-chain-1, bech32=msg, decimals=18


文档版本: v1.0.0 | 链: msg-chain-1 | 前缀: msg | 精度: 18 | 技术栈: CosmWasm 1.5 + Rust + React + TypeScript

R. 术语表

术语 英文 说明
金库 Vault 用户存入资产并获得份额的核心合约
策略 Strategy 将资金部署到外部协议以产生收益的模块
基础资产 Want Token Vault 接受和管理的底层代币 (通常为 LP Token)
份额代币 Share Token 代表用户在 Vault 中所有权比例的代币
收割 Harvest 领取收益并复投的操作
复投 Auto-Compound 将收益自动 reinvest 以产生复利效应
绩效费 Performance Fee 从 Harvest 收益中按比例收取的费用
管理费 Management Fee 按时间线性累积的资产管理费用
高水位线 High-Water Mark 防止对同一利润重复收费的机制
滑点 Slippage 交易执行价格与预期价格之间的差异
TVL Total Value Locked 协议中锁定的总资产价值
APR Annual Percentage Rate 年化收益率 (单利)
APY Annual Percentage Yield 年化收益率 (复利)
Keeper Keeper / Bot 自动执行 Harvest 等操作的机器人
时间锁 Timelock 延迟执行管理操作的合约机制
多签 Multi-Sig 需要多个签名才能执行操作的合约

S. 相关资源


本文档为 MSG Chain 收益聚合器的完整实现指南。涵盖合约设计、前端开发、部署脚本、Harvester 机器人等全流程内容。所有代码示例基于 CosmWasm 1.5 + MSG Chain 主网参数。

Copyright 2026 MSG Chain DevKit. MIT License.