基于MSG Chain的过度抵押稳定币协议实现指南
版本: 1.0.0
链ID:msg-chain-1
地址前缀:msg
精度: 18位小数
Gas: 1,000,000,000 attoMSG/gas
出块时间: 5秒
智能合约框架: CosmWasm
主网状态: No-Go
目录
1. 概述
1.1 什么是过度抵押稳定币
过度抵押稳定币(Over-Collateralized Stablecoin)是一种以加密资产作为抵押品、以超额抵押方式发行的价格稳定数字资产。与法币储备支持的稳定币(如USDC、USDT)不同,过度抵押稳定币完全运行在区块链上,无需中心化托管方。
核心原理:用户锁定价值高于发行量的抵押品,通过超额抵押确保稳定币的价值安全。即使抵押品价格波动,系统仍然保持偿付能力。
超额抵押比率:抵押品价值 / 发行的稳定币价值。典型值为150%(1.5倍),即存入价值150美元的抵押品最多发行100美元的稳定币。
1.2 MakerDAO与CDP机制
MakerDAO是以太坊上最知名的过度抵押稳定币协议,其核心机制是抵押债务头寸(Collateralized Debt Position, CDP):
- 用户存入抵押品(如ETH)到CDP合约
- 系统铸造DAI(MakerDAO的稳定币)并发送给用户
- 用户偿还DAI + 稳定费来解锁抵押品
- 清算:当抵押率低于阈值时,任何人都可清算该头寸
CDP的关键参数:
| 参数 | 描述 | 典型值 |
|---|---|---|
| 最低抵押率 | 开仓要求 | 150% |
| 清算阈值 | 触发清算 | 120% |
| 清算罚金 | 清算额外费用 | 13% |
| 稳定费 | 年化借贷利率 | 0.5%-5% |
1.3 MSG Chain稳定币设计
本指南将实现名为 MUSD 的过度抵押稳定币协议,基于MSG Chain的CosmWasm智能合约平台。
核心设计原则
MSG Chain 稳定币协议架构
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +
| 用户界面层 |
| Dashboard · CDP Manager · Swap Interface |
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +
| 合约层 |
| +----------+ +----------+ +------------------+ |
| | CDP引擎 | | 清算管理器 | | 稳定池 | |
| +----------+ +----------+ +------------------+ |
| +----------+ +----------+ +------------------+ |
| | 预言机馈送 | | 工厂合约 | | 治理模块 | |
| +----------+ +----------+ +------------------+ |
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +
| MSG Chain 基础层 |
| CosmWasm · IBC · CometBFT · Dilithium-5 |
| ChainID: msg-chain-1 · bech32: msg |
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +
技术规格
- 抵押品类型: MSG(原生代币)、USDC(通过IBC桥接)
- 稳定币: MUSD(18位精度,与USD锚定)
- 最低抵押率: 150%(1.5倍)
- 清算阈值: 120%(1.2倍)
- 清算罚金: 13%(0.13)
- 全局债务上限: 10,000,000 MUSD
- 稳定费: 每年0.5%-5%(动态调整)
MSG Chain特有优势
- 量子安全: Dilithium-5签名提供后量子安全
- 5秒出块: 快速清算响应
- 低Gas: 0.01-0.04 umsg,降低运营成本
- CosmWasm: 成熟的Rust智能合约框架,天然防重入攻击
1.4 核心组件一览
| 组件 | 合约名称 | 职责 |
|---|---|---|
| 工厂合约 | stablecoin_factory |
部署和管理CDP实例 |
| CDP引擎 | cdp_engine |
管理抵押债务头寸 |
| 预言机馈送 | oracle_feeder |
价格数据源 |
| 清算管理器 | liquidation_manager |
清算不安全头寸 |
| 稳定池 | stability_pool |
用于清算的流动性池 |
1.5 经济模型
MUSD 经济模型
+----------+
| 用户/借款人 |
+-----+----+
|
存入MSG/USDC
铸造MUSD
|
v
+----------+ +--------------+
| CDP引擎 |<----| 预言机价格馈送 |
+-----+----+ +--------------+
|
+-----------+------------+
| | |
v v v
+----------+ +----------+ +----------+
| 稳定费收入 | | 清算罚金 | | 清算获利 |
+----------+ +----------+ +----------+
| | |
+-----------+------------+
|
v
+--------------+
| 协议金库 |
| (DAO治理) |
+--------------+
收入来源:
- 稳定费: 借款人支付的年化利息
- 清算罚金: 被清算头寸的额外费用
- 流动性提供: 稳定池提供者获得清算收益
2. 系统架构
2.1 合约架构
2.1.1 工厂合约 (stablecoin_factory)
工厂合约负责部署和管理所有CDP相关合约。它作为系统的入口点,维护合约注册表。
2.1.2 工厂合约完整代码
// contracts/stablecoin_factory/src/contract.rs
use cosmwasm_std::{
entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo,
Response, StdResult, Addr, CosmosMsg, WasmMsg, Empty,
};
use cw2::set_contract_version;
use crate::error::ContractError;
use crate::msg::{
FactoryExecuteMsg, FactoryQueryMsg, InstantiateMsg, MigrateMsg,
};
use crate::state::{
Config, CdpEngineInfo,
CONFIG, CDP_ENGINES, ENGINE_COUNT,
};
const CONTRACT_NAME: &str = "stablecoin-factory";
const CONTRACT_VERSION: &str = "1.0.0";
#[entry_point]
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
let config = Config {
owner: info.sender.clone(),
fee_collector: deps.api.addr_validate(&msg.fee_collector)?,
treasury: deps.api.addr_validate(&msg.treasury)?,
paused: false,
};
CONFIG.save(deps.storage, &config)?;
ENGINE_COUNT.save(deps.storage, &0u64)?;
Ok(Response::new()
.add_attribute("action", "instantiate")
.add_attribute("owner", info.sender))
}
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: FactoryExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
FactoryExecuteMsg::CreateCdpEngine {
collateral_token,
stable_token,
min_collateral_ratio,
liquidation_ratio,
liquidation_fee,
oracle,
debt_ceiling,
name,
symbol,
code_id,
} => execute_create_cdp_engine(
deps, env, info,
collateral_token, stable_token,
min_collateral_ratio, liquidation_ratio,
liquidation_fee, oracle,
debt_ceiling, name, symbol, code_id,
),
FactoryExecuteMsg::UpdateConfig {
owner,
fee_collector,
treasury,
} => execute_update_config(deps, env, info, owner, fee_collector, treasury),
FactoryExecuteMsg::Pause {} => execute_pause(deps, env, info),
FactoryExecuteMsg::Unpause {} => execute_unpause(deps, env, info),
FactoryExecuteMsg::UpgradeEngine {
engine_id,
new_code_id,
} => execute_upgrade_engine(deps, env, info, engine_id, new_code_id),
}
}
fn execute_create_cdp_engine(
deps: DepsMut,
env: Env,
info: MessageInfo,
collateral_token: String,
stable_token: String,
min_collateral_ratio: String,
liquidation_ratio: String,
liquidation_fee: String,
oracle: String,
debt_ceiling: String,
name: String,
symbol: String,
code_id: u64,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
if info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
if config.paused {
return Err(ContractError::ContractPaused {});
}
let coll_addr = deps.api.addr_validate(&collateral_token)?;
let stable_addr = deps.api.addr_validate(&stable_token)?;
let oracle_addr = deps.api.addr_validate(&oracle)?;
let min_cr: cosmwasm_std::Decimal = min_collateral_ratio.parse()
.map_err(|_| ContractError::InvalidParameter {
param: "min_collateral_ratio".to_string(),
})?;
let liq_r: cosmwasm_std::Decimal = liquidation_ratio.parse()
.map_err(|_| ContractError::InvalidParameter {
param: "liquidation_ratio".to_string(),
})?;
let liq_fee: cosmwasm_std::Decimal = liquidation_fee.parse()
.map_err(|_| ContractError::InvalidParameter {
param: "liquidation_fee".to_string(),
})?;
let ceiling: cosmwasm_std::Uint128 = debt_ceiling.parse()
.map_err(|_| ContractError::InvalidParameter {
param: "debt_ceiling".to_string(),
})?;
if min_cr <= liq_r {
return Err(ContractError::InvalidParameter {
param: "min_collateral_ratio must be > liquidation_ratio".to_string(),
});
}
let engine_id = ENGINE_COUNT.load(deps.storage)? + 1;
let init_msg = cdp_engine::msg::InstantiateMsg {
collateral_token,
stable_token,
min_collateral_ratio,
liquidation_ratio,
liquidation_fee,
oracle,
debt_ceiling: ceiling,
engine_id,
factory: env.contract.address.to_string(),
name: name.clone(),
symbol: symbol.clone(),
};
let init_msg_binary = to_json_binary(&init_msg)?;
let label = format!("cdp-engine-{}-{}", engine_id, name);
let instantiate_msg = WasmMsg::Instantiate {
admin: Some(config.owner.to_string()),
code_id,
msg: init_msg_binary,
funds: vec![],
label,
};
ENGINE_COUNT.save(deps.storage, &engine_id)?;
let engine_info = CdpEngineInfo {
id: engine_id,
contract_address: None,
collateral_token: coll_addr,
stable_token: stable_addr,
oracle: oracle_addr,
min_collateral_ratio: min_cr,
liquidation_ratio: liq_r,
liquidation_fee: liq_fee,
debt_ceiling: ceiling,
active: true,
created_at: env.block.height,
};
CDP_ENGINES.save(deps.storage, engine_id, &engine_info)?;
Ok(Response::new()
.add_message(instantiate_msg)
.add_attribute("action", "create_cdp_engine")
.add_attribute("engine_id", engine_id.to_string()))
}
fn execute_update_config(
deps: DepsMut,
_env: Env,
info: MessageInfo,
owner: Option<String>,
fee_collector: Option<String>,
treasury: Option<String>,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
if let Some(new_owner) = owner {
config.owner = deps.api.addr_validate(&new_owner)?;
}
if let Some(new_fee_collector) = fee_collector {
config.fee_collector = deps.api.addr_validate(&new_fee_collector)?;
}
if let Some(new_treasury) = treasury {
config.treasury = deps.api.addr_validate(&new_treasury)?;
}
CONFIG.save(deps.storage, &config)?;
Ok(Response::new()
.add_attribute("action", "update_config"))
}
fn execute_pause(
deps: DepsMut,
_env: Env,
info: MessageInfo,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
config.paused = true;
CONFIG.save(deps.storage, &config)?;
Ok(Response::new()
.add_attribute("action", "pause"))
}
fn execute_unpause(
deps: DepsMut,
_env: Env,
info: MessageInfo,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
config.paused = false;
CONFIG.save(deps.storage, &config)?;
Ok(Response::new()
.add_attribute("action", "unpause"))
}
fn execute_upgrade_engine(
deps: DepsMut,
_env: Env,
info: MessageInfo,
engine_id: u64,
new_code_id: u64,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
if info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
let info = CDP_ENGINES.load(deps.storage, engine_id)?;
if !info.active {
return Err(ContractError::EngineInactive { engine_id });
}
let migrate_msg = WasmMsg::Migrate {
contract_addr: info.contract_address.unwrap().to_string(),
new_code_id,
msg: to_json_binary(&Empty {})?,
};
Ok(Response::new()
.add_message(migrate_msg)
.add_attribute("action", "upgrade_engine")
.add_attribute("engine_id", engine_id.to_string()))
}
#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: FactoryQueryMsg) -> StdResult<Binary> {
match msg {
FactoryQueryMsg::GetConfig {} => to_json_binary(&CONFIG.load(deps.storage)?),
FactoryQueryMsg::GetEngine { engine_id } => {
to_json_binary(&CDP_ENGINES.load(deps.storage, engine_id)?)
}
FactoryQueryMsg::ListEngines { start, limit } => {
let start = start.unwrap_or(1);
let limit = limit.unwrap_or(10).min(30);
let count = ENGINE_COUNT.load(deps.storage)?;
let mut engines = vec![];
for id in start..=count.min(start + limit - 1) {
if let Ok(engine) = CDP_ENGINES.may_load(deps.storage, id)? {
engines.push(engine);
}
}
to_json_binary(&engines)
}
FactoryQueryMsg::EngineCount {} => {
to_json_binary(&ENGINE_COUNT.load(deps.storage)?)
}
}
}
#[entry_point]
pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
Ok(Response::new())
}
// contracts/stablecoin_factory/src/msg.rs
use cosmwasm_schema::{cw_serde, QueryResponses};
use cosmwasm_std::{Addr, Uint128, Decimal};
#[cw_serde]
pub struct InstantiateMsg {
pub fee_collector: String,
pub treasury: String,
}
#[cw_serde]
pub enum FactoryExecuteMsg {
CreateCdpEngine {
collateral_token: String,
stable_token: String,
min_collateral_ratio: String,
liquidation_ratio: String,
liquidation_fee: String,
oracle: String,
debt_ceiling: String,
name: String,
symbol: String,
code_id: u64,
},
UpdateConfig {
owner: Option<String>,
fee_collector: Option<String>,
treasury: Option<String>,
},
Pause {},
Unpause {},
UpgradeEngine {
engine_id: u64,
new_code_id: u64,
},
}
#[cw_serde]
#[derive(QueryResponses)]
pub enum FactoryQueryMsg {
#[returns(Config)]
GetConfig {},
#[returns(CdpEngineInfo)]
GetEngine { engine_id: u64 },
#[returns(Vec<CdpEngineInfo>)]
ListEngines { start: Option<u64>, limit: Option<u32> },
#[returns(u64)]
EngineCount {},
}
#[cw_serde]
pub struct MigrateMsg {}
#[cw_serde]
pub struct Config {
pub owner: Addr,
pub fee_collector: Addr,
pub treasury: Addr,
pub paused: bool,
}
#[cw_serde]
pub struct CdpEngineInfo {
pub id: u64,
pub contract_address: Option<Addr>,
pub collateral_token: Addr,
pub stable_token: Addr,
pub oracle: Addr,
pub min_collateral_ratio: Decimal,
pub liquidation_ratio: Decimal,
pub liquidation_fee: Decimal,
pub debt_ceiling: Uint128,
pub active: bool,
pub created_at: u64,
}
// contracts/stablecoin_factory/src/error.rs
use cosmwasm_std::StdError;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),
#[error("Unauthorized")]
Unauthorized {},
#[error("Contract is paused")]
ContractPaused {},
#[error("Invalid parameter: {param}")]
InvalidParameter { param: String },
#[error("Engine {engine_id} is inactive")]
EngineInactive { engine_id: u64 },
#[error("Engine {engine_id} not found")]
EngineNotFound { engine_id: u64 },
}
// contracts/stablecoin_factory/src/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 Config {
pub owner: Addr,
pub fee_collector: Addr,
pub treasury: Addr,
pub paused: bool,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct CdpEngineInfo {
pub id: u64,
pub contract_address: Option<Addr>,
pub collateral_token: Addr,
pub stable_token: Addr,
pub oracle: Addr,
pub min_collateral_ratio: Decimal,
pub liquidation_ratio: Decimal,
pub liquidation_fee: Decimal,
pub debt_ceiling: Uint128,
pub active: bool,
pub created_at: u64,
}
pub const CONFIG: Item<Config> = Item::new("config");
pub const CDP_ENGINES: Map<u64, CdpEngineInfo> = Map::new("cdp_engines");
pub const ENGINE_COUNT: Item<u64> = Item::new("engine_count");
2.2 交互流程
2.2.1 系统初始化流程
初始化流程
步骤 1: 部署工厂合约
User --tx--> 部署 stablecoin_factory.wasm
+-- code_id = 1
+-- instantiate: {fee_collector, treasury}
步骤 2: 部署CDP引擎(通过工厂)
Owner --tx--> factory.createCdpEngine{
collateral_token: "umsg",
stable_token: "factory/.../MUSD",
min_collateral_ratio: "1.5",
liquidation_ratio: "1.2",
liquidation_fee: "0.13",
oracle: "msg1oracle...",
debt_ceiling: "10000000000000000000000000"
}
|
v
Factory --msg--> Instantiate CDP Engine Contract
+-- code_id = 2
+-- address: msg1engine...
+-- store in CDP_ENGINES map
步骤 3: 部署预言机馈送
Owner --tx--> 部署 oracle_feeder.wasm
+-- instantiate: {owner, update_interval: 300s}
+-- address: msg1oracle...
步骤 4: 部署稳定池
Owner --tx--> 部署 stability_pool.wasm
+-- instantiate: {cdp_engine, stable_token}
+-- address: msg1pool...
步骤 5: 部署清算管理器
Owner --tx--> 部署 liquidation_manager.wasm
+-- instantiate: {cdp_engine, stability_pool}
+-- address: msg1liquidator...
2.2.2 用户操作流程
用户操作流程
A. 开仓 (Open CDP)
1. Alice 发送 1000 MSG 到 CDP 引擎
2. CDP 引擎查询预言机获取 MSG/USD 价格(假设 $10)
3. 抵押品价值 = 1000 * $10 = $10,000
4. 最大可铸造 = $10,000 / 1.5 = $6,666.67 MUSD
5. Alice 选择铸造 5000 MUSD
6. CDP 引擎铸造 5000 MUSD 并发送给 Alice
7. CDP 记录: {owner: Alice, collateral: 1000 MSG, debt: 5000 MUSD}
B. 加仓 (Deposit More Collateral)
1. Alice 发送 500 MSG 到 CDP 引擎
2. CDP 更新: {collateral: 1500 MSG, debt: 5000 MUSD}
3. 新抵押率 = (1500 * $10) / $5000 = 300%
C. 铸造更多 (Mint More)
1. Alice 请求铸造 2000 MUSD
2. 检查: 抵押率 = (1500 * $10) / $7000 = 214% > 150%
3. CDP 引擎铸造 2000 MUSD 并发送给 Alice
4. CDP 更新: {debt: 7000 MUSD}
D. 还款 (Repay Debt)
1. Alice 发送 3000 MUSD 到 CDP 引擎
2. CDP 引擎销毁发送的 MUSD
3. CDP 更新: {debt: 4000 MUSD}
4. 抵押率 = (1500 * $10) / $4000 = 375%
E. 平仓 (Close CDP)
1. Alice 发送 4000 MUSD 到 CDP 引擎(还清债务 + 稳定费)
2. CDP 引擎销毁所有 MUSD
3. CDP 引擎发送 1500 MSG 返回给 Alice
4. CDP 标记为已关闭
F. 清算 (Liquidation)
1. MSG 价格从 $10 跌至 $6
2. Alice 的 CDP: 抵押率 = (1500 * $6) / $5000 = 180%
3. 仍然 > 120%
4. MSG 价格跌至 $4
5. 抵押率 = (1500 * $4) / $5000 = 120% 触发清算!
6. 清算者 Bob 发送 2500 MUSD 到清算管理器
7. 清算管理器获取抵押品: 1500 * 50% * $4 / $4 = 750 MSG
8. 清算罚金: 750 * 13% = 97.5 MSG 给 Bob
9. CDP 更新: {collateral: 750 MSG, debt: 2500 MUSD}
2.3 合约间通信
合约间调用图
+--------------+ query_price() +--------------+
| Oracle |<-----------------------+ CDP Engine |
| Feeder |--- price_response ---->+ |
+--------------+ +------+-------+
|
+---------------------+----------+
| | |
v v v
+--------------+ +--------------+
| Liquidation | | Stability |
| Manager |<----| Pool |
| |----->| |
| liquidate() | | deposit() |
| redeem() | | withdraw() |
+--------------+ +--------------+
|
v
+--------------+
| CW20 |
| (MUSD Token) |
| mint/burn |
+--------------+
3. CDP引擎合约
3.1 合约结构
// contracts/cdp_engine/src/contract.rs
use cosmwasm_std::{
entry_point, to_json_binary, BankMsg, Binary, Coin, Decimal, Deps,
DepsMut, Env, MessageInfo, Response, StdResult, Uint128, Addr,
CosmosMsg, WasmMsg, from_json,
};
use cw2::set_contract_version;
use crate::error::ContractError;
use crate::msg::{
ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg,
CdpResponse, CdpListResponse, GlobalStatsResponse,
OpenCdpMsg, DepositCollateralMsg, WithdrawCollateralMsg,
MintStableMsg, RepayDebtMsg, CloseCdpMsg, AdjustPositionMsg,
LiquidateMsg,
};
use crate::state::{
Config, Cdp, CdpStatus,
CONFIG, CDPS, CDP_COUNT, TOTAL_DEBT, TOTAL_COLLATERAL,
USER_CDP_INDEX, TOTAL_STABLE_MINTED,
};
const CONTRACT_NAME: &str = "cdp-engine";
const CONTRACT_VERSION: &str = "1.0.0";
const MAX_CDP_PER_USER: u32 = 10;
#[entry_point]
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
let config = Config {
owner: info.sender,
factory: deps.api.addr_validate(&msg.factory)?,
collateral_token: deps.api.addr_validate(&msg.collateral_token)?,
stable_token: deps.api.addr_validate(&msg.stable_token)?,
oracle: deps.api.addr_validate(&msg.oracle)?,
min_collateral_ratio: msg.min_collateral_ratio.parse()
.map_err(|_| ContractError::InvalidParameter {
param: "min_collateral_ratio".to_string(),
})?,
liquidation_ratio: msg.liquidation_ratio.parse()
.map_err(|_| ContractError::InvalidParameter {
param: "liquidation_ratio".to_string(),
})?,
liquidation_fee: msg.liquidation_fee.parse()
.map_err(|_| ContractError::InvalidParameter {
param: "liquidation_fee".to_string(),
})?,
debt_ceiling: msg.debt_ceiling,
engine_id: msg.engine_id,
name: msg.name,
symbol: msg.symbol,
paused: false,
stability_fee_rate: Decimal::percent(1),
last_fee_update: _env.block.time.seconds(),
total_fees_collected: Uint128::zero(),
};
CONFIG.save(deps.storage, &config)?;
CDP_COUNT.save(deps.storage, &0u64)?;
TOTAL_DEBT.save(deps.storage, &Uint128::zero())?;
TOTAL_COLLATERAL.save(deps.storage, &Uint128::zero())?;
TOTAL_STABLE_MINTED.save(deps.storage, &Uint128::zero())?;
Ok(Response::new()
.add_attribute("action", "instantiate")
.add_attribute("engine_id", msg.engine_id.to_string()))
}
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
if config.paused {
return Err(ContractError::ContractPaused {});
}
match msg {
ExecuteMsg::OpenCdp(open_msg) => {
execute_open_cdp(deps, env, info, open_msg)
}
ExecuteMsg::DepositCollateral(deposit_msg) => {
execute_deposit_collateral(deps, env, info, deposit_msg)
}
ExecuteMsg::WithdrawCollateral(withdraw_msg) => {
execute_withdraw_collateral(deps, env, info, withdraw_msg)
}
ExecuteMsg::MintStable(mint_msg) => {
execute_mint_stable(deps, env, info, mint_msg)
}
ExecuteMsg::RepayDebt(repay_msg) => {
execute_repay_debt(deps, env, info, repay_msg)
}
ExecuteMsg::CloseCdp(close_msg) => {
execute_close_cdp(deps, env, info, close_msg)
}
ExecuteMsg::AdjustPosition(adjust_msg) => {
execute_adjust_position(deps, env, info, adjust_msg)
}
ExecuteMsg::Liquidate(liquidate_msg) => {
execute_liquidate(deps, env, info, liquidate_msg)
}
ExecuteMsg::UpdateConfig { .. } => {
execute_update_config(deps, env, info, msg)
}
ExecuteMsg::Pause {} => execute_pause(deps, env, info),
ExecuteMsg::Unpause {} => execute_unpause(deps, env, info),
}
}
3.2 开仓 (Open CDP)
fn execute_open_cdp(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: OpenCdpMsg,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let user_cdps = USER_CDP_INDEX
.prefix(info.sender.as_bytes().to_vec())
.keys(deps.storage, None, None, cosmwasm_std::Order::Ascending)
.count();
if user_cdps >= MAX_CDP_PER_USER as usize {
return Err(ContractError::MaxCdpsPerUser {
max: MAX_CDP_PER_USER,
});
}
let total_debt = TOTAL_DEBT.load(deps.storage)?;
if total_debt.checked_add(msg.debt_amount)? > config.debt_ceiling {
return Err(ContractError::DebtCeilingExceeded {});
}
let collateral_amount = info
.funds
.iter()
.find(|c| c.denom == "umsg")
.map(|c| c.amount)
.unwrap_or(Uint128::zero());
if collateral_amount.is_zero() {
return Err(ContractError::InsufficientCollateral {});
}
let price = query_oracle_price(deps.as_ref(), &config.oracle)?;
if price.is_zero() {
return Err(ContractError::OraclePriceInvalid {});
}
let collateral_value = collateral_amount
.checked_multiply_ratio(price, Uint128::one())?;
let required_collateral_value = msg.debt_amount
.checked_multiply_ratio(config.min_collateral_ratio, Uint128::one())?;
if collateral_value < required_collateral_value {
return Err(ContractError::CollateralRatioTooLow {
current: format_ratio(collateral_value, msg.debt_amount),
required: config.min_collateral_ratio.to_string(),
});
}
// Create CDP
let cdp_id = CDP_COUNT.load(deps.storage)? + 1;
let now = env.block.time.seconds();
let cdp = Cdp {
id: cdp_id,
owner: info.sender.clone(),
collateral: collateral_amount,
debt: msg.debt_amount,
collateral_asset: config.collateral_token.to_string(),
stable_asset: config.stable_token.to_string(),
status: CdpStatus::Active,
created_at: now,
updated_at: now,
last_interest_update: now,
};
CDPS.save(deps.storage, cdp_id, &cdp)?;
CDP_COUNT.save(deps.storage, &cdp_id)?;
TOTAL_DEBT.update(deps.storage, |d| -> Result<_, ContractError> {
Ok(d.checked_add(msg.debt_amount)?)
})?;
TOTAL_COLLATERAL.update(deps.storage, |c| -> Result<_, ContractError> {
Ok(c.checked_add(collateral_amount)?)
})?;
TOTAL_STABLE_MINTED.update(deps.storage, |m| -> Result<_, ContractError> {
Ok(m.checked_add(msg.debt_amount)?)
})?;
let key = (info.sender.as_bytes().to_vec(), cdp_id);
USER_CDP_INDEX.save(deps.storage, key, &true)?;
let mint_msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: config.stable_token.to_string(),
msg: to_json_binary(&cw20::Cw20ExecuteMsg::Mint {
recipient: info.sender.to_string(),
amount: msg.debt_amount,
})?,
funds: vec![],
});
Ok(Response::new()
.add_message(mint_msg)
.add_attribute("action", "open_cdp")
.add_attribute("cdp_id", cdp_id.to_string())
.add_attribute("owner", info.sender.to_string())
.add_attribute("collateral", collateral_amount.to_string())
.add_attribute("debt", msg.debt_amount.to_string()))
}
3.3 调整仓位
fn execute_deposit_collateral(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: DepositCollateralMsg,
) -> Result<Response, ContractError> {
let mut cdp = CDPS.load(deps.storage, msg.cdp_id)?;
if cdp.owner != info.sender {
return Err(ContractError::Unauthorized {});
}
if cdp.status != CdpStatus::Active {
return Err(ContractError::CdpNotActive { cdp_id: msg.cdp_id });
}
let deposit_amount = info
.funds
.iter()
.find(|c| c.denom == "umsg")
.map(|c| c.amount)
.unwrap_or(Uint128::zero());
if deposit_amount.is_zero() {
return Err(ContractError::InsufficientCollateral {});
}
cdp.collateral = cdp.collateral.checked_add(deposit_amount)?;
cdp.updated_at = _env.block.time.seconds();
CDPS.save(deps.storage, msg.cdp_id, &cdp)?;
TOTAL_COLLATERAL.update(deps.storage, |c| -> Result<_, ContractError> {
Ok(c.checked_add(deposit_amount)?)
})?;
Ok(Response::new()
.add_attribute("action", "deposit_collateral")
.add_attribute("cdp_id", msg.cdp_id.to_string())
.add_attribute("deposit_amount", deposit_amount.to_string()))
}
fn execute_withdraw_collateral(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: WithdrawCollateralMsg,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let mut cdp = CDPS.load(deps.storage, msg.cdp_id)?;
if cdp.owner != info.sender {
return Err(ContractError::Unauthorized {});
}
if cdp.status != CdpStatus::Active {
return Err(ContractError::CdpNotActive { cdp_id: msg.cdp_id });
}
if msg.amount > cdp.collateral {
return Err(ContractError::InsufficientCollateral {});
}
let new_collateral = cdp.collateral.checked_sub(msg.amount)?;
if new_collateral.is_zero() && !cdp.debt.is_zero() {
return Err(ContractError::CollateralRatioTooLow {
current: "0".to_string(),
required: config.min_collateral_ratio.to_string(),
});
}
let price = query_oracle_price(deps.as_ref(), &config.oracle)?;
if !new_collateral.is_zero() {
let collateral_value = new_collateral
.checked_multiply_ratio(price, Uint128::one())?;
let ratio = Decimal::from_ratio(collateral_value, cdp.debt);
if ratio < config.min_collateral_ratio {
return Err(ContractError::CollateralRatioTooLow {
current: ratio.to_string(),
required: config.min_collateral_ratio.to_string(),
});
}
}
cdp.collateral = new_collateral;
cdp.updated_at = env.block.time.seconds();
CDPS.save(deps.storage, msg.cdp_id, &cdp)?;
TOTAL_COLLATERAL.update(deps.storage, |c| -> Result<_, ContractError> {
Ok(c.checked_sub(msg.amount)?)
})?;
let withdraw_msg = CosmosMsg::Bank(BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![Coin {
denom: "umsg".to_string(),
amount: msg.amount,
}],
});
Ok(Response::new()
.add_message(withdraw_msg)
.add_attribute("action", "withdraw_collateral")
.add_attribute("cdp_id", msg.cdp_id.to_string())
.add_attribute("withdraw_amount", msg.amount.to_string()))
}
fn execute_mint_stable(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: MintStableMsg,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let mut cdp = CDPS.load(deps.storage, msg.cdp_id)?;
if cdp.owner != info.sender {
return Err(ContractError::Unauthorized {});
}
if cdp.status != CdpStatus::Active {
return Err(ContractError::CdpNotActive { cdp_id: msg.cdp_id });
}
let total_debt = TOTAL_DEBT.load(deps.storage)?;
let new_total_debt = total_debt.checked_add(msg.amount)?;
if new_total_debt > config.debt_ceiling {
return Err(ContractError::DebtCeilingExceeded {});
}
let price = query_oracle_price(deps.as_ref(), &config.oracle)?;
let collateral_value = cdp
.collateral
.checked_multiply_ratio(price, Uint128::one())?;
let new_debt = cdp.debt.checked_add(msg.amount)?;
let ratio = Decimal::from_ratio(collateral_value, new_debt);
if ratio < config.min_collateral_ratio {
return Err(ContractError::CollateralRatioTooLow {
current: ratio.to_string(),
required: config.min_collateral_ratio.to_string(),
});
}
cdp.debt = new_debt;
cdp.updated_at = env.block.time.seconds();
CDPS.save(deps.storage, msg.cdp_id, &cdp)?;
TOTAL_DEBT.update(deps.storage, |d| -> Result<_, ContractError> {
Ok(d.checked_add(msg.amount)?)
})?;
TOTAL_STABLE_MINTED.update(deps.storage, |m| -> Result<_, ContractError> {
Ok(m.checked_add(msg.amount)?)
})?;
let mint_msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: config.stable_token.to_string(),
msg: to_json_binary(&cw20::Cw20ExecuteMsg::Mint {
recipient: info.sender.to_string(),
amount: msg.amount,
})?,
funds: vec![],
});
Ok(Response::new()
.add_message(mint_msg)
.add_attribute("action", "mint_stable")
.add_attribute("cdp_id", msg.cdp_id.to_string())
.add_attribute("amount", msg.amount.to_string()))
}
3.4 还款与平仓
fn execute_repay_debt(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: RepayDebtMsg,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let mut cdp = CDPS.load(deps.storage, msg.cdp_id)?;
if cdp.owner != info.sender {
return Err(ContractError::Unauthorized {});
}
if cdp.status != CdpStatus::Active {
return Err(ContractError::CdpNotActive { cdp_id: msg.cdp_id });
}
let stability_fee = compute_stability_fee(&cdp, &config, env.block.time.seconds());
let total_repay = msg.amount.min(cdp.debt.checked_add(stability_fee)?);
let debt_repay = total_repay.min(cdp.debt);
let fee_repay = total_repay.checked_sub(debt_repay)?;
let cw20_msg = cw20::Cw20ExecuteMsg::TransferFrom {
owner: info.sender.to_string(),
recipient: env.contract.address.to_string(),
amount: total_repay,
};
cdp.debt = cdp.debt.checked_sub(debt_repay)?;
cdp.updated_at = env.block.time.seconds();
cdp.last_interest_update = env.block.time.seconds();
CDPS.save(deps.storage, msg.cdp_id, &cdp)?;
TOTAL_DEBT.update(deps.storage, |d| -> Result<_, ContractError> {
Ok(d.checked_sub(debt_repay)?)
})?;
if !fee_repay.is_zero() {
CONFIG.update(deps.storage, |mut c| -> Result<_, ContractError> {
c.total_fees_collected = c.total_fees_collected.checked_add(fee_repay)?;
Ok(c)
})?;
}
Ok(Response::new()
.add_message(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: config.stable_token.to_string(),
msg: to_json_binary(&cw20_msg)?,
funds: vec![],
}))
.add_attribute("action", "repay_debt")
.add_attribute("cdp_id", msg.cdp_id.to_string())
.add_attribute("repay_amount", debt_repay.to_string())
.add_attribute("fee_amount", fee_repay.to_string()))
}
fn execute_close_cdp(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: CloseCdpMsg,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let mut cdp = CDPS.load(deps.storage, msg.cdp_id)?;
if cdp.owner != info.sender {
return Err(ContractError::Unauthorized {});
}
if cdp.status != CdpStatus::Active {
return Err(ContractError::CdpNotActive { cdp_id: msg.cdp_id });
}
let stability_fee = compute_stability_fee(&cdp, &config, env.block.time.seconds());
let total_repay = cdp.debt.checked_add(stability_fee)?;
let collateral_to_return = cdp.collateral;
let transfer_msg = cw20::Cw20ExecuteMsg::TransferFrom {
owner: info.sender.to_string(),
recipient: env.contract.address.to_string(),
amount: total_repay,
};
cdp.status = CdpStatus::Closed;
cdp.debt = Uint128::zero();
cdp.collateral = Uint128::zero();
cdp.updated_at = env.block.time.seconds();
CDPS.save(deps.storage, msg.cdp_id, &cdp)?;
TOTAL_DEBT.update(deps.storage, |d| -> Result<_, ContractError> {
Ok(d.checked_sub(cdp.debt)?)
})?;
TOTAL_COLLATERAL.update(deps.storage, |c| -> Result<_, ContractError> {
Ok(c.checked_sub(collateral_to_return)?)
})?;
if !stability_fee.is_zero() {
CONFIG.update(deps.storage, |mut c| -> Result<_, ContractError> {
c.total_fees_collected = c.total_fees_collected.checked_add(stability_fee)?;
Ok(c)
})?;
}
let return_msg = CosmosMsg::Bank(BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![Coin {
denom: "umsg".to_string(),
amount: collateral_to_return,
}],
});
Ok(Response::new()
.add_message(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: config.stable_token.to_string(),
msg: to_json_binary(&transfer_msg)?,
funds: vec![],
}))
.add_message(return_msg)
.add_attribute("action", "close_cdp")
.add_attribute("cdp_id", msg.cdp_id.to_string())
.add_attribute("collateral_returned", collateral_to_return.to_string()))
}
3.5 一次性调整仓位
fn execute_adjust_position(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: AdjustPositionMsg,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let mut cdp = CDPS.load(deps.storage, msg.cdp_id)?;
if cdp.owner != info.sender {
return Err(ContractError::Unauthorized {});
}
if cdp.status != CdpStatus::Active {
return Err(ContractError::CdpNotActive { cdp_id: msg.cdp_id });
}
let stability_fee = compute_stability_fee(&cdp, &config, env.block.time.seconds());
let debt_increase = msg.mint_stable.unwrap_or(false);
// Check collateral sufficiency for withdrawal
if cdp.collateral < msg.collateral_delta {
return Err(ContractError::InsufficientCollateral {});
}
// Check debt ceiling
if debt_increase {
let total_debt = TOTAL_DEBT.load(deps.storage)?;
if total_debt.checked_add(msg.debt_delta)? > config.debt_ceiling {
return Err(ContractError::DebtCeilingExceeded {});
}
}
// Update collateral
let new_collateral = cdp.collateral + msg.collateral_delta;
let new_debt = if debt_increase {
cdp.debt.checked_add(msg.debt_delta)?
} else {
cdp.debt.checked_sub(msg.debt_delta)?
};
// Verify collateral ratio
let price = query_oracle_price(deps.as_ref(), &config.oracle)?;
let collateral_value = new_collateral
.checked_multiply_ratio(price, Uint128::one())?;
if !new_debt.is_zero() {
let ratio = Decimal::from_ratio(collateral_value, new_debt);
if ratio < config.min_collateral_ratio {
return Err(ContractError::CollateralRatioTooLow {
current: ratio.to_string(),
required: config.min_collateral_ratio.to_string(),
});
}
}
cdp.collateral = new_collateral;
cdp.debt = new_debt;
cdp.updated_at = env.block.time.seconds();
cdp.last_interest_update = env.block.time.seconds();
CDPS.save(deps.storage, msg.cdp_id, &cdp)?;
TOTAL_COLLATERAL.update(deps.storage, |c| -> Result<_, ContractError> {
if msg.collateral_delta > Uint128::zero() {
Ok(c.checked_add(msg.collateral_delta)?)
} else {
Ok(c.checked_sub(msg.collateral_delta)?)
}
})?;
TOTAL_DEBT.update(deps.storage, |d| -> Result<_, ContractError> {
if debt_increase {
Ok(d.checked_add(msg.debt_delta)?)
} else {
Ok(d.checked_sub(msg.debt_delta)?)
}
})?;
let mut response = Response::new()
.add_attribute("action", "adjust_position")
.add_attribute("cdp_id", msg.cdp_id.to_string());
if debt_increase && !msg.debt_delta.is_zero() {
let mint_msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: config.stable_token.to_string(),
msg: to_json_binary(&cw20::Cw20ExecuteMsg::Mint {
recipient: info.sender.to_string(),
amount: msg.debt_delta,
})?,
funds: vec![],
});
response = response.add_message(mint_msg);
}
Ok(response)
}
3.6 查询功能
#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::GetConfig {} => to_json_binary(&CONFIG.load(deps.storage)?),
QueryMsg::GetCdp { cdp_id } => {
let cdp = CDPS.load(deps.storage, cdp_id)?;
let config = CONFIG.load(deps.storage)?;
let price = query_oracle_price(deps, &config.oracle)
.unwrap_or(Uint128::zero());
let collateral_value = cdp
.collateral
.checked_multiply_ratio(price, Uint128::one())
.unwrap_or(Uint128::zero());
let ratio = if cdp.debt.is_zero() {
Decimal::zero()
} else {
Decimal::from_ratio(collateral_value, cdp.debt)
};
to_json_binary(&CdpResponse {
id: cdp.id,
owner: cdp.owner.to_string(),
collateral: cdp.collateral,
debt: cdp.debt,
collateral_value,
collateral_ratio: ratio,
status: cdp.status,
created_at: cdp.created_at,
updated_at: cdp.updated_at,
collateral_asset: cdp.collateral_asset,
stable_asset: cdp.stable_asset,
})
}
QueryMsg::ListCdpsByOwner { owner, start, limit } => {
let owner_addr = deps.api.addr_validate(&owner)?;
let limit = limit.unwrap_or(10).min(30);
let start = start.unwrap_or(0);
let config = CONFIG.load(deps.storage)?;
let price = query_oracle_price(deps, &config.oracle)
.unwrap_or(Uint128::zero());
let cdps: Vec<CdpResponse> = USER_CDP_INDEX
.prefix(owner_addr.as_bytes().to_vec())
.range(deps.storage, None, None, cosmwasm_std::Order::Ascending)
.skip(start as usize)
.take(limit as usize)
.filter_map(|item| {
item.ok().and_then(|(cdp_id, _)| {
CDPS.load(deps.storage, cdp_id).ok().map(|cdp| {
let cv = cdp.collateral
.checked_multiply_ratio(price, Uint128::one())
.unwrap_or(Uint128::zero());
let r = if cdp.debt.is_zero() {
Decimal::zero()
} else {
Decimal::from_ratio(cv, cdp.debt)
};
CdpResponse {
id: cdp.id,
owner: cdp.owner.to_string(),
collateral: cdp.collateral,
debt: cdp.debt,
collateral_value: cv,
collateral_ratio: r,
status: cdp.status,
created_at: cdp.created_at,
updated_at: cdp.updated_at,
collateral_asset: cdp.collateral_asset,
stable_asset: cdp.stable_asset,
}
})
})
})
.collect();
to_json_binary(&CdpListResponse { cdps })
}
QueryMsg::GetGlobalStats {} => {
let config = CONFIG.load(deps.storage)?;
let total_debt = TOTAL_DEBT.load(deps.storage)?;
let total_collateral = TOTAL_COLLATERAL.load(deps.storage)?;
let total_minted = TOTAL_STABLE_MINTED.load(deps.storage)?;
let cdp_count = CDP_COUNT.load(deps.storage)?;
let price = query_oracle_price(deps, &config.oracle)
.unwrap_or(Uint128::zero());
let total_collateral_value = total_collateral
.checked_multiply_ratio(price, Uint128::one())
.unwrap_or(Uint128::zero());
to_json_binary(&GlobalStatsResponse {
total_debt,
total_collateral,
total_collateral_value,
total_minted,
cdp_count,
debt_ceiling: config.debt_ceiling,
utilization: if config.debt_ceiling.is_zero() {
Decimal::zero()
} else {
Decimal::from_ratio(total_debt, config.debt_ceiling)
},
current_price: price,
min_collateral_ratio: config.min_collateral_ratio,
liquidation_ratio: config.liquidation_ratio,
stability_fee_rate: config.stability_fee_rate,
total_fees_collected: config.total_fees_collected,
})
}
QueryMsg::GetUnsafeCdps { start, limit } => {
let limit = limit.unwrap_or(10).min(30);
let start = start.unwrap_or(1);
let count = CDP_COUNT.load(deps.storage)?;
let config = CONFIG.load(deps.storage)?;
let price = query_oracle_price(deps, &config.oracle)
.unwrap_or(Uint128::zero());
let unsafe_cdps: Vec<CdpResponse> = (start..=count)
.filter_map(|id| {
CDPS.load(deps.storage, id).ok().and_then(|cdp| {
if cdp.status != CdpStatus::Active || cdp.debt.is_zero() {
return None;
}
let cv = cdp.collateral
.checked_multiply_ratio(price, Uint128::one())
.ok()?;
let ratio = Decimal::from_ratio(cv, cdp.debt);
if ratio < config.liquidation_ratio {
Some(CdpResponse {
id: cdp.id,
owner: cdp.owner.to_string(),
collateral: cdp.collateral,
debt: cdp.debt,
collateral_value: cv,
collateral_ratio: ratio,
status: cdp.status,
created_at: cdp.created_at,
updated_at: cdp.updated_at,
collateral_asset: cdp.collateral_asset,
stable_asset: cdp.stable_asset,
})
} else {
None
}
})
})
.take(limit as usize)
.collect();
to_json_binary(&CdpListResponse { cdps: unsafe_cdps })
}
QueryMsg::CheckCollateralRatio { cdp_id } => {
let cdp = CDPS.load(deps.storage, cdp_id)?;
let config = CONFIG.load(deps.storage)?;
let price = query_oracle_price(deps, &config.oracle)
.unwrap_or(Uint128::zero());
let cv = cdp.collateral
.checked_multiply_ratio(price, Uint128::one())
.unwrap_or(Uint128::zero());
let ratio = if cdp.debt.is_zero() {
Decimal::zero()
} else {
Decimal::from_ratio(cv, cdp.debt)
};
let is_safe = ratio >= config.liquidation_ratio;
to_json_binary(&serde_json::json!({
"cdp_id": cdp_id,
"collateral_ratio": ratio.to_string(),
"is_safe": is_safe,
}))
}
}
}
3.7 辅助函数
fn format_ratio(value: Uint128, debt: Uint128) -> String {
if debt.is_zero() {
return "infinity".to_string();
}
Decimal::from_ratio(value, debt).to_string()
}
fn compute_stability_fee(
cdp: &Cdp,
config: &Config,
current_time: u64,
) -> Uint128 {
if cdp.debt.is_zero() || config.stability_fee_rate.is_zero() {
return Uint128::zero();
}
let time_elapsed = current_time.saturating_sub(cdp.last_interest_update);
if time_elapsed == 0 {
return Uint128::zero();
}
let year_seconds = Uint128::from(31_536_000u64);
let elapsed = Uint128::from(time_elapsed);
let fee = cdp
.debt
.checked_multiply_ratio(config.stability_fee_rate, Uint128::one())
.unwrap_or(Uint128::zero())
.checked_multiply_ratio(elapsed, year_seconds)
.unwrap_or(Uint128::zero());
fee
}
fn query_oracle_price(deps: Deps, oracle: &Addr) -> StdResult<Uint128> {
let price_response: oracle_feeder::msg::PriceResponse = deps
.querier
.query_wasm_smart(
oracle.clone(),
&oracle_feeder::msg::QueryMsg::GetPrice {},
)?;
Ok(price_response.price)
}
fn execute_update_config(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
if let ExecuteMsg::UpdateConfig {
owner, min_collateral_ratio, liquidation_ratio,
liquidation_fee, debt_ceiling, oracle, stability_fee_rate,
} = msg {
if let Some(new_owner) = owner {
config.owner = deps.api.addr_validate(&new_owner)?;
}
if let Some(ratio) = min_collateral_ratio {
config.min_collateral_ratio = ratio.parse()
.map_err(|_| ContractError::InvalidParameter {
param: "min_collateral_ratio".to_string(),
})?;
}
if let Some(ratio) = liquidation_ratio {
config.liquidation_ratio = ratio.parse()
.map_err(|_| ContractError::InvalidParameter {
param: "liquidation_ratio".to_string(),
})?;
}
if let Some(fee) = liquidation_fee {
config.liquidation_fee = fee.parse()
.map_err(|_| ContractError::InvalidParameter {
param: "liquidation_fee".to_string(),
})?;
}
if let Some(ceiling) = debt_ceiling {
config.debt_ceiling = ceiling;
}
if let Some(addr) = oracle {
config.oracle = deps.api.addr_validate(&addr)?;
}
if let Some(rate) = stability_fee_rate {
config.stability_fee_rate = rate.parse()
.map_err(|_| ContractError::InvalidParameter {
param: "stability_fee_rate".to_string(),
})?;
}
CONFIG.save(deps.storage, &config)?;
}
Ok(Response::new()
.add_attribute("action", "update_config"))
}
fn execute_pause(
deps: DepsMut,
_env: Env,
info: MessageInfo,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
config.paused = true;
CONFIG.save(deps.storage, &config)?;
Ok(Response::new()
.add_attribute("action", "pause"))
}
fn execute_unpause(
deps: DepsMut,
_env: Env,
info: MessageInfo,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
config.paused = false;
CONFIG.save(deps.storage, &config)?;
Ok(Response::new()
.add_attribute("action", "unpause"))
}
fn execute_liquidate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: LiquidateMsg,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let mut cdp = CDPS.load(deps.storage, msg.cdp_id)?;
if cdp.status != CdpStatus::Active {
return Err(ContractError::CdpNotActive { cdp_id: msg.cdp_id });
}
let price = query_oracle_price(deps.as_ref(), &config.oracle)?;
let collateral_value = cdp.collateral
.checked_multiply_ratio(price, Uint128::one())?;
let ratio = Decimal::from_ratio(collateral_value, cdp.debt);
if ratio >= config.liquidation_ratio {
return Err(ContractError::LiquidationNotRequired {
ratio: ratio.to_string(),
threshold: config.liquidation_ratio.to_string(),
});
}
let debt_to_cover = msg.debt_to_cover.min(cdp.debt);
let collateral_to_seize = debt_to_cover
.checked_multiply_ratio(Uint128::one(), price)?;
let bonus = collateral_to_seize
.checked_multiply_ratio(config.liquidation_fee, Uint128::one())?;
let total_reward = collateral_to_seize.checked_add(bonus)?;
if total_reward > cdp.collateral {
return Err(ContractError::InsufficientCollateral {});
}
cdp.collateral = cdp.collateral.checked_sub(total_reward)?;
cdp.debt = cdp.debt.checked_sub(debt_to_cover)?;
cdp.updated_at = env.block.time.seconds();
CDPS.save(deps.storage, msg.cdp_id, &cdp)?;
TOTAL_DEBT.update(deps.storage, |d| -> Result<_, ContractError> {
Ok(d.checked_sub(debt_to_cover)?)
})?;
TOTAL_COLLATERAL.update(deps.storage, |c| -> Result<_, ContractError> {
Ok(c.checked_sub(total_reward)?)
})?;
Ok(Response::new()
.add_attribute("action", "liquidate")
.add_attribute("cdp_id", msg.cdp_id.to_string())
.add_attribute("liquidator", info.sender.to_string())
.add_attribute("debt_covered", debt_to_cover.to_string())
.add_attribute("collateral_seized", collateral_to_seize.to_string())
.add_attribute("bonus", bonus.to_string())
.add_attribute("total_reward", total_reward.to_string()))
}
3.8 消息与状态类型
// contracts/cdp_engine/src/msg.rs
use cosmwasm_schema::{cw_serde, QueryResponses};
use cosmwasm_std::{Decimal, Uint128};
use crate::state::CdpStatus;
#[cw_serde]
pub struct InstantiateMsg {
pub collateral_token: String,
pub stable_token: String,
pub min_collateral_ratio: String,
pub liquidation_ratio: String,
pub liquidation_fee: String,
pub oracle: String,
pub debt_ceiling: Uint128,
pub engine_id: u64,
pub factory: String,
pub name: String,
pub symbol: String,
}
#[cw_serde]
pub enum ExecuteMsg {
OpenCdp(OpenCdpMsg),
DepositCollateral(DepositCollateralMsg),
WithdrawCollateral(WithdrawCollateralMsg),
MintStable(MintStableMsg),
RepayDebt(RepayDebtMsg),
CloseCdp(CloseCdpMsg),
AdjustPosition(AdjustPositionMsg),
Liquidate(LiquidateMsg),
UpdateConfig {
owner: Option<String>,
min_collateral_ratio: Option<String>,
liquidation_ratio: Option<String>,
liquidation_fee: Option<String>,
debt_ceiling: Option<Uint128>,
oracle: Option<String>,
stability_fee_rate: Option<String>,
},
Pause {},
Unpause {},
}
#[cw_serde]
pub struct OpenCdpMsg {
pub debt_amount: Uint128,
}
#[cw_serde]
pub struct DepositCollateralMsg {
pub cdp_id: u64,
}
#[cw_serde]
pub struct WithdrawCollateralMsg {
pub cdp_id: u64,
pub amount: Uint128,
}
#[cw_serde]
pub struct MintStableMsg {
pub cdp_id: u64,
pub amount: Uint128,
}
#[cw_serde]
pub struct RepayDebtMsg {
pub cdp_id: u64,
pub amount: Uint128,
}
#[cw_serde]
pub struct CloseCdpMsg {
pub cdp_id: u64,
}
#[cw_serde]
pub struct AdjustPositionMsg {
pub cdp_id: u64,
pub collateral_delta: Uint128,
pub debt_delta: Uint128,
pub mint_stable: Option<bool>,
}
#[cw_serde]
pub struct LiquidateMsg {
pub cdp_id: u64,
pub debt_to_cover: Uint128,
}
#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
#[returns(crate::state::Config)]
GetConfig {},
#[returns(CdpResponse)]
GetCdp { cdp_id: u64 },
#[returns(CdpListResponse)]
ListCdpsByOwner { owner: String, start: Option<u64>, limit: Option<u32> },
#[returns(CdpListResponse)]
ListAllCdps { start: Option<u64>, limit: Option<u32> },
#[returns(GlobalStatsResponse)]
GetGlobalStats {},
#[returns(CdpListResponse)]
GetUnsafeCdps { start: Option<u64>, limit: Option<u32> },
#[returns(serde_json::Value)]
CheckCollateralRatio { cdp_id: u64 },
}
#[cw_serde]
pub struct CdpResponse {
pub id: u64,
pub owner: String,
pub collateral: Uint128,
pub debt: Uint128,
pub collateral_value: Uint128,
pub collateral_ratio: Decimal,
pub status: CdpStatus,
pub created_at: u64,
pub updated_at: u64,
pub collateral_asset: String,
pub stable_asset: String,
}
#[cw_serde]
pub struct CdpListResponse {
pub cdps: Vec<CdpResponse>,
}
#[cw_serde]
pub struct GlobalStatsResponse {
pub total_debt: Uint128,
pub total_collateral: Uint128,
pub total_collateral_value: Uint128,
pub total_minted: Uint128,
pub cdp_count: u64,
pub debt_ceiling: Uint128,
pub utilization: Decimal,
pub current_price: Uint128,
pub min_collateral_ratio: Decimal,
pub liquidation_ratio: Decimal,
pub stability_fee_rate: Decimal,
pub total_fees_collected: Uint128,
}
#[cw_serde]
pub struct MigrateMsg {}
// contracts/cdp_engine/src/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 enum CdpStatus {
Active,
Closed,
Liquidated,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
pub owner: Addr,
pub factory: Addr,
pub collateral_token: Addr,
pub stable_token: Addr,
pub oracle: Addr,
pub min_collateral_ratio: Decimal,
pub liquidation_ratio: Decimal,
pub liquidation_fee: Decimal,
pub debt_ceiling: Uint128,
pub engine_id: u64,
pub name: String,
pub symbol: String,
pub paused: bool,
pub stability_fee_rate: Decimal,
pub last_fee_update: u64,
pub total_fees_collected: Uint128,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Cdp {
pub id: u64,
pub owner: Addr,
pub collateral: Uint128,
pub debt: Uint128,
pub collateral_asset: String,
pub stable_asset: String,
pub status: CdpStatus,
pub created_at: u64,
pub updated_at: u64,
pub last_interest_update: u64,
}
pub const CONFIG: Item<Config> = Item::new("config");
pub const CDPS: Map<u64, Cdp> = Map::new("cdps");
pub const CDP_COUNT: Item<u64> = Item::new("cdp_count");
pub const TOTAL_DEBT: Item<Uint128> = Item::new("total_debt");
pub const TOTAL_COLLATERAL: Item<Uint128> = Item::new("total_collateral");
pub const TOTAL_STABLE_MINTED: Item<Uint128> = Item::new("total_stable_minted");
pub const USER_CDP_INDEX: Map<(Vec<u8>, u64), bool> = Map::new("user_cdp_index");
// contracts/cdp_engine/src/error.rs
use cosmwasm_std::{StdError, Uint128};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),
#[error("Unauthorized")]
Unauthorized {},
#[error("Contract is paused")]
ContractPaused {},
#[error("CDP {cdp_id} not found")]
CdpNotFound { cdp_id: u64 },
#[error("CDP {cdp_id} is not active")]
CdpNotActive { cdp_id: u64 },
#[error("Max CDPs per user ({max}) exceeded")]
MaxCdpsPerUser { max: u32 },
#[error("Insufficient collateral")]
InsufficientCollateral {},
#[error("Collateral ratio too low: current {current}, required {required}")]
CollateralRatioTooLow { current: String, required: String },
#[error("Debt ceiling exceeded")]
DebtCeilingExceeded {},
#[error("Insufficient repayment: required {required}, provided {provided}")]
InsufficientRepayment { required: Uint128, provided: Uint128 },
#[error("Invalid parameter: {param}")]
InvalidParameter { param: String },
#[error("Oracle price invalid")]
OraclePriceInvalid {},
#[error("Math overflow")]
Overflow {},
#[error("Liquidation not required: ratio {ratio} is above threshold {threshold}")]
LiquidationNotRequired { ratio: String, threshold: String },
#[error("CDP already fully liquidated")]
AlreadyLiquidated {},
}
impl From<cosmwasm_std::OverflowError> for ContractError {
fn from(_: cosmwasm_std::OverflowError) -> Self {
ContractError::Overflow {}
}
}
3.9 数学工具
// contracts/cdp_engine/src/math.rs
use cosmwasm_std::{Decimal, Uint128};
const DECIMAL_PRECISION: u128 = 1_000_000_000_000_000_000u128;
pub fn multiply_ratio(value: Uint128, numerator: Uint128, denominator: Uint128) -> Uint128 {
if denominator.is_zero() {
return Uint128::zero();
}
Uint128::try_from(
(value.u128() * numerator.u128()) / denominator.u128()
).unwrap_or(Uint128::zero())
}
pub fn calculate_liquidation_price(
debt: Uint128,
collateral: Uint128,
liquidation_ratio: Decimal,
) -> Uint128 {
if collateral.is_zero() {
return Uint128::zero();
}
// liquidation_price = debt * liquidation_ratio / collateral
let liq_ratio_numer = liquidation_ratio.numerator();
let liq_ratio_denom = liquidation_ratio.denominator();
debt.checked_multiply_ratio(liq_ratio_numer, liq_ratio_denom)
.unwrap_or(Uint128::zero())
.checked_div(collateral)
.unwrap_or(Uint128::zero())
}
pub fn calculate_collateral_value(amount: Uint128, price: Uint128) -> Uint128 {
amount.checked_multiply_ratio(price, Uint128::one())
.unwrap_or(Uint128::zero())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_multiply_ratio() {
let result = multiply_ratio(
Uint128::new(1000),
Uint128::new(150),
Uint128::new(100),
);
assert_eq!(result, Uint128::new(1500));
}
#[test]
fn test_calculate_liquidation_price() {
let price = calculate_liquidation_price(
Uint128::new(5000_000_000_000_000_000u128),
Uint128::new(1000_000_000_000_000_000_000u128),
Decimal::percent(120),
);
assert!(price.u128() > 0);
}
#[test]
fn test_edge_cases() {
// Zero denominator
let result = multiply_ratio(
Uint128::new(100), Uint128::new(1), Uint128::zero()
);
assert_eq!(result, Uint128::zero());
// Zero value
let result = multiply_ratio(
Uint128::zero(), Uint128::new(100), Uint128::new(1)
);
assert_eq!(result, Uint128::zero());
}
}
3.10 CDP引擎单元测试
// contracts/cdp_engine/src/tests.rs
#[cfg(test)]
mod tests {
use cosmwasm_std::{
testing::{mock_dependencies, mock_env, mock_info},
from_json, Addr, Coin, Decimal, Uint128,
};
use cw_multi_test::{App, ContractWrapper, Executor, BasicApp};
use crate::{
contract::{instantiate, execute, query},
msg::{
InstantiateMsg, ExecuteMsg, QueryMsg, OpenCdpMsg,
DepositCollateralMsg, WithdrawCollateralMsg,
MintStableMsg, RepayDebtMsg, CloseCdpMsg,
},
state::{Config, Cdp, CdpStatus},
};
fn setup_contract() -> (App, Addr) {
let mut app = BasicApp::new(|router, _, storage| {
router
.bank
.init_balance(
storage,
&Addr::unchecked("user1"),
vec![Coin {
denom: "umsg".to_string(),
amount: Uint128::new(10_000_000_000_000_000_000_000u128),
}],
)
.unwrap();
});
let code = ContractWrapper::new(execute, instantiate, query);
let code_id = app.store_code(Box::new(code));
let msg = InstantiateMsg {
collateral_token: "umsg".to_string(),
stable_token: "factory/msg1stable/MUSD".to_string(),
min_collateral_ratio: "1.5".to_string(),
liquidation_ratio: "1.2".to_string(),
liquidation_fee: "0.13".to_string(),
oracle: "msg1oracle".to_string(),
debt_ceiling: Uint128::new(10_000_000_000_000_000_000_000_000u128),
engine_id: 1,
factory: "msg1factory".to_string(),
name: "MUSD Engine".to_string(),
symbol: "MUSD".to_string(),
};
let addr = app
.instantiate_contract(code_id, Addr::unchecked("owner"), &msg, &[], "CDP", None)
.unwrap();
(app, addr)
}
#[test]
fn test_instantiate() {
let (app, addr) = setup_contract();
let config: Config = app
.wrap()
.query_wasm_smart(addr.clone(), &QueryMsg::GetConfig {})
.unwrap();
assert_eq!(config.owner.to_string(), "owner");
assert_eq!(config.min_collateral_ratio, Decimal::percent(150));
assert_eq!(config.liquidation_ratio, Decimal::percent(120));
assert_eq!(config.liquidation_fee, Decimal::percent(13));
assert!(!config.paused);
assert_eq!(config.engine_id, 1);
}
#[test]
fn test_collateral_ratio_calculation() {
let collateral = Uint128::new(1_000_000_000_000_000_000_000u128);
let price = Uint128::new(10_000_000_000_000_000_000u128);
let debt = Uint128::new(5_000_000_000_000_000_000_000u128);
let collateral_value = collateral
.checked_multiply_ratio(price, Uint128::one())
.unwrap();
assert_eq!(collateral_value, Uint128::new(10_000_000_000_000_000_000_000u128));
let ratio = Decimal::from_ratio(collateral_value, debt);
assert_eq!(ratio, Decimal::from_ratio(2u128, 1u128));
}
#[test]
fn test_liquidation_scenario() {
let initial_collateral = Uint128::new(1_000_000_000_000_000_000_000u128);
let initial_debt = Uint128::new(6_000_000_000_000_000_000_000u128);
// Price at $10 - safe
let price_high = Uint128::new(10_000_000_000_000_000_000u128);
let cv_high = initial_collateral
.checked_multiply_ratio(price_high, Uint128::one())
.unwrap();
let ratio_high = Decimal::from_ratio(cv_high, initial_debt);
assert!(ratio_high > Decimal::percent(120));
// Price at $6 - unsafe (ratio = 100%)
let price_low = Uint128::new(6_000_000_000_000_000_000u128);
let cv_low = initial_collateral
.checked_multiply_ratio(price_low, Uint128::one())
.unwrap();
let ratio_low = Decimal::from_ratio(cv_low, initial_debt);
assert!(ratio_low < Decimal::percent(120));
// 50% partial liquidation
let debt_to_cover = Uint128::new(3_000_000_000_000_000_000_000u128);
let collateral_to_take = debt_to_cover
.checked_multiply_ratio(Uint128::one(), price_low)
.unwrap();
assert_eq!(collateral_to_take, Uint128::new(500_000_000_000_000_000_000u128));
let bonus = collateral_to_take
.checked_multiply_ratio(Decimal::percent(13), Uint128::one())
.unwrap();
assert_eq!(bonus, Uint128::new(65_000_000_000_000_000_000u128));
let remaining_collateral = initial_collateral
.checked_sub(collateral_to_take.checked_add(bonus).unwrap())
.unwrap();
let remaining_debt = initial_debt.checked_sub(debt_to_cover).unwrap();
assert_eq!(remaining_collateral, Uint128::new(435_000_000_000_000_000_000u128));
assert_eq!(remaining_debt, Uint128::new(3_000_000_000_000_000_000_000u128));
}
#[test]
fn test_unauthorized_access() {
let (mut app, addr) = setup_contract();
let update_msg = ExecuteMsg::UpdateConfig {
owner: None,
min_collateral_ratio: Some("2.0".to_string()),
liquidation_ratio: None,
liquidation_fee: None,
debt_ceiling: None,
oracle: None,
stability_fee_rate: None,
};
let result = app.execute_contract(
Addr::unchecked("hacker"),
addr.clone(),
&update_msg,
&[],
);
assert!(result.is_err());
}
#[test]
fn test_global_stats_aggregation() {
let cdps = vec![
(Uint128::new(1000), Uint128::new(5000)),
(Uint128::new(2000), Uint128::new(8000)),
(Uint128::new(500), Uint128::new(2000)),
];
let total_collateral: Uint128 = cdps.iter().map(|(c, _)| *c).sum();
let total_debt: Uint128 = cdps.iter().map(|(_, d)| *d).sum();
assert_eq!(total_collateral, Uint128::new(3500));
assert_eq!(total_debt, Uint128::new(15000));
}
#[test]
fn test_stability_fee_calculation() {
// 1% annual rate on 1000 MUSD for 1 year
let debt = Uint128::new(1000_000_000_000_000_000_000u128);
let rate = Decimal::percent(1);
let year_seconds = Uint128::from(31_536_000u64);
let fees = debt
.checked_multiply_ratio(rate, Uint128::one())
.unwrap()
.checked_multiply_ratio(year_seconds, year_seconds)
.unwrap();
assert_eq!(fees, Uint128::new(10_000_000_000_000_000_000u128));
}
#[test]
fn test_math_precision() {
let small = Uint128::new(1);
let price = Uint128::new(1_234_567_890_123_456_789u128);
let result = small.checked_multiply_ratio(price, Uint128::one()).unwrap();
assert_eq!(result, price);
}
#[test]
fn test_full_cdp_lifecycle_mocked() {
let mut app = BasicApp::new(|router, _, storage| {
router
.bank
.init_balance(
storage,
&Addr::unchecked("user1"),
vec![Coin {
denom: "umsg".to_string(),
amount: Uint128::new(10_000_000_000_000_000_000_000u128),
}],
)
.unwrap();
});
let cdp_code = ContractWrapper::new(execute, instantiate, query);
let cdp_code_id = app.store_code(Box::new(cdp_code));
let msg = InstantiateMsg {
collateral_token: "umsg".to_string(),
stable_token: "factory/msg1stable/MUSD".to_string(),
min_collateral_ratio: "1.5".to_string(),
liquidation_ratio: "1.2".to_string(),
liquidation_fee: "0.13".to_string(),
oracle: "msg1oracle".to_string(),
debt_ceiling: Uint128::new(10_000_000_000_000_000_000_000_000u128),
engine_id: 1,
factory: "msg1factory".to_string(),
name: "MUSD Engine".to_string(),
symbol: "MUSD".to_string(),
};
let cdp_addr = app
.instantiate_contract(cdp_code_id, Addr::unchecked("owner"), &msg, &[], "CDP", None)
.unwrap();
let config: Config = app
.wrap()
.query_wasm_smart(cdp_addr.clone(), &QueryMsg::GetConfig {})
.unwrap();
assert_eq!(config.name, "MUSD Engine");
assert_eq!(config.engine_id, 1);
}
}
3.11 库入口
// contracts/cdp_engine/src/lib.rs
pub mod contract;
pub mod error;
pub mod math;
pub mod msg;
pub mod state;
#[cfg(test)]
mod tests;
4. 清算机制
4.1 清算触发条件
清算是在借款人抵押率低于清算阈值时触发的风险控制机制。
清算触发条件:
抵押率 = 抵押品价值 / 债务金额
条件: 抵押率 < 清算阈值 (e.g., 120%)
示例:
抵押品: 1000 MSG
MSG/USD价格: $6
抵押品价值: 1000 * $6 = $6,000
债务: 5000 MUSD
抵押率: $6,000 / $5,000 = 120%
结果: 触发清算!
4.2 部分清算模型
系统采用部分清算机制,每次清算只处理头寸的一部分(例如50%债务)。
部分清算流程:
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +
| 清算前 |
| CDP #1: 1000 MSG | 5000 MUSD | 比率: 120% |
| |
| 步骤 1: 清算者选择覆盖50%债务 (2500 MUSD) |
| 步骤 2: 清算者发送2500 MUSD到清算合约 |
| 步骤 3: 合约计算应获得的抵押品 |
| 应得抵押品 = 2500 MUSD / $6 = 416.67 MSG |
| 清算奖金 = 416.67 * 13% = 54.17 MSG |
| 总计 = 416.67 + 54.17 = 470.84 MSG |
| |
| 清算后 |
| CDP #1: 529.16 MSG | 2500 MUSD | 比率: 127% |
| (尚未完全安全,可再次清算或由借款人补充) |
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +
4.3 清算管理器合约
// contracts/liquidation_manager/src/contract.rs
use cosmwasm_std::{
entry_point, to_json_binary, Binary, Coin, Decimal, Deps,
DepsMut, Env, MessageInfo, Response, StdResult, Uint128, Addr,
CosmosMsg, WasmMsg,
};
use cw2::set_contract_version;
use crate::error::ContractError;
use crate::msg::{
ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg,
LiquidationEvent,
};
use crate::state::{
Config,
CONFIG, LIQUIDATION_EVENTS, EVENT_COUNT,
TOTAL_COVERED_DEBT, TOTAL_DISTRIBUTED_COLLATERAL,
};
const CONTRACT_NAME: &str = "liquidation-manager";
const CONTRACT_VERSION: &str = "1.0.0";
const MAX_BATCH_SIZE: u32 = 20;
#[entry_point]
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
let config = Config {
owner: info.sender,
cdp_engine: deps.api.addr_validate(&msg.cdp_engine)?,
stability_pool: deps.api.addr_validate(&msg.stability_pool)?,
oracle: deps.api.addr_validate(&msg.oracle)?,
collateral_token: deps.api.addr_validate(&msg.collateral_token)?,
stable_token: deps.api.addr_validate(&msg.stable_token)?,
liquidation_fee: Decimal::percent(13),
liquidation_ratio: Decimal::percent(120),
};
CONFIG.save(deps.storage, &config)?;
EVENT_COUNT.save(deps.storage, &0u64)?;
TOTAL_COVERED_DEBT.save(deps.storage, &Uint128::zero())?;
TOTAL_DISTRIBUTED_COLLATERAL.save(deps.storage, &Uint128::zero())?;
Ok(Response::new()
.add_attribute("action", "instantiate"))
}
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::Liquidate { cdp_id, debt_to_cover, min_collateral_out } => {
execute_liquidate(deps, env, info, cdp_id, debt_to_cover, min_collateral_out)
}
ExecuteMsg::BatchLiquidate { liquidations } => {
execute_batch_liquidate(deps, env, info, liquidations)
}
ExecuteMsg::SetParams { cdp_engine, stability_pool, .. } => {
execute_set_params(deps, env, info, cdp_engine, stability_pool)
}
}
}
fn execute_liquidate(
deps: DepsMut,
env: Env,
info: MessageInfo,
cdp_id: u64,
debt_to_cover: Uint128,
min_collateral_out: Option<Uint128>,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
// Query CDP from engine
let cdp_query = cdp_engine::msg::QueryMsg::GetCdp { cdp_id };
let cdp_response: cdp_engine::msg::CdpResponse = deps
.querier
.query_wasm_smart(config.cdp_engine.clone(), &cdp_query)?;
if cdp_response.status.to_string() != "Active" {
return Err(ContractError::CdpNotActive { cdp_id });
}
if cdp_response.collateral_ratio >= config.liquidation_ratio {
return Err(ContractError::LiquidationNotRequired {
ratio: cdp_response.collateral_ratio.to_string(),
threshold: config.liquidation_ratio.to_string(),
});
}
let actual_debt_to_cover = debt_to_cover.min(cdp_response.debt);
let min_debt = Uint128::new(1_000_000_000_000_000_000u128);
if actual_debt_to_cover < min_debt {
return Err(ContractError::LiquidationTooSmall {
amount: actual_debt_to_cover,
minimum: min_debt,
});
}
// Get price from oracle
let price_response: oracle_feeder::msg::PriceResponse = deps
.querier
.query_wasm_smart(config.oracle.clone(), &oracle_feeder::msg::QueryMsg::GetPrice {})?;
let price = price_response.price;
if price.is_zero() {
return Err(ContractError::OracleStale {});
}
// Calculate collateral to seize
let collateral_to_seize = actual_debt_to_cover
.checked_multiply_ratio(Uint128::one(), price)?;
if collateral_to_seize > cdp_response.collateral {
return Err(ContractError::InsufficientCollateral {});
}
let bonus = collateral_to_seize
.checked_multiply_ratio(config.liquidation_fee, Uint128::one())?;
let total_collateral = collateral_to_seize.checked_add(bonus)?;
if let Some(min_out) = min_collateral_out {
if total_collateral < min_out {
return Err(ContractError::SlippageExceeded {
expected: min_out,
actual: total_collateral,
});
}
}
let event_id = EVENT_COUNT.load(deps.storage)? + 1;
let event = LiquidationEvent {
id: event_id,
cdp_id,
liquidator: info.sender.clone(),
debt_covered: actual_debt_to_cover,
collateral_seized: collateral_to_seize,
bonus,
price_at_liquidation: price,
timestamp: env.block.time.seconds(),
block_height: env.block.height,
};
LIQUIDATION_EVENTS.save(deps.storage, event_id, &event)?;
EVENT_COUNT.save(deps.storage, &event_id)?;
TOTAL_COVERED_DEBT.update(deps.storage, |d| -> Result<_, ContractError> {
Ok(d.checked_add(actual_debt_to_cover)?)
})?;
TOTAL_DISTRIBUTED_COLLATERAL.update(deps.storage, |c| -> Result<_, ContractError> {
Ok(c.checked_add(total_collateral)?)
})?;
Ok(Response::new()
.add_message(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: config.cdp_engine.to_string(),
msg: to_json_binary(&cdp_engine::msg::ExecuteMsg::Liquidate(
cdp_engine::msg::LiquidateMsg {
cdp_id,
debt_to_cover: actual_debt_to_cover,
},
))?,
funds: vec![],
}))
.add_attribute("action", "liquidate")
.add_attribute("cdp_id", cdp_id.to_string())
.add_attribute("liquidator", info.sender.to_string())
.add_attribute("debt_covered", actual_debt_to_cover.to_string())
.add_attribute("collateral_seized", collateral_to_seize.to_string())
.add_attribute("bonus", bonus.to_string()))
}
fn execute_batch_liquidate(
deps: DepsMut,
env: Env,
info: MessageInfo,
liquidations: Vec<(u64, Uint128)>,
) -> Result<Response, ContractError> {
if liquidations.len() > MAX_BATCH_SIZE as usize {
return Err(ContractError::BatchTooLarge {
max: MAX_BATCH_SIZE,
actual: liquidations.len() as u32,
});
}
if liquidations.is_empty() {
return Err(ContractError::EmptyBatch {});
}
let config = CONFIG.load(deps.storage)?;
let mut messages: Vec<CosmosMsg> = vec![];
let mut total_debt = Uint128::zero();
let mut total_collateral = Uint128::zero();
for (cdp_id, debt_to_cover) in &liquidations {
let cdp_response: cdp_engine::msg::CdpResponse = deps
.querier
.query_wasm_smart(
config.cdp_engine.clone(),
&cdp_engine::msg::QueryMsg::GetCdp { cdp_id: *cdp_id },
)?;
if cdp_response.status.to_string() != "Active" { continue; }
if cdp_response.collateral_ratio >= config.liquidation_ratio { continue; }
let actual_debt = (*debt_to_cover).min(cdp_response.debt);
messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: config.cdp_engine.to_string(),
msg: to_json_binary(&cdp_engine::msg::ExecuteMsg::Liquidate(
cdp_engine::msg::LiquidateMsg {
cdp_id: *cdp_id,
debt_to_cover: actual_debt,
},
))?,
funds: vec![],
}));
total_debt = total_debt.checked_add(actual_debt)?;
total_collateral = total_collateral.checked_add(
actual_debt.checked_multiply_ratio(Uint128::one(),
cdp_response.collateral_value.checked_div(cdp_response.collateral).unwrap_or(Uint128::one()))?
)?;
}
if messages.is_empty() {
return Err(ContractError::NoLiquidatablePositions {});
}
Ok(Response::new()
.add_messages(messages)
.add_attribute("action", "batch_liquidate")
.add_attribute("total_debt", total_debt.to_string())
.add_attribute("total_collateral", total_collateral.to_string()))
}
fn execute_set_params(
deps: DepsMut,
_env: Env,
info: MessageInfo,
cdp_engine: Option<String>,
stability_pool: Option<String>,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
if let Some(engine) = cdp_engine {
config.cdp_engine = deps.api.addr_validate(&engine)?;
}
if let Some(pool) = stability_pool {
config.stability_pool = deps.api.addr_validate(&pool)?;
}
CONFIG.save(deps.storage, &config)?;
Ok(Response::new().add_attribute("action", "set_params"))
}
#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::GetConfig {} => to_json_binary(&CONFIG.load(deps.storage)?),
QueryMsg::GetEvent { event_id } => {
to_json_binary(&LIQUIDATION_EVENTS.load(deps.storage, event_id)?)
}
QueryMsg::ListEvents { start, limit } => {
let start = start.unwrap_or(1);
let limit = limit.unwrap_or(10).min(30);
let count = EVENT_COUNT.load(deps.storage)?;
let events: Vec<LiquidationEvent> = (start..=count.min(start + limit as u64 - 1))
.filter_map(|id| LIQUIDATION_EVENTS.may_load(deps.storage, id).ok().flatten())
.collect();
to_json_binary(&events)
}
QueryMsg::GetStats {} => {
to_json_binary(&serde_json::json!({
"total_events": EVENT_COUNT.load(deps.storage)?,
"total_debt_covered": TOTAL_COVERED_DEBT.load(deps.storage)?,
"total_collateral_distributed": TOTAL_DISTRIBUTED_COLLATERAL.load(deps.storage)?,
}))
}
}
}
4.4 清算消息和状态定义
// contracts/liquidation_manager/src/msg.rs
use cosmwasm_schema::{cw_serde, QueryResponses};
use cosmwasm_std::{Addr, Uint128, Decimal};
#[cw_serde]
pub struct InstantiateMsg {
pub cdp_engine: String,
pub stability_pool: String,
pub oracle: String,
pub collateral_token: String,
pub stable_token: String,
}
#[cw_serde]
pub enum ExecuteMsg {
Liquidate { cdp_id: u64, debt_to_cover: Uint128, min_collateral_out: Option<Uint128> },
BatchLiquidate { liquidations: Vec<(u64, Uint128)> },
SetParams { cdp_engine: Option<String>, stability_pool: Option<String>, liquidation_fee: Option<Decimal>, oracle: Option<String>, liquidation_ratio: Option<Decimal> },
}
#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
#[returns(Config)]
GetConfig {},
#[returns(LiquidationEvent)]
GetEvent { event_id: u64 },
#[returns(Vec<LiquidationEvent>)]
ListEvents { start: Option<u64>, limit: Option<u32> },
#[returns(serde_json::Value)]
GetStats {},
}
#[cw_serde]
pub struct Config {
pub owner: Addr,
pub cdp_engine: Addr,
pub stability_pool: Addr,
pub oracle: Addr,
pub collateral_token: Addr,
pub stable_token: Addr,
pub liquidation_fee: Decimal,
pub liquidation_ratio: Decimal,
}
#[cw_serde]
pub struct LiquidationEvent {
pub id: u64,
pub cdp_id: u64,
pub liquidator: Addr,
pub debt_covered: Uint128,
pub collateral_seized: Uint128,
pub bonus: Uint128,
pub price_at_liquidation: Uint128,
pub timestamp: u64,
pub block_height: u64,
}
#[cw_serde]
pub struct MigrateMsg {}
4.5 TypeScript清算脚本
// scripts/liquidate.ts
import { SigningStargateClient } from "@cosmjs/stargate";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
import { calculateFee, GasPrice } from "@cosmjs/stargate";
const CHAIN_ID = "msg-chain-1";
const RPC_ENDPOINT = "https://rpc.msgchain.org";
const PREFIX = "msg";
const GAS_PRICE = GasPrice.fromString("1000000000attoMSG");
const LIQUIDATION_MANAGER = "msg1liquidation...";
const CDP_ENGINE = "msg1engine...";
interface CDP {
id: number;
owner: string;
collateral: string;
debt: string;
collateral_value: string;
collateral_ratio: string;
status: string;
}
interface OraclePrice {
price: string;
timestamp: number;
is_valid: boolean;
}
async function checkUnsafePositions(
client: SigningStargateClient
): Promise<CDP[]> {
const result = await client.queryContractSmart(CDP_ENGINE, {
get_unsafe_cdps: { start: 1, limit: 50 },
});
return result.cdps || [];
}
async function getOraclePrice(client: SigningStargateClient): Promise<string> {
const oracleAddr = "msg1oracle...";
const result: OraclePrice = await client.queryContractSmart(oracleAddr, {
get_price: {},
});
return result.price;
}
async function liquidateCDP(
client: SigningStargateClient,
sender: string,
cdpId: number,
debtToCover: string
): Promise<string> {
const fee = calculateFee(200_000, GAS_PRICE);
const msg = {
typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract",
value: {
sender: sender,
contract: LIQUIDATION_MANAGER,
msg: toBase64({
liquidate: {
cdp_id: cdpId,
debt_to_cover: debtToCover,
min_collateral_out: null,
},
}),
funds: [],
},
};
const result = await client.signAndBroadcast(sender, [msg], fee);
return result.transactionHash;
}
async function autoLiquidateAll(client: SigningStargateClient, sender: string) {
const unsafeCdps = await checkUnsafePositions(client);
const price = await getOraclePrice(client);
console.log(`Found ${unsafeCdps.length} unsafe CDPs`);
console.log(`Current MSG/USD price: ${price}`);
for (const cdp of unsafeCdps) {
const debtToCover = BigInt(cdp.debt) / 2n;
console.log(
`Liquidating CDP #${cdp.id}: covering ${debtToCover} MUSD`
);
try {
const txHash = await liquidateCDP(
client, sender, cdp.id, debtToCover.toString()
);
console.log(`Success! TX: ${txHash}`);
} catch (err) {
console.error(`Failed to liquidate CDP #${cdp.id}:`, err);
}
}
}
// Main execution
async function main() {
const mnemonic = process.env.MNEMONIC || "";
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, {
prefix: PREFIX,
});
const client = await SigningStargateClient.connectWithSigner(
RPC_ENDPOINT,
wallet
);
const [account] = await wallet.getAccounts();
console.log(`Liquidator: ${account.address}`);
await autoLiquidateAll(client, account.address);
}
main().catch(console.error);
4.6 稳定池合约
// contracts/stability_pool/src/contract.rs
use cosmwasm_std::{
entry_point, to_json_binary, Binary, Coin, Deps, DepsMut, Env,
MessageInfo, Response, StdResult, Uint128, Addr, CosmosMsg, WasmMsg,
BankMsg, Order,
};
use cw2::set_contract_version;
use crate::error::ContractError;
use crate::msg::{
ExecuteMsg, InstantiateMsg, QueryMsg, PoolInfoResponse,
};
use crate::state::{
Config, UserDeposit,
CONFIG, DEPOSITS, TOTAL_DEPOSITS, USER_DEPOSIT_INDEX,
DEPOSIT_COUNT,
};
#[entry_point]
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
set_contract_version(deps.storage, "stability-pool", "1.0.0")?;
CONFIG.save(deps.storage, &Config {
owner: info.sender,
cdp_engine: deps.api.addr_validate(&msg.cdp_engine)?,
liquidation_manager: deps.api.addr_validate(&msg.liquidation_manager)?,
stable_token: deps.api.addr_validate(&msg.stable_token)?,
collateral_token: deps.api.addr_validate(&msg.collateral_token)?,
})?;
TOTAL_DEPOSITS.save(deps.storage, &Uint128::zero())?;
DEPOSIT_COUNT.save(deps.storage, &0u64)?;
Ok(Response::new().add_attribute("action", "instantiate"))
}
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::Deposit { amount } => execute_deposit(deps, env, info, amount),
ExecuteMsg::Withdraw { deposit_id, amount } => {
execute_withdraw(deps, env, info, deposit_id, amount)
}
ExecuteMsg::ClaimCollateral { deposit_id, cdp_id } => {
execute_claim_collateral(deps, env, info, deposit_id, cdp_id)
}
}
}
fn execute_deposit(
deps: DepsMut,
env: Env,
info: MessageInfo,
amount: Uint128,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let sent = info.funds.iter()
.find(|c| c.denom == config.stable_token.as_str())
.map(|c| c.amount)
.unwrap_or(Uint128::zero());
if sent < amount {
return Err(ContractError::InsufficientDeposit {
expected: amount, received: sent,
});
}
let deposit_id = DEPOSIT_COUNT.load(deps.storage)? + 1;
DEPOSITS.save(deps.storage, deposit_id, &UserDeposit {
id: deposit_id,
depositor: info.sender.clone(),
amount,
collateral_claimed: Uint128::zero(),
deposit_time: env.block.time.seconds(),
last_update: env.block.time.seconds(),
})?;
DEPOSIT_COUNT.save(deps.storage, &deposit_id)?;
let key = (info.sender.as_bytes().to_vec(), deposit_id);
USER_DEPOSIT_INDEX.save(deps.storage, key, &true)?;
TOTAL_DEPOSITS.update(deps.storage, |t| -> Result<_, ContractError> {
Ok(t.checked_add(amount)?)
})?;
Ok(Response::new()
.add_attribute("action", "deposit")
.add_attribute("deposit_id", deposit_id.to_string())
.add_attribute("amount", amount.to_string()))
}
fn execute_withdraw(
deps: DepsMut,
env: Env,
info: MessageInfo,
deposit_id: u64,
amount: Uint128,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let mut deposit = DEPOSITS.load(deps.storage, deposit_id)?;
if deposit.depositor != info.sender {
return Err(ContractError::Unauthorized {});
}
if amount > deposit.amount {
return Err(ContractError::InsufficientBalance {
balance: deposit.amount, requested: amount,
});
}
deposit.amount = deposit.amount.checked_sub(amount)?;
deposit.last_update = env.block.time.seconds();
if deposit.amount.is_zero() {
DEPOSITS.remove(deps.storage, deposit_id);
} else {
DEPOSITS.save(deps.storage, deposit_id, &deposit)?;
}
TOTAL_DEPOSITS.update(deps.storage, |t| -> Result<_, ContractError> {
Ok(t.checked_sub(amount)?)
})?;
Ok(Response::new()
.add_message(CosmosMsg::Bank(BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![Coin {
denom: config.stable_token.to_string(),
amount,
}],
}))
.add_attribute("action", "withdraw")
.add_attribute("amount", amount.to_string()))
}
fn execute_claim_collateral(
deps: DepsMut,
_env: Env,
info: MessageInfo,
deposit_id: u64,
cdp_id: u64,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let mut deposit = DEPOSITS.load(deps.storage, deposit_id)?;
if deposit.depositor != info.sender {
return Err(ContractError::Unauthorized {});
}
let total_pool = TOTAL_DEPOSITS.load(deps.storage)?;
let share = Decimal::from_ratio(deposit.amount, total_pool);
let total_deposits_snapshot = deposit.amount;
// Simplified: claim based on share of total deposits
let claim_amount = total_deposits_snapshot
.checked_multiply_ratio(share, Uint128::one())?;
deposit.collateral_claimed = deposit.collateral_claimed.checked_add(claim_amount)?;
DEPOSITS.save(deps.storage, deposit_id, &deposit)?;
Ok(Response::new()
.add_message(CosmosMsg::Bank(BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![Coin {
denom: "umsg".to_string(),
amount: claim_amount,
}],
}))
.add_attribute("action", "claim_collateral")
.add_attribute("amount", claim_amount.to_string()))
}
#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::GetPoolInfo {} => {
let total = TOTAL_DEPOSITS.load(deps.storage)?;
to_json_binary(&PoolInfoResponse {
total_deposits: total,
depositor_count: DEPOSIT_COUNT.load(deps.storage)?,
})
}
QueryMsg::GetUserDeposits { user } => {
let user_addr = deps.api.addr_validate(&user)?;
let deposits: Vec<UserDeposit> = USER_DEPOSIT_INDEX
.prefix(user_addr.as_bytes().to_vec())
.range(deps.storage, None, None, Order::Ascending)
.filter_map(|item| {
item.ok().and_then(|(id, _)| {
DEPOSITS.may_load(deps.storage, id).ok().flatten()
})
})
.collect();
to_json_binary(&deposits)
}
}
}
5. 预言机集成
5.1 Oracle价格馈送
预言机是CDP系统的核心组件,提供准确的资产价格数据以确保系统安全。
5.1.1 Oracle合约
// contracts/oracle_feeder/src/contract.rs
use cosmwasm_std::{
entry_point, to_json_binary, Binary, Deps, DepsMut, Env,
MessageInfo, Response, StdResult, Uint128, Addr,
};
use cw2::set_contract_version;
use crate::error::ContractError;
use crate::msg::{
ExecuteMsg, InstantiateMsg, QueryMsg, PriceResponse,
SourceConfig,
};
use crate::state::{
Config, PriceSource, PriceInfo,
CONFIG, PRICE_SOURCES, CURRENT_PRICE, LAST_UPDATE,
PRICE_HISTORY, HISTORY_INDEX,
};
const CONTRACT_NAME: &str = "oracle-feeder";
const CONTRACT_VERSION: &str = "1.0.0";
const MAX_SOURCES: u32 = 10;
const MAX_HISTORY: u64 = 1000;
const STALE_PRICE_THRESHOLD: u64 = 3600; // 1 hour
#[entry_point]
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
CONFIG.save(deps.storage, &Config {
owner: info.sender,
paused: false,
min_sources: msg.min_sources,
deviation_threshold: msg.deviation_threshold,
staleness_threshold: msg.staleness_threshold,
})?;
CURRENT_PRICE.save(deps.storage, &PriceInfo {
price: Uint128::zero(),
timestamp: 0,
source_count: 0,
round_id: 0,
})?;
LAST_UPDATE.save(deps.storage, &0u64)?;
HISTORY_INDEX.save(deps.storage, &0u64)?;
Ok(Response::new()
.add_attribute("action", "instantiate"))
}
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::SubmitPrice { asset, price, source } => {
execute_submit_price(deps, env, info, asset, price, source)
}
ExecuteMsg::AddSource { name, address, weight } => {
execute_add_source(deps, env, info, name, address, weight)
}
ExecuteMsg::RemoveSource { name } => {
execute_remove_source(deps, env, info, name)
}
ExecuteMsg::Pause {} => execute_pause(deps, env, info),
ExecuteMsg::Unpause {} => execute_unpause(deps, env, info),
}
}
fn execute_submit_price(
deps: DepsMut,
env: Env,
info: MessageInfo,
asset: String,
price: Uint128,
source: String,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
if config.paused {
return Err(ContractError::ContractPaused {});
}
// Verify source is authorized
let authorized = PRICE_SOURCES
.range(deps.storage, None, None, cosmwasm_std::Order::Ascending)
.any(|item| {
item.map(|(_, s)| s.address == info.sender).unwrap_or(false)
});
if !authorized && info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
// Store price submission
let now = env.block.time.seconds();
let submission = PriceSubmission {
source: source.clone(),
price,
timestamp: now,
submitter: info.sender.clone(),
};
PRICE_SUBMISSIONS.save(deps.storage, (&asset, &source), &submission)?;
// Try to compute median
let sources: Vec<Uint128> = PRICE_SOURCES
.range(deps.storage, None, None, cosmwasm_std::Order::Ascending)
.filter_map(|item| item.ok())
.filter_map(|(_, s)| {
PRICE_SUBMISSIONS
.may_load(deps.storage, (&asset, &s.name))
.ok()
.flatten()
.filter(|sub| {
now.saturating_sub(sub.timestamp) < config.staleness_threshold
})
.map(|sub| sub.price)
})
.collect();
if sources.len() < config.min_sources as usize {
return Ok(Response::new()
.add_attribute("action", "price_submitted")
.add_attribute("sources_count", sources.len().to_string())
.add_attribute("status", "waiting_for_more_sources"));
}
// Compute median price
let median_price = compute_median(&sources);
// Check deviation
let current = CURRENT_PRICE.load(deps.storage)?;
if !current.price.is_zero() {
let deviation = if current.price > median_price {
Decimal::from_ratio(
current.price.checked_sub(median_price)?,
current.price,
)
} else {
Decimal::from_ratio(
median_price.checked_sub(current.price)?,
median_price,
)
};
if deviation > config.deviation_threshold {
return Err(ContractError::PriceDeviationExceeded {
current: current.price,
new: median_price,
deviation,
threshold: config.deviation_threshold,
});
}
}
// Update current price
let round_id = current.round_id + 1;
let price_info = PriceInfo {
price: median_price,
timestamp: now,
source_count: sources.len() as u32,
round_id,
};
CURRENT_PRICE.save(deps.storage, &price_info)?;
LAST_UPDATE.save(deps.storage, &now)?;
// Store in history
let history_idx = HISTORY_INDEX.load(deps.storage)? + 1;
PRICE_HISTORY.save(deps.storage, history_idx, &price_info)?;
HISTORY_INDEX.save(deps.storage, &history_idx)?;
// Trim old history
if history_idx > MAX_HISTORY {
PRICE_HISTORY.remove(deps.storage, history_idx - MAX_HISTORY);
}
Ok(Response::new()
.add_attribute("action", "price_updated")
.add_attribute("asset", &asset)
.add_attribute("price", median_price.to_string())
.add_attribute("round", round_id.to_string())
.add_attribute("sources", sources.len().to_string()))
}
fn compute_median(prices: &[Uint128]) -> Uint128 {
if prices.is_empty() {
return Uint128::zero();
}
let mut sorted = prices.to_vec();
sorted.sort();
let mid = sorted.len() / 2;
if sorted.len() % 2 == 0 {
sorted[mid - 1].checked_add(sorted[mid])
.map(|s| s.checked_div(Uint128::from(2u128)).unwrap_or(sorted[mid]))
.unwrap_or(sorted[mid])
} else {
sorted[mid]
}
}
fn execute_add_source(
deps: DepsMut,
_env: Env,
info: MessageInfo,
name: String,
address: String,
weight: u32,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
if info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
let count = PRICE_SOURCES
.range(deps.storage, None, None, cosmwasm_std::Order::Ascending)
.count();
if count >= MAX_SOURCES as usize {
return Err(ContractError::MaxSourcesExceeded { max: MAX_SOURCES });
}
PRICE_SOURCES.save(deps.storage, &name, &PriceSource {
name: name.clone(),
address: deps.api.addr_validate(&address)?,
weight,
active: true,
added_at: _env.block.time.seconds(),
})?;
Ok(Response::new()
.add_attribute("action", "add_source")
.add_attribute("name", &name))
}
fn execute_remove_source(
deps: DepsMut,
_env: Env,
info: MessageInfo,
name: String,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
if info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
PRICE_SOURCES.remove(deps.storage, &name);
Ok(Response::new()
.add_attribute("action", "remove_source")
.add_attribute("name", &name))
}
fn execute_pause(
deps: DepsMut,
_env: Env,
info: MessageInfo,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
config.paused = true;
CONFIG.save(deps.storage, &config)?;
Ok(Response::new().add_attribute("action", "pause"))
}
fn execute_unpause(
deps: DepsMut,
_env: Env,
info: MessageInfo,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
config.paused = false;
CONFIG.save(deps.storage, &config)?;
Ok(Response::new().add_attribute("action", "unpause"))
}
#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::GetPrice {} => {
let info = CURRENT_PRICE.load(deps.storage)?;
to_json_binary(&PriceResponse {
price: info.price,
timestamp: info.timestamp,
round_id: info.round_id,
is_valid: !info.price.is_zero()
&& info.timestamp > 0,
})
}
QueryMsg::GetConfig {} => to_json_binary(&CONFIG.load(deps.storage)?),
QueryMsg::GetSource { name } => {
to_json_binary(&PRICE_SOURCES.load(deps.storage, &name)?)
}
QueryMsg::ListSources {} => {
let sources: Vec<PriceSource> = PRICE_SOURCES
.range(deps.storage, None, None, cosmwasm_std::Order::Ascending)
.filter_map(|item| item.ok().map(|(_, s)| s))
.collect();
to_json_binary(&sources)
}
QueryMsg::GetHistory { start, limit } => {
let limit = limit.unwrap_or(10).min(100);
let history: Vec<PriceInfo> = PRICE_HISTORY
.range(
deps.storage,
None,
None,
cosmwasm_std::Order::Descending,
)
.take(limit as usize)
.filter_map(|item| item.ok().map(|(_, p)| p))
.collect();
to_json_binary(&history)
}
}
}
// contracts/oracle_feeder/src/msg.rs
use cosmwasm_schema::{cw_serde, QueryResponses};
use cosmwasm_std::{Uint128, Decimal};
#[cw_serde]
pub struct InstantiateMsg {
pub min_sources: u32,
pub deviation_threshold: Decimal,
pub staleness_threshold: u64,
}
#[cw_serde]
pub enum ExecuteMsg {
SubmitPrice { asset: String, price: Uint128, source: String },
AddSource { name: String, address: String, weight: u32 },
RemoveSource { name: String },
Pause {},
Unpause {},
}
#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
#[returns(PriceResponse)]
GetPrice {},
#[returns(Config)]
GetConfig {},
#[returns(PriceSource)]
GetSource { name: String },
#[returns(Vec<PriceSource>)]
ListSources {},
#[returns(Vec<PriceInfo>)]
GetHistory { start: Option<u64>, limit: Option<u32> },
}
#[cw_serde]
pub struct PriceResponse {
pub price: Uint128,
pub timestamp: u64,
pub round_id: u64,
pub is_valid: bool,
}
#[cw_serde]
pub struct PriceSource {
pub name: String,
pub address: String,
pub weight: u32,
pub active: bool,
}
#[cw_serde]
pub struct PriceInfo {
pub price: Uint128,
pub timestamp: u64,
pub source_count: u32,
pub round_id: u64,
}
#[cw_serde]
pub struct MigrateMsg {}
5.2 Python价格馈送客户端
# scripts/oracle_feeder.py
import json
import time
import hmac
import hashlib
import requests
from typing import Optional
from dataclasses import dataclass
from cosmos_sdk.client.lcd import LCDClient
from cosmos_sdk.key.mnemonic import MnemonicKey
from cosmos_sdk.core.wasm import MsgExecuteContract
MSG_CHAIN_RPC = "https://rpc.msgchain.org"
MSG_CHAIN_LCD = "https://lcd.msgchain.org"
CHAIN_ID = "msg-chain-1"
GAS_PRICE = "1000000000attoMSG"
GAS_ADJUSTMENT = 1.3
ORACLE_CONTRACT = "msg1oracle..."
ORACLE_SOURCES = {
"coingecko": "https://api.coingecko.com/api/v3/simple/price?ids=msg-chain&vs_currencies=usd",
"binance": "https://api.binance.com/api/v3/ticker/price?symbol=MSGUSDT",
"agent_api": "http://localhost:8080/oracle/price",
}
@dataclass
class OracleConfig:
mnemonic: str
source_name: str
asset: str = "msg"
update_interval: int = 300 # 5 minutes
max_deviation: float = 0.05 # 5%
class PriceFeeder:
def __init__(self, config: OracleConfig):
self.config = config
mk = MnemonicKey(mnemonic=config.mnemonic)
self.client = LCDClient(
url=MSG_CHAIN_LCD,
chain_id=CHAIN_ID,
gas_prices=GAS_PRICE,
gas_adjustment=GAS_ADJUSTMENT,
)
self.wallet = self.client.wallet(mk)
def fetch_price_coingecko(self) -> Optional[float]:
try:
resp = requests.get(ORACLE_SOURCES["coingecko"], timeout=10)
data = resp.json()
return data.get("msg-chain", {}).get("usd")
except Exception as e:
print(f"CoinGecko error: {e}")
return None
def fetch_price_binance(self) -> Optional[float]:
try:
resp = requests.get(ORACLE_SOURCES["binance"], timeout=10)
data = resp.json()
return float(data.get("price", 0))
except Exception as e:
print(f"Binance error: {e}")
return None
def fetch_price_agent_api(self) -> Optional[float]:
try:
resp = requests.get(ORACLE_SOURCES["agent_api"], timeout=10)
data = resp.json()
return float(data.get("price", 0))
except Exception as e:
print(f"Agent API error: {e}")
return None
def fetch_median_price(self) -> Optional[float]:
prices = []
for fetcher in [
self.fetch_price_coingecko,
self.fetch_price_binance,
self.fetch_price_agent_api,
]:
price = fetcher()
if price and price > 0:
prices.append(price)
if len(prices) < 2:
print(f"Not enough price sources: {len(prices)}")
return None
prices.sort()
mid = len(prices) // 2
if len(prices) % 2 == 0:
return (prices[mid - 1] + prices[mid]) / 2
return prices[mid]
def submit_price(self, price: float) -> str:
price_atomic = int(price * 10**18)
msg = MsgExecuteContract(
sender=self.wallet.key.acc_address,
contract=ORACLE_CONTRACT,
msg=json.dumps({
"submit_price": {
"asset": self.config.asset,
"price": str(price_atomic),
"source": self.config.source_name,
}
}),
funds=[],
)
tx = self.wallet.create_and_sign_tx(
msgs=[msg],
memo=f"oracle-price-{self.config.source_name}",
)
result = self.client.tx.broadcast(tx)
return result.txhash
def run(self):
print(f"Starting price feeder: {self.config.source_name}")
print(f"Asset: {self.config.asset}")
print(f"Update interval: {self.config.update_interval}s")
while True:
try:
price = self.fetch_median_price()
if price:
print(f"[{time.ctime()}] Median price: ${price:.4f}")
tx_hash = self.submit_price(price)
print(f" Submitted. TX: {tx_hash}")
else:
print(f"[{time.ctime()}] Failed to fetch price")
except Exception as e:
print(f"Error: {e}")
time.sleep(self.config.update_interval)
if __name__ == "__main__":
import os
config = OracleConfig(
mnemonic=os.environ.get("ORACLE_MNEMONIC", ""),
source_name=os.environ.get("SOURCE_NAME", "feeder-1"),
update_interval=int(os.environ.get("UPDATE_INTERVAL", "300")),
)
feeder = PriceFeeder(config)
feeder.run()
5.3 TypeScript价格馈送客户端
// scripts/oracleFeeder.ts
import { SigningStargateClient } from "@cosmjs/stargate";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
import { calculateFee, GasPrice } from "@cosmjs/stargate";
const GAS_PRICE = GasPrice.fromString("1000000000attoMSG");
const ORACLE_CONTRACT = "msg1oracle...";
interface PriceSource {
name: string;
fetchPrice(): Promise<number>;
}
class CoinGeckoSource implements PriceSource {
name = "coingecko";
async fetchPrice(): Promise<number> {
const resp = await fetch(
"https://api.coingecko.com/api/v3/simple/price?ids=msg-chain&vs_currencies=usd"
);
const data = await resp.json();
return data["msg-chain"]?.usd || 0;
}
}
class AgentAPISource implements PriceSource {
name = "agent_api";
async fetchPrice(): Promise<number> {
const resp = await fetch("http://localhost:8080/oracle/price");
const data = await resp.json();
return data.price || 0;
}
}
function computeMedian(prices: number[]): number {
const sorted = [...prices].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 === 0
? (sorted[mid - 1] + sorted[mid]) / 2
: sorted[mid];
}
async function oracleFeederLoop() {
const mnemonic = process.env.ORACLE_MNEMONIC || "";
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, {
prefix: "msg",
});
const client = await SigningStargateClient.connectWithSigner(
"https://rpc.msgchain.org",
wallet
);
const [account] = await wallet.getAccounts();
const sources: PriceSource[] = [new CoinGeckoSource(), new AgentAPISource()];
const INTERVAL_MS = 300_000; // 5 minutes
async function submitRound() {
const prices: { source: string; price: number }[] = [];
for (const source of sources) {
try {
const price = await source.fetchPrice();
if (price > 0) {
prices.push({ source: source.name, price });
console.log(`[${source.name}] $${price}`);
}
} catch (e) {
console.error(`[${source.name}] Error:`, e);
}
}
if (prices.length < 2) return;
const medianPrice = computeMedian(prices.map((p) => p.price));
const priceAtomic = BigInt(Math.floor(medianPrice * 1e18)).toString();
console.log(`Median price: $${medianPrice}, atomic: ${priceAtomic}`);
const fee = calculateFee(300_000, GAS_PRICE);
const msg = {
typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract",
value: {
sender: account.address,
contract: ORACLE_CONTRACT,
msg: Buffer.from(
JSON.stringify({
submit_price: {
asset: "msg",
price: priceAtomic,
source: "feeder-ts",
},
})
).toString("base64"),
funds: [],
},
};
const result = await client.signAndBroadcast(account.address, [msg], fee);
console.log(`Submitted. TX: ${result.transactionHash}`);
}
console.log("Starting oracle feeder loop...");
await submitRound();
setInterval(submitRound, INTERVAL_MS);
}
oracleFeederLoop().catch(console.error);
5.4 Agent API集成
// scripts/agentOracleQuery.ts
async function queryAgentOraclePrice(): Promise<{
price: number;
timestamp: number;
source: string;
}> {
const resp = await fetch("http://localhost:8080/oracle/price", {
headers: { Authorization: `Bearer ${process.env.AGENT_API_KEY}` },
});
if (!resp.ok) throw new Error(`Agent API error: ${resp.status}`);
return resp.json();
}
async function submitToOnChain(
price: number,
sender: string,
client: SigningStargateClient
) {
const oracleAddr = "msg1oracle...";
const priceAtomic = BigInt(Math.floor(price * 1e18)).toString();
const fee = calculateFee(200_000, GasPrice.fromString("1000000000attoMSG"));
const msg = {
typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract",
value: {
sender,
contract: oracleAddr,
msg: Buffer.from(
JSON.stringify({
submit_price: {
asset: "msg",
price: priceAtomic,
source: "agent-api",
},
})
).toString("base64"),
funds: [],
},
};
const result = await client.signAndBroadcast(sender, [msg], fee);
return result.transactionHash;
}
6. 稳定币交易
6.1 铸造与销毁
MUSD的铸造和销毁通过CDP引擎完成。当用户开仓时铸造MUSD,还款时销毁MUSD。
MUSD生命周期:
铸造 (Mint):
用户存入MSG/USDC抵押品 -> CDP引擎铸造MUSD -> 发送给用户
销毁 (Burn):
用户发送MUSD到CDP引擎 -> 减少债务 -> MUSD被销毁
价格稳定机制:
当MUSD > $1: 套利者铸造MUSD卖出
当MUSD < $1: 套利者买入MUSD还款套利
6.2 AMM集成
MUSD与MSG在去中心化交易所(如Osmosis)上创建流动性池,实现稳定币交易。
// scripts/ammIntegration.ts
import { SigningStargateClient } from "@cosmjs/stargate";
import { calculateFee, GasPrice } from "@cosmjs/stargate";
const DEX_CONTRACT = "msg1amm...";
const MUSD_TOKEN = "factory/msg1stable/MUSD";
interface PoolInfo {
id: number;
reserves_musd: string;
reserves_msg: string;
total_shares: string;
price: string;
}
async function getPoolInfo(
client: SigningStargateClient,
poolId: number
): Promise<PoolInfo> {
return client.queryContractSmart(DEX_CONTRACT, {
pool: { pool_id: poolId },
});
}
async function swapMUSDforMSG(
client: SigningStargateClient,
sender: string,
musdAmount: string,
minMsgOut: string,
poolId: number
): Promise<string> {
const fee = calculateFee(300_000, GasPrice.fromString("1000000000attoMSG"));
const msg = {
typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract",
value: {
sender,
contract: DEX_CONTRACT,
msg: Buffer.from(
JSON.stringify({
swap: {
pool_id: poolId,
offer_asset: {
info: { token: { contract_addr: MUSD_TOKEN } },
amount: musdAmount,
},
ask_asset_info: { native_token: { denom: "umsg" } },
min_receive: minMsgOut,
},
})
).toString("base64"),
funds: [],
},
};
const result = await client.signAndBroadcast(sender, [msg], fee);
return result.transactionHash;
}
async function addLiquidity(
client: SigningStargateClient,
sender: string,
musdAmount: string,
msgAmount: string,
poolId: number
): Promise<string> {
const fee = calculateFee(400_000, GasPrice.fromString("1000000000attoMSG"));
const funds = [
{ denom: "umsg", amount: msgAmount },
{ denom: MUSD_TOKEN, amount: musdAmount },
];
const msg = {
typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract",
value: {
sender,
contract: DEX_CONTRACT,
msg: Buffer.from(
JSON.stringify({
provide_liquidity: {
pool_id: poolId,
assets: [
{
info: { native_token: { denom: "umsg" } },
amount: msgAmount,
},
{
info: { token: { contract_addr: MUSD_TOKEN } },
amount: musdAmount,
},
],
},
})
).toString("base64"),
funds,
},
};
const result = await client.signAndBroadcast(sender, [msg], fee);
return result.transactionHash;
}
6.3 套利机制
套利是维持MUSD锚定1 USD的关键机制。
套利流程:
场景1: MUSD交易价格 > $1
1. 用户存入1 MSG(价值$10)到CDP引擎
2. 铸造6.66 MUSD(1.5倍抵押率)
3. 在DEX上以$1.05/MUSD出售6.66 MUSD
4. 获得约6.99 MSG
5. 利润: 约5.99 MSG - 交易费
场景2: MUSD交易价格 < $1
1. 用户在DEX上以$0.95/MUSD买入MUSD
2. 使用MUSD偿还CDP债务
3. 取回抵押品MSG
4. 利润: 抵押品价值 - 买入成本 - 稳定费
// scripts/arbitrage.ts
interface ArbitrageOpportunity {
type: "mint_to_sell" | "buy_to_repay";
profit: bigint;
estimatedGas: bigint;
}
async function checkMintArbitrage(
client: SigningStargateClient,
msgPrice: bigint,
musdPrice: bigint
): ArbitrageOpportunity | null {
const peg = BigInt(1e18); // 1 USD in atomic units
if (musdPrice <= peg) return null;
const minCr = 150n; // 150%
const depositMsg = BigInt(1000) * BigInt(1e18); // 1000 MSG
const depositValue = depositMsg * msgPrice / BigInt(1e18);
const maxMint = depositValue * BigInt(1e18) / minCr;
const musdOut = maxMint * BigInt(1e18) / musdPrice;
const profit = musdOut - depositMsg;
if (profit <= BigInt(0)) return null;
return {
type: "mint_to_sell",
profit,
estimatedGas: BigInt(500_000) * BigInt(25_000_000_000_000n), // 1000000000attoMSG * 500k
};
}
async function checkRepayArbitrage(
client: SigningStargateClient,
musdPrice: bigint,
existingCdp: { debt: bigint; collateral: bigint }
): ArbitrageOpportunity | null {
const peg = BigInt(1e18);
if (musdPrice >= peg) return null;
const buyCost = existingCdp.debt * musdPrice / BigInt(1e18);
const collateralValue = existingCdp.collateral * BigInt(1e18); // Assume $1 MSG
const profit = collateralValue - buyCost;
if (profit <= BigInt(0)) return null;
return {
type: "buy_to_repay",
profit,
estimatedGas: BigInt(400_000) * BigInt(25_000_000_000_000n),
};
}
6.4 稳定费模型
// Stability fee calculation
pub fn calculate_stability_fee(
debt: Uint128,
rate: Decimal,
elapsed_seconds: u64,
) -> Uint128 {
if debt.is_zero() || rate.is_zero() || elapsed_seconds == 0 {
return Uint128::zero();
}
let year_seconds = Uint128::from(31_536_000u64);
let elapsed = Uint128::from(elapsed_seconds);
// fee = debt * rate * time / year
debt.checked_multiply_ratio(rate, Uint128::one())
.unwrap_or(Uint128::zero())
.checked_multiply_ratio(elapsed, year_seconds)
.unwrap_or(Uint128::zero())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stability_fee() {
let debt = Uint128::new(1000_000_000_000_000_000_000u128); // 1000 MUSD
let rate = Decimal::percent(1); // 1% per year
let one_year = 31_536_000u64;
let fee = calculate_stability_fee(debt, rate, one_year);
assert_eq!(fee, Uint128::new(10_000_000_000_000_000_000u128));
assert_eq!(fee, debt * rate / Uint128::one());
}
#[test]
fn test_fee_half_year() {
let debt = Uint128::new(1000_000_000_000_000_000_000u128);
let rate = Decimal::percent(1);
let half_year = 15_768_000u64;
let fee = calculate_stability_fee(debt, rate, half_year);
let expected = Uint128::new(5_000_000_000_000_000_000u128);
assert_eq!(fee, expected);
}
}
7. 前端集成
7.1 React组件
// src/components/Dashboard.tsx
import React, { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Card, Stat, Table, Badge } from "./ui";
import { createClient } from "../lib/client";
import { formatUnits, formatRatio } from "../lib/format";
interface GlobalStats {
total_debt: string;
total_collateral: string;
total_collateral_value: string;
total_minted: string;
cdp_count: number;
debt_ceiling: string;
utilization: string;
current_price: string;
min_collateral_ratio: string;
liquidation_ratio: string;
stability_fee_rate: string;
total_fees_collected: string;
}
interface CDP {
id: number;
owner: string;
collateral: string;
debt: string;
collateral_value: string;
collateral_ratio: string;
status: "active" | "closed" | "liquidated";
}
function useGlobalStats() {
return useQuery<GlobalStats>({
queryKey: ["globalStats"],
queryFn: async () => {
const client = await createClient();
return client.queryContractSmart("msg1engine...", {
get_global_stats: {},
});
},
refetchInterval: 10_000,
});
}
function useUnsafeCdps() {
return useQuery<{ cdps: CDP[] }>({
queryKey: ["unsafeCdps"],
queryFn: async () => {
const client = await createClient();
return client.queryContractSmart("msg1engine...", {
get_unsafe_cdps: { start: 1, limit: 20 },
});
},
refetchInterval: 5_000,
});
}
export function Dashboard() {
const { data: stats } = useGlobalStats();
const { data: unsafe } = useUnsafeCdps();
return (
<div className="dashboard">
<h1>MUSD稳定币仪表盘</h1>
<div className="stats-grid">
<Card>
<Stat
label="MUSD总供应量"
value={formatUnits(stats?.total_minted, 18)}
suffix="MUSD"
/>
</Card>
<Card>
<Stat
label="总债务"
value={formatUnits(stats?.total_debt, 18)}
suffix="MUSD"
/>
</Card>
<Card>
<Stat
label="总抵押品"
value={formatUnits(stats?.total_collateral, 18)}
suffix="MSG"
/>
</Card>
<Card>
<Stat
label="抵押品总价值"
value={formatUnits(stats?.total_collateral_value, 18)}
suffix="USD"
/>
</Card>
<Card>
<Stat
label="MSG/USD价格"
value={formatUnits(stats?.current_price, 18)}
prefix="$"
/>
</Card>
<Card>
<Stat
label="利用率"
value={
stats ? (parseFloat(stats.utilization) * 100).toFixed(2) : "0"
}
suffix="%"
/>
</Card>
</div>
<div className="system-params">
<h2>系统参数</h2>
<div className="params-grid">
<div>最低抵押率: {formatRatio(stats?.min_collateral_ratio)}%</div>
<div>清算阈值: {formatRatio(stats?.liquidation_ratio)}%</div>
<div>稳定费率: {formatRatio(stats?.stability_fee_rate)}% APY</div>
<div>
债务上限: {formatUnits(stats?.debt_ceiling, 18)} MUSD
</div>
<div>CDP总数: {stats?.cdp_count || 0}</div>
</div>
</div>
<div className="unsafe-cdps">
<h2>不安全头寸 ({unsafe?.cdps.length || 0})</h2>
<Table>
<thead>
<tr>
<th>ID</th>
<th>拥有者</th>
<th>抵押品</th>
<th>债务</th>
<th>抵押率</th>
<th>状态</th>
</tr>
</thead>
<tbody>
{(unsafe?.cdps || []).map((cdp) => (
<tr key={cdp.id}>
<td>{cdp.id}</td>
<td>{cdp.owner.slice(0, 12)}...</td>
<td>{formatUnits(cdp.collateral, 18)} MSG</td>
<td>{formatUnits(cdp.debt, 18)} MUSD</td>
<td className="danger">
{(parseFloat(cdp.collateral_ratio) * 100).toFixed(2)}%
</td>
<td>
<Badge variant="danger">{cdp.status}</Badge>
</td>
</tr>
))}
</tbody>
</Table>
</div>
</div>
);
}
7.2 创建CDP表单
// src/components/OpenCdpForm.tsx
import React, { useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { Button, Input, Alert } from "./ui";
import { createClient, executeContract } from "../lib/client";
interface OpenCdpFormProps {
cdpEngine: string;
msgPrice: string;
}
export function OpenCdpForm({ cdpEngine, msgPrice }: OpenCdpFormProps) {
const [collateral, setCollateral] = useState("");
const [debtAmount, setDebtAmount] = useState("");
const [error, setError] = useState("");
const collateralNum = parseFloat(collateral) || 0;
const debtNum = parseFloat(debtAmount) || 0;
const collateralValue = collateralNum * (parseFloat(msgPrice) || 0);
const minCollateralRatio = 1.5;
const maxDebt = collateralValue / minCollateralRatio;
const currentRatio = debtNum > 0 ? collateralValue / debtNum : 0;
const mutation = useMutation({
mutationFn: async () => {
const client = await createClient();
return executeContract(client, cdpEngine, {
open_cdp: {
debt_amount: BigInt(Math.floor(debtNum * 1e18)).toString(),
},
funds: [
{
denom: "umsg",
amount: BigInt(Math.floor(collateralNum * 1e18)).toString(),
},
],
});
},
onSuccess: () => {
setCollateral("");
setDebtAmount("");
},
onError: (err: Error) => setError(err.message),
});
const isDisabled = !collateral || !debtAmount || debtNum > maxDebt;
return (
<div className="open-cdp-form">
<h2>创建新CDP</h2>
<div className="form-info">
<div>MSG/USD价格: ${parseFloat(msgPrice).toFixed(2)}</div>
<div>最低抵押率: {minCollateralRatio * 100}%</div>
<div>最大可铸造: {maxDebt.toFixed(2)} MUSD</div>
</div>
<div className="form-group">
<label>抵押品数量 (MSG)</label>
<Input
type="number"
value={collateral}
onChange={(e) => setCollateral(e.target.value)}
placeholder="输入MSG数量"
/>
</div>
<div className="form-group">
<label>铸造数量 (MUSD)</label>
<Input
type="number"
value={debtAmount}
onChange={(e) => setDebtAmount(e.target.value)}
placeholder="输入MUSD数量"
/>
</div>
<div className="ratio-preview">
<div>
预计抵押率:
<span className={currentRatio >= minCollateralRatio ? "safe" : "danger"}>
{currentRatio > 0 ? (currentRatio * 100).toFixed(2) : "---"}%
</span>
</div>
{debtNum > maxDebt && (
<Alert variant="warning">
铸造金额超过最大值 {maxDebt.toFixed(2)} MUSD
</Alert>
)}
</div>
{error && <Alert variant="error">{error}</Alert>}
<Button
onClick={() => mutation.mutate()}
disabled={isDisabled || mutation.isPending}
>
{mutation.isPending ? "交易处理中..." : "创建CDP"}
</Button>
</div>
);
}
7.3 CDP管理UI
// src/components/CdpManager.tsx
import React, { useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { Card, Button, Input, Alert, Badge } from "./ui";
import { createClient, executeContract } from "../lib/client";
interface CdpInfo {
id: number;
owner: string;
collateral: string;
debt: string;
collateral_value: string;
collateral_ratio: string;
status: string;
}
interface CdpManagerProps {
cdpEngine: string;
owner: string;
msgPrice: string;
}
export function CdpManager({ cdpEngine, owner, msgPrice }: CdpManagerProps) {
const [action, setAction] = useState<"deposit" | "withdraw" | "mint" | "repay" | "close">("deposit");
const [amount, setAmount] = useState("");
const { data: cdps, refetch } = useQuery({
queryKey: ["userCdps", owner],
queryFn: async () => {
const client = await createClient();
return client.queryContractSmart(cdpEngine, {
list_cdps_by_owner: { owner, start: 1, limit: 10 },
});
},
});
const mutation = useMutation({
mutationFn: async (cdpId: number) => {
const client = await createClient();
const amountAtomic = BigInt(Math.floor(parseFloat(amount || "0") * 1e18)).toString();
let msg: any;
switch (action) {
case "deposit":
msg = { deposit_collateral: { cdp_id: cdpId } };
break;
case "withdraw":
msg = { withdraw_collateral: { cdp_id: cdpId, amount: amountAtomic } };
break;
case "mint":
msg = { mint_stable: { cdp_id: cdpId, amount: amountAtomic } };
break;
case "repay":
msg = { repay_debt: { cdp_id: cdpId, amount: amountAtomic } };
break;
case "close":
msg = { close_cdp: { cdp_id: cdpId } };
break;
}
return executeContract(client, cdpEngine, msg);
},
onSuccess: () => refetch(),
});
return (
<div className="cdp-manager">
<h2>管理CDP</h2>
{cdps?.cdps?.length === 0 && <p>没有活跃的CDP</p>}
<div className="cdp-list">
{cdps?.cdps?.map((cdp: CdpInfo) => {
const cr = parseFloat(cdp.collateral_ratio) * 100;
const isSafe = cr >= 120;
const liqPrice = parseFloat(cdp.debt) * 1.2 / parseFloat(cdp.collateral) * (parseFloat(msgPrice) / parseFloat(cdp.collateral_value) * parseFloat(cdp.collateral));
return (
<Card key={cdp.id} className="cdp-card">
<div className="cdp-header">
<h3>CDP #{cdp.id}</h3>
<Badge variant={isSafe ? "success" : "danger"}>
{cr.toFixed(2)}%
</Badge>
</div>
<div className="cdp-details">
<div>抵押品: {cdp.collateral} MSG</div>
<div>债务: {cdp.debt} MUSD</div>
<div>抵押品价值: ${parseFloat(cdp.collateral_value)}</div>
<div>清算价格: ${liqPrice.toFixed(2)}</div>
<div>状态: {cdp.status}</div>
</div>
<div className="cdp-actions">
<select value={action} onChange={(e) => setAction(e.target.value as any)}>
<option value="deposit">存入抵押品</option>
<option value="withdraw">提取抵押品</option>
<option value="mint">铸造MUSD</option>
<option value="repay">偿还债务</option>
<option value="close">平仓</option>
</select>
{action !== "close" && (
<Input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="数量"
/>
)}
<Button
onClick={() => mutation.mutate(cdp.id)}
disabled={mutation.isPending}
>
执行
</Button>
</div>
</Card>
);
})}
</div>
</div>
);
}
7.4 Web3连接库
// src/lib/client.ts
import {
SigningStargateClient,
GasPrice,
StargateClient,
calculateFee,
} from "@cosmjs/stargate";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
import { toBase64 } from "@cosmjs/encoding";
const RPC_ENDPOINT = "https://rpc.msgchain.org";
const PREFIX = "msg";
const DEFAULT_GAS_PRICE = GasPrice.fromString("1000000000attoMSG");
const DEFAULT_GAS_LIMIT = 300_000;
export async function createClient(
mnemonic?: string
): Promise<SigningStargateClient> {
if (mnemonic) {
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, {
prefix: PREFIX,
});
return SigningStargateClient.connectWithSigner(RPC_ENDPOINT, wallet, {
gasPrice: DEFAULT_GAS_PRICE,
});
}
throw new Error("Mnemonic required for signing client");
}
export async function queryContract<T>(
contract: string,
msg: Record<string, any>
): Promise<T> {
const client = await StargateClient.connect(RPC_ENDPOINT);
return client.queryContractSmart(contract, msg);
}
export async function executeContract(
client: SigningStargateClient,
contract: string,
msg: Record<string, any>,
funds?: { denom: string; amount: string }[]
): Promise<string> {
const [account] = await client.getAccounts();
const fee = calculateFee(DEFAULT_GAS_LIMIT, DEFAULT_GAS_PRICE);
const execMsg = {
typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract",
value: {
sender: account.address,
contract,
msg: toBase64(msg),
funds: funds || [],
},
};
const result = await client.signAndBroadcast(
account.address,
[execMsg],
fee
);
if (result.code !== 0) {
throw new Error(`Transaction failed: ${result.rawLog}`);
}
return result.transactionHash;
}
export async function getBalance(
address: string,
denom: string
): Promise<string> {
const client = await StargateClient.connect(RPC_ENDPOINT);
const coin = await client.getBalance(address, denom);
return coin.amount;
}
7.5 格式化和工具函数
// src/lib/format.ts
export function formatUnits(
amount?: string,
decimals: number = 18,
display: number = 2
): string {
if (!amount) return "0";
const padded = amount.padStart(decimals + 1, "0");
const intPart = padded.slice(0, padded.length - decimals) || "0";
const decPart = padded.slice(padded.length - decimals, padded.length - decimals + display);
return `${intPart}.${decPart}`;
}
export function formatRatio(ratio?: string): string {
if (!ratio) return "0";
return (parseFloat(ratio) * 100).toFixed(2);
}
export function shortenAddress(address: string, chars: number = 8): string {
return `${address.slice(0, chars)}...${address.slice(-4)}`;
}
export function calculateCollateralRatio(
collateralValue: string,
debt: string
): number {
const cv = parseFloat(collateralValue) || 0;
const d = parseFloat(debt) || 1;
return cv / d;
}
export function calculateLiquidationPrice(
collateral: string,
debt: string,
liquidationRatio: number = 1.2
): number {
const c = parseFloat(collateral) || 1;
const d = parseFloat(debt) || 0;
return (d * liquidationRatio) / c;
}
export function formatUSD(amount: string): string {
const num = parseFloat(amount) / 1e18;
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(num);
}
8. 风险管理
8.1 抵押率监控
持续监控所有CDP的抵押率,确保系统偿付能力。
风险监控指标:
1. 全系统抵押率 = 总抵押品价值 / 总债务
目标: > 200%
预警: < 150%
危险: < 120%
2. 单个CDP抵押率
安全: >= 150%
预警: 120% - 150%
清算触发: < 120%
3. 债务利用率 = 总债务 / 全局债务上限
目标: < 75%
预警: > 85%
封顶: 100%
8.2 全局债务上限
// Risk management: debt ceiling enforcement
pub fn enforce_debt_ceiling(
current_total_debt: Uint128,
new_debt: Uint128,
ceiling: Uint128,
) -> Result<(), ContractError> {
let projected = current_total_debt.checked_add(new_debt)?;
if projected > ceiling {
Err(ContractError::DebtCeilingExceeded {})
} else {
Ok(())
}
}
pub fn calculate_utilization_rate(total_debt: Uint128, ceiling: Uint128) -> Decimal {
if ceiling.is_zero() {
return Decimal::zero();
}
Decimal::from_ratio(total_debt, ceiling)
}
8.3 紧急关停 (全局结算)
// contracts/emergency_shutdown/src/contract.rs
use cosmwasm_std::{
entry_point, to_json_binary, Binary, Deps, DepsMut, Env,
MessageInfo, Response, StdResult, Uint128, Addr,
CosmosMsg, WasmMsg, BankMsg, Coin,
};
use crate::error::ContractError;
use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, ShutdownInfo};
use crate::state::{Config, ShutdownState, CONFIG, SHUTDOWN_STATE, REDEMPTIONS};
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::TriggerShutdown { reason } => {
execute_trigger_shutdown(deps, env, info, reason)
}
ExecuteMsg::Redeem { cdp_id } => {
execute_redeem(deps, env, info, cdp_id)
}
ExecuteMsg::ProcessRedemption { cdp_id } => {
execute_process_redemption(deps, env, info, cdp_id)
}
ExecuteMsg::Finalize {} => {
execute_finalize(deps, env, info)
}
}
}
fn execute_trigger_shutdown(
deps: DepsMut,
env: Env,
info: MessageInfo,
reason: String,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
if info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
let state = ShutdownState {
active: true,
trigger_time: env.block.time.seconds(),
trigger_height: env.block.height,
reason,
finalized: false,
collateral_price: Uint128::zero(),
};
SHUTDOWN_STATE.save(deps.storage, &state)?;
Ok(Response::new()
.add_attribute("action", "trigger_shutdown")
.add_attribute("reason", &state.reason))
}
fn execute_redeem(
deps: DepsMut,
_env: Env,
info: MessageInfo,
cdp_id: u64,
) -> Result<Response, ContractError> {
let shutdown = SHUTDOWN_STATE.load(deps.storage)?;
if !shutdown.active || shutdown.finalized {
return Err(ContractError::ShutdownNotActive {});
}
let cdp: cdp_engine::msg::CdpResponse = deps
.querier
.query_wasm_smart(
CONFIG.load(deps.storage)?.cdp_engine.clone(),
&cdp_engine::msg::QueryMsg::GetCdp { cdp_id },
)?;
// Calculate redemption amount based on frozen price
let redemption_value = cdp.debt
.checked_multiply_ratio(
Uint128::one(),
shutdown.collateral_price,
)?;
REDEMPTIONS.save(deps.storage, cdp_id, &redemption_value)?;
Ok(Response::new()
.add_attribute("action", "redeem")
.add_attribute("cdp_id", cdp_id.to_string())
.add_attribute("value", redemption_value.to_string()))
}
fn execute_process_redemption(
deps: DepsMut,
_env: Env,
info: MessageInfo,
cdp_id: u64,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let redemption = REDEMPTIONS.load(deps.storage, cdp_id)?;
Ok(Response::new()
.add_message(CosmosMsg::Bank(BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![Coin {
denom: "umsg".to_string(),
amount: redemption,
}],
}))
.add_attribute("action", "process_redemption")
.add_attribute("cdp_id", cdp_id.to_string())
.add_attribute("amount", redemption.to_string()))
}
fn execute_finalize(
deps: DepsMut,
_env: Env,
info: MessageInfo,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
if info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
SHUTDOWN_STATE.update(deps.storage, |mut s| -> Result<_, ContractError> {
s.finalized = true;
Ok(s)
})?;
Ok(Response::new()
.add_attribute("action", "finalize"))
}
#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::GetShutdownState {} => {
to_json_binary(&SHUTDOWN_STATE.load(deps.storage)?)
}
QueryMsg::GetRedemption { cdp_id } => {
to_json_binary(&REDEMPTIONS.load(deps.storage, cdp_id)?)
}
}
}
8.4 治理参数
// Governance parameters with timelock
pub struct GovernanceConfig {
pub owner: Addr,
pub timelock_delay: u64, // seconds
pub proposal_threshold: Uint128, // minimum tokens to propose
pub voting_period: u64, // seconds
pub quorum: Decimal, // minimum participation
pub majority: Decimal, // passing threshold
}
pub enum ParameterChange {
MinCollateralRatio(Decimal),
LiquidationRatio(Decimal),
LiquidationFee(Decimal),
DebtCeiling(Uint128),
StabilityFeeRate(Decimal),
OracleAddress(Addr),
}
impl GovernanceConfig {
pub fn validate_change(&self, change: &ParameterChange) -> Result<(), String> {
match change {
ParameterChange::MinCollateralRatio(r) => {
if r < &Decimal::percent(100) {
return Err("Min collateral ratio below 100%".to_string());
}
}
ParameterChange::LiquidationRatio(r) => {
if r < &Decimal::percent(100) {
return Err("Liquidation ratio below 100%".to_string());
}
}
ParameterChange::LiquidationFee(f) => {
if f > &Decimal::percent(50) {
return Err("Liquidation fee exceeds 50%".to_string());
}
}
_ => {}
}
Ok(())
}
}
8.5 风险监控脚本
// scripts/monitor.ts
interface RiskMetrics {
globalCollateralRatio: number;
totalDebt: string;
totalCollateralValue: string;
unsafePositions: number;
utilizationRate: number;
msuPegDeviation: number;
}
async function collectRiskMetrics(): Promise<RiskMetrics> {
const stats = await queryContract("msg1engine...", {
get_global_stats: {},
});
const musdPool = await queryContract("msg1amm...", {
pool: { pool_id: 1 },
});
const unsafe = await queryContract("msg1engine...", {
get_unsafe_cdps: { start: 1, limit: 100 },
});
const musdPrice = parseFloat(musdPool.price) / 1e18;
const pegDeviation = Math.abs(musdPrice - 1.0);
return {
globalCollateralRatio: parseFloat(stats.total_collateral_value)
/ parseFloat(stats.total_debt),
totalDebt: stats.total_debt,
totalCollateralValue: stats.total_collateral_value,
unsafePositions: unsafe.cdps?.length || 0,
utilizationRate: parseFloat(stats.utilization),
msuPegDeviation: pegDeviation,
};
}
async function checkAlerts(metrics: RiskMetrics) {
const alerts: string[] = [];
if (metrics.globalCollateralRatio < 1.5) {
alerts.push(`CRITICAL: Global CR ${metrics.globalCollateralRatio.toFixed(2)}`);
}
if (metrics.unsafePositions > 0) {
alerts.push(`WARNING: ${metrics.unsafePositions} unsafe positions`);
}
if (metrics.utilizationRate > 0.85) {
alerts.push(`WARNING: Utilization ${(metrics.utilizationRate * 100).toFixed(1)}%`);
}
if (metrics.msuPegDeviation > 0.02) {
alerts.push(`INFO: MUSD peg deviation ${(metrics.msuPegDeviation * 100).toFixed(2)}%`);
}
alerts.forEach((a) => console.log(`[${new Date().toISOString()}] ${a}`));
}
setInterval(async () => {
try {
const metrics = await collectRiskMetrics();
await checkAlerts(metrics);
} catch (err) {
console.error("Monitor error:", err);
}
}, 15_000);
9. 完整测试
9.1 集成测试:完整生命周期
// tests/integration_test.rs
use cosmwasm_std::{
testing::mock_env, Addr, Coin, Decimal, Uint128,
};
use cw_multi_test::{App, ContractWrapper, Executor, BasicApp};
use cdp_engine::{
contract::{instantiate, execute, query},
msg::{
InstantiateMsg, ExecuteMsg, QueryMsg, OpenCdpMsg,
DepositCollateralMsg, WithdrawCollateralMsg,
MintStableMsg, RepayDebtMsg, CloseCdpMsg,
},
state::Config,
};
fn setup_app() -> BasicApp {
BasicApp::new(|router, _, storage| {
router
.bank
.init_balance(
storage,
&Addr::unchecked("alice"),
vec![Coin {
denom: "umsg".to_string(),
amount: Uint128::new(10_000_000_000_000_000_000_000u128),
}],
)
.unwrap();
router
.bank
.init_balance(
storage,
&Addr::unchecked("bob"),
vec![Coin {
denom: "umsg".to_string(),
amount: Uint128::new(5_000_000_000_000_000_000_000u128),
}],
)
.unwrap();
})
}
fn instantiate_cdp_engine(app: &mut BasicApp) -> Addr {
let code = ContractWrapper::new(execute, instantiate, query);
let code_id = app.store_code(Box::new(code));
let msg = InstantiateMsg {
collateral_token: "umsg".to_string(),
stable_token: "factory/msg1stable/MUSD".to_string(),
min_collateral_ratio: "1.5".to_string(),
liquidation_ratio: "1.2".to_string(),
liquidation_fee: "0.13".to_string(),
oracle: "msg1oracle".to_string(),
debt_ceiling: Uint128::new(10_000_000_000_000_000_000_000_000u128),
engine_id: 1,
factory: "msg1factory".to_string(),
name: "MUSD CDP Engine".to_string(),
symbol: "MUSD".to_string(),
};
app.instantiate_contract(code_id, Addr::unchecked("owner"), &msg, &[], "CDP", None)
.unwrap()
}
#[test]
fn test_full_lifecycle() {
let mut app = setup_app();
let engine = instantiate_cdp_engine(&mut app);
// Verify initial state
let config: Config = app
.wrap()
.query_wasm_smart(engine.clone(), &QueryMsg::GetConfig {})
.unwrap();
assert!(!config.paused);
assert_eq!(config.min_collateral_ratio, Decimal::percent(150));
// Open CDP with 1000 MSG, mint 5000 MUSD
let funds_1 = vec![Coin {
denom: "umsg".to_string(),
amount: Uint128::new(1_000_000_000_000_000_000_000u128),
}];
let open_msg = ExecuteMsg::OpenCdp(OpenCdpMsg {
debt_amount: Uint128::new(5_000_000_000_000_000_000_000u128),
});
let result = app.execute_contract(
Addr::unchecked("alice"),
engine.clone(),
&open_msg,
&funds_1,
);
// May fail without oracle - structural test
println!("Open CDP result: {:?}", result);
// Verify global state
let stats: serde_json::Value = app
.wrap()
.query_wasm_smart(engine.clone(), &QueryMsg::GetGlobalStats {})
.unwrap();
println!("Global stats: {:?}", stats);
}
#[test]
fn test_multiple_users() {
let mut app = setup_app();
let engine = instantiate_cdp_engine(&mut app);
// Alice and Bob open positions
for (user, collateral, debt) in [
("alice", 1_000u128, 5_000u128),
("bob", 500u128, 2_000u128),
] {
let funds = vec![Coin {
denom: "umsg".to_string(),
amount: Uint128::new(collateral * 1_000_000_000_000_000_000u128),
}];
let _ = app.execute_contract(
Addr::unchecked(user),
engine.clone(),
&ExecuteMsg::OpenCdp(OpenCdpMsg {
debt_amount: Uint128::new(debt * 1_000_000_000_000_000_000u128),
}),
&funds,
);
}
}
#[test]
fn test_math_consistency() {
// Test core math operations used throughout the protocol
let amount = Uint128::new(1_000_000_000_000_000_000_000u128); // 1000 MSG
let price = Uint128::new(10_000_000_000_000_000_000u128); // $10
let debt = Uint128::new(5_000_000_000_000_000_000_000u128); // 5000 MUSD
// Collateral value = amount * price
let cv = amount.checked_multiply_ratio(price, Uint128::one()).unwrap();
assert_eq!(cv, Uint128::new(10_000_000_000_000_000_000_000u128));
// Collateral ratio = cv / debt
let cr = Decimal::from_ratio(cv, debt);
assert_eq!(cr, Decimal::from_ratio(2u128, 1u128));
// Liquidation threshold check
assert!(cr > Decimal::percent(120));
// Price drop to $6
let price_low = Uint128::new(6_000_000_000_000_000_000u128);
let cv_low = amount.checked_multiply_ratio(price_low, Uint128::one()).unwrap();
let cr_low = Decimal::from_ratio(cv_low, debt);
assert!(cr_low < Decimal::percent(120));
}
9.2 清算测试
#[test]
fn test_liquidation_math() {
let initial_collateral = Uint128::new(1_000_000_000_000_000_000_000u128);
let initial_debt = Uint128::new(6_000_000_000_000_000_000_000u128);
let liquidation_fee = Decimal::percent(13);
let price = Uint128::new(6_000_000_000_000_000_000u128); // $6
// 50% partial liquidation
let debt_to_cover = initial_debt.checked_div(Uint128::from(2u128)).unwrap();
assert_eq!(debt_to_cover, Uint128::new(3_000_000_000_000_000_000_000u128));
// Collateral to seize = debt_to_cover / price
let collateral_to_seize = debt_to_cover
.checked_multiply_ratio(Uint128::one(), price)
.unwrap();
assert_eq!(collateral_to_seize, Uint128::new(500_000_000_000_000_000_000u128));
// Bonus = collateral_to_seize * fee
let bonus = collateral_to_seize
.checked_multiply_ratio(liquidation_fee, Uint128::one())
.unwrap();
assert_eq!(bonus, Uint128::new(65_000_000_000_000_000_000u128));
// Liquidator receives: seized + bonus
let liquidator_reward = collateral_to_seize.checked_add(bonus).unwrap();
assert_eq!(liquidator_reward, Uint128::new(565_000_000_000_000_000_000u128));
// Remaining CDP
let remaining_collateral = initial_collateral
.checked_sub(liquidator_reward)
.unwrap();
let remaining_debt = initial_debt.checked_sub(debt_to_cover).unwrap();
assert_eq!(remaining_collateral, Uint128::new(435_000_000_000_000_000_000u128));
assert_eq!(remaining_debt, Uint128::new(3_000_000_000_000_000_000_000u128));
}
#[test]
fn test_mass_liquidation() {
// Simulate 100 CDPs being liquidated
let cdps: Vec<(Uint128, Uint128)> = (0..100)
.map(|i| {
let collateral = Uint128::new((500 + i * 10) * 1_000_000_000_000_000_000u128);
let debt = Uint128::new((3000 + i * 100) * 1_000_000_000_000_000_000u128);
(collateral, debt)
})
.collect();
let price = Uint128::new(5_000_000_000_000_000_000u128); // $5 - below liquidation
let liq_fee = Decimal::percent(13);
let mut total_debt_covered = Uint128::zero();
let mut total_collateral_seized = Uint128::zero();
for (collateral, debt) in &cdps {
let cv = collateral.checked_multiply_ratio(price, Uint128::one()).unwrap();
let cr = Decimal::from_ratio(cv, debt);
if cr < Decimal::percent(120) {
let debt_cover = debt.checked_div(Uint128::from(2u128)).unwrap();
let coll_seize = debt_cover
.checked_multiply_ratio(Uint128::one(), price)
.unwrap();
let bonus = coll_seize
.checked_multiply_ratio(liq_fee, Uint128::one())
.unwrap();
total_debt_covered = total_debt_covered.checked_add(debt_cover).unwrap();
total_collateral_seized = total_collateral_seized
.checked_add(coll_seize.checked_add(bonus).unwrap())
.unwrap();
}
}
assert!(total_debt_covered > Uint128::zero());
println!("Mass liquidation: {} debt covered, {} collateral seized",
total_debt_covered, total_collateral_seized);
}
9.3 边缘情况测试
#[test]
fn test_edge_cases() {
// Oracle failure
let price_zero = Uint128::zero();
assert_eq!(price_zero, Uint128::zero());
// Zero collateral
let zero_collateral = Uint128::zero();
assert!(zero_collateral.is_zero());
// Zero debt
let zero_debt = Uint128::zero();
assert_eq!(Decimal::from_ratio(Uint128::new(1000), zero_debt), Decimal::zero());
// Max uint
let max_uint = Uint128::new(u128::MAX);
let overflow = max_uint.checked_add(Uint128::new(1));
assert!(overflow.is_err());
// Division by zero
let result = Decimal::from_ratio(Uint128::new(1), Uint128::zero());
assert_eq!(result, Decimal::zero());
// Extreme price movement
let price_normal = Uint128::new(10_000_000_000_000_000_000u128);
let price_crash = Uint128::new(100_000_000_000_000_000u128); // 99% drop
let ratio = Decimal::from_ratio(price_crash, price_normal);
assert!(ratio < Decimal::percent(1));
}
#[test]
fn test_oracle_failure_scenario() {
// Simulate system behavior when oracle returns zero
let price = Uint128::zero();
let config = Config {
oracle: Addr::unchecked("msg1oracle"),
// ... other fields would be set
};
// Contract should reject operations when price is zero
let is_valid = !price.is_zero();
assert!(!is_valid);
// In real contract, this would return OraclePriceInvalid
println!("Oracle failure correctly detected");
}
#[test]
fn test_ceiling_enforcement() {
let ceiling = Uint128::new(10_000_000_000_000_000_000_000_000u128);
let total = Uint128::new(8_000_000_000_000_000_000_000_000u128);
let new_mint = Uint128::new(3_000_000_000_000_000_000_000_000u128);
let projected = total.checked_add(new_mint).unwrap();
assert!(projected > ceiling);
let utilization = Decimal::from_ratio(total, ceiling);
assert!(utilization < Decimal::percent(100));
assert!(utilization > Decimal::percent(50));
}
9.4 Gas估算
MSG Chain Gas估算 (单位: umsg):
操作 Gas使用 Gas费用 (1000000000attoMSG)
------------------------------------------------
部署工厂合约 1,500,000 37,500 umsg
部署CDP引擎 1,200,000 30,000 umsg
开仓 250,000 6,250 umsg
存入抵押品 150,000 3,750 umsg
提取抵押品 200,000 5,000 umsg
铸造MUSD 180,000 4,500 umsg
偿还债务 200,000 5,000 umsg
平仓 250,000 6,250 umsg
清算 300,000 7,500 umsg
批量清算 (10个) 1,800,000 45,000 umsg
预言机提交 250,000 6,250 umsg
Gas费用参考:
低优先级: 0.01 umsg/gas
标准: 1,000,000,000 attoMSG/gas
高优先级: 0.04 umsg/gas
5秒出块时间确保清算在1-2个区块内完成。
附录
A.1 部署脚本
#!/bin/bash
# scripts/deploy.sh
# MSG Chain 稳定币协议部署脚本
CHAIN_ID="msg-chain-1"
NODE="https://rpc.msgchain.org"
TXFLAGS="--chain-id=$CHAIN_ID --gas-prices=1000000000attoMSG --gas=auto --gas-adjustment=1.3 --node=$NODE"
MSG_CLI="msgd"
DEPLOYER=$(msgd keys show deployer -a)
echo "Deployer: $DEPLOYER"
# 1. Upload contracts
echo "=== Uploading Factory ==="
FACTORY_CODE=$($MSG_CLI tx wasm store artifacts/stablecoin_factory.wasm $TXFLAGS --from deployer -y | jq -r '.logs[0].events[] | select(.type=="store_code").attributes[] | select(.key=="code_id").value')
echo "Factory code_id: $FACTORY_CODE"
echo "=== Uploading CDP Engine ==="
ENGINE_CODE=$($MSG_CLI tx wasm store artifacts/cdp_engine.wasm $TXFLAGS --from deployer -y | jq -r '.logs[0].events[] | select(.type=="store_code").attributes[] | select(.key=="code_id").value')
echo "Engine code_id: $ENGINE_CODE"
echo "=== Uploading Oracle ==="
ORACLE_CODE=$($MSG_CLI tx wasm store artifacts/oracle_feeder.wasm $TXFLAGS --from deployer -y | jq -r '.logs[0].events[] | select(.type=="store_code").attributes[] | select(.key=="code_id").value')
echo "Oracle code_id: $ORACLE_CODE"
echo "=== Uploading Liquidation Manager ==="
LIQ_CODE=$($MSG_CLI tx wasm store artifacts/liquidation_manager.wasm $TXFLAGS --from deployer -y | jq -r '.logs[0].events[] | select(.type=="store_code").attributes[] | select(.key=="code_id").value')
echo "Liquidation code_id: $LIQ_CODE"
echo "=== Uploading Stability Pool ==="
POOL_CODE=$($MSG_CLI tx wasm store artifacts/stability_pool.wasm $TXFLAGS --from deployer -y | jq -r '.logs[0].events[] | select(.type=="store_code").attributes[] | select(.key=="code_id").value')
echo "Pool code_id: $POOL_CODE"
# 2. Instantiate Factory
echo "=== Instantiating Factory ==="
FACTORY_ADDR=$($MSG_CLI tx wasm instantiate $FACTORY_CODE \
'{"fee_collector":"'$DEPLOYER'","treasury":"'$DEPLOYER'"}' \
--admin $DEPLOYER --label "musd-factory" $TXFLAGS --from deployer -y | \
jq -r '.logs[0].events[] | select(.type=="instantiate").attributes[] | select(.key=="_contract_address").value')
echo "Factory: $FACTORY_ADDR"
# 3. Instantiate Oracle
echo "=== Instantiating Oracle ==="
ORACLE_ADDR=$($MSG_CLI tx wasm instantiate $ORACLE_CODE \
'{"min_sources":2,"deviation_threshold":"0.05","staleness_threshold":3600}' \
--admin $DEPLOYER --label "musd-oracle" $TXFLAGS --from deployer -y | \
jq -r '.logs[0].events[] | select(.type=="instantiate").attributes[] | select(.key=="_contract_address").value')
echo "Oracle: $ORACLE_ADDR"
echo "=== Deployment Complete ==="
echo "Factory: $FACTORY_ADDR"
echo "Oracle: $ORACLE_ADDR"
echo "CDP Engine Code: $ENGINE_CODE"
echo "Liquidation Code: $LIQ_CODE"
echo "Pool Code: $POOL_CODE"
A.2 查询脚本
#!/bin/bash
# scripts/query.sh
ENGINE="msg1engine..."
ORACLE="msg1oracle..."
# Query global stats
echo "=== Global Stats ==="
msgd query wasm contract-state smart $ENGINE '{"get_global_stats":{}}' --node https://rpc.msgchain.org
# Query CDP by ID
echo "=== CDP #1 ==="
msgd query wasm contract-state smart $ENGINE '{"get_cdp":{"cdp_id":1}}' --node https://rpc.msgchain.org
# Query unsafe CDPs
echo "=== Unsafe CDPs ==="
msgd query wasm contract-state smart $ENGINE '{"get_unsafe_cdps":{"start":1,"limit":10}}' --node https://rpc.msgchain.org
# Query oracle price
echo "=== Oracle Price ==="
msgd query wasm contract-state smart $ORACLE '{"get_price":{}}' --node https://rpc.msgchain.org
# Query user CDPs
echo "=== User CDPs ==="
msgd query wasm contract-state smart $ENGINE '{"list_cdps_by_owner":{"owner":"msg1user...","start":1,"limit":10}}' --node https://rpc.msgchain.org
A.3 Cargo.toml
[package]
name = "cdp-engine"
version = "1.0.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
cosmwasm-std = { version = "2", features = ["staking"] }
cosmwasm-schema = "2"
cw-storage-plus = "2"
cw2 = "2"
cw20 = "2"
schemars = "0.8"
serde = { version = "1", features = ["derive"] }
thiserror = "2"
[dev-dependencies]
cw-multi-test = "2"
cosmwasm-std = { version = "2", features = ["stargate", "staking"] }
[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
codegen-units = 1
panic = "abort"
A.4 错误码参考
| 错误码 | 错误消息 | 说明 |
|---|---|---|
| E001 | Unauthorized | 调用者无权限 |
| E002 | ContractPaused | 合约已暂停 |
| E003 | CdpNotFound | CDP不存在 |
| E004 | CdpNotActive | CDP已关闭或已清算 |
| E005 | MaxCdpsPerUser | 超过用户最大CDP数量 |
| E006 | InsufficientCollateral | 抵押品不足 |
| E007 | CollateralRatioTooLow | 抵押率低于最低要求 |
| E008 | DebtCeilingExceeded | 超过全局债务上限 |
| E009 | InsufficientRepayment | 还款金额不足 |
| E010 | OraclePriceInvalid | 预言机价格无效 |
| E011 | LiquidationNotRequired | CDP不需要清算 |
| E012 | SlippageExceeded | 滑点超过限制 |
A.5 术语表
| 中文 | English | 说明 |
|---|---|---|
| 过度抵押稳定币 | Over-Collateralized Stablecoin | 以超额抵押品发行的稳定币 |
| 抵押债务头寸 | Collateralized Debt Position (CDP) | 用户抵押资产借出稳定币的头寸 |
| 抵押率 | Collateral Ratio | 抵押品价值 / 债务金额的比率 |
| 清算 | Liquidation | 对不安全的CDP进行强制平仓 |
| 清算罚金 | Liquidation Fee/Penalty | 清算时收取的额外费用 |
| 稳定费 | Stability Fee | 借款人支付的年化利率 |
| 稳定池 | Stability Pool | 用户存入稳定币用于清算的池子 |
| 预言机 | Oracle | 提供链外价格数据的组件 |
| 全局债务上限 | Global Debt Ceiling | 系统最大可发行的稳定币总量 |
| 紧急关停 | Emergency Shutdown | 系统全局结算机制 |
文档版本: 1.0.0
本文档基于 MSG Chain 代码库核实的技术事实。
白皮书系统: https://msgchain.org/whitepaper/
作者: MSG Chain DeFi Team
许可证: MIT
