收益聚合器 (Yield Aggregator) 实现指南
MSG Chain 自动复投金库完全实现
主网状态: No-Go
1. 概述
1.1 什么是收益聚合器
收益聚合器(Yield Aggregator)是一种自动化资产管理协议,它自动将用户的资产部署到最优收益策略中,并通过自动复投机制持续 compounding 收益。Yearn Finance 是这一领域的先驱,其 Vault 模型已成为 DeFi 收益聚合的标准架构。
收益聚合器的核心价值在于:
- 自动化复投 — 手动领取收益并 reinvest 会产生 Gas 成本和操作复杂度,聚合器自动完成此过程
- 策略优化 — 自动将资金分配到最高收益的协议中
- Gas 聚合 — 多个用户的收益操作合并执行,分摊 Gas 成本
- 降低门槛 — 用户只需存入资产,无需自行管理策略
1.2 核心概念
Vault(金库) — 用户存入资金的核心合约。用户向 Vault 存入基础资产(want token),获得代表其份额的 Vault 份额代币。Vault 负责:
- 管理用户存款和取款
- 分配资金到各个 Strategy
- 收取并分配费用
- 计算份额价格
Strategy(策略) — 具体部署资金到外部协议的模块。每个 Strategy 负责:
- 将资金存入外部协议(如 AMM 农场、借贷市场)
- 定期收割收益(Harvest)
- 将收益兑换为基础资产并 reinvest
- 提供资金余额报告
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 智能合约平台。
关键参数:
- Chain ID:
msg-chain-1 - Bech32 前缀:
msg - 原生代币精度: 18 decimals
- 智能合约: CosmWasm (Rust) + EVM 兼容
- 共识: Tendermint POA
对 Yield Aggregator 的意义:
- CosmWasm 合约支持 -> Rust 编写 Vault 和 Strategy 合约
- IBC 支持 -> 跨链资产策略的可能性
- 低 Gas 费用 -> 高频 Harvest 可行
- 18 decimals -> 高精度份额计算
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 中直接扣除 (推荐)
- 实时反映费用, 份额价格持续增长
- 在 swap 之前从奖励中扣除
模式 B: 在 Vault 层面统一扣除
- 单一实现, 易于审计
- 需要等待 Harvest 完成后再计算
// 模式 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 贡献新的策略! 请遵循以下流程:
- Fork 仓库并创建新分支
- 在 contracts/strategies/ 下创建新策略目录
- 实现 Strategy trait (deposit, withdraw, harvest, balance_of)
- 编写单元测试和集成测试
- 在测试网上部署并验证
- 提交 PR 并附带测试结果
测试要求:
- 单元测试覆盖率 >= 80%
- 集成测试覆盖 deposit -> harvest -> withdraw 完整流程
- 包含异常场景测试 (零金额, 暂停状态, 余额不足等)
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. 相关资源
- CosmWasm 官方文档: https://docs.cosmwasm.com
- CW20 代币标准: https://github.com/CosmWasm/cw-plus
- Yearn Finance Vaults: https://yearn.fi
- MSG Chain 开发者门户: https://docs.msgchain.io
- MSG DevKit CLI: https://github.com/anomalyco/msg-chain-devkit
- CosmJS SDK: https://github.com/cosmos/cosmjs
本文档为 MSG Chain 收益聚合器的完整实现指南。涵盖合约设计、前端开发、部署脚本、Harvester 机器人等全流程内容。所有代码示例基于 CosmWasm 1.5 + MSG Chain 主网参数。
Copyright 2026 MSG Chain DevKit. MIT License.
