MSG Chain AMM 闪电贷 (Flash Loan) 完整实现指南
基于 CosmWasm 的闪贷实现 — 无抵押借贷,单笔交易内完成借出与归还
适用链:MSG Chain (msg-chain-1) | Bech32:msg| 精度:18 位小数
主网状态:No-Go
依赖:AMM DEX Pair 合约(参见AMM_DEX完整实现.md)
目录
1. 概述
1.1 什么是闪电贷?
闪电贷(Flash Loan)是一种无需抵押品的借贷方式,要求借款和归还在同一笔交易内完成。如果借款人未能归还借款(含手续费),整个交易将回滚,等同于贷款从未发生。
核心特性:
- 零抵押:不需要任何抵押品
- 原子性:借出和归还在同一笔交易中,不可分割
- 风险隔离:如果借款人无法归还,交易回滚,贷方无损失
- 无需信用:任何人可以在任何时间借用任意数量(只要流动性允许)
1.2 闪电贷流程
┌────────────────────────────────────────────────────────────┐
│ 单笔交易 (Atomic Tx) │
│ │
│ 1. 发起者调用 flash_loan │
│ 2. AMM Pair 将 tokens 发送给 Receiver 合约 │
│ 3. AMM Pair 调用 receiver.execute_operation() │
│ 4. Receiver 使用借来的 tokens(套利/清算/抵押品置换) │
│ 5. Receiver 将借款 + 手续费归还给 AMM Pair │
│ 6. AMM Pair 验证余额(或交易回滚) │
└────────────────────────────────────────────────────────────┘
1.3 应用场景
| 场景 | 描述 | 收益来源 |
|---|---|---|
| 套利 (Arbitrage) | 在不同 DEX 之间利用价差获利 | 价格差异 |
| 清算 (Liquidation) | 清算抵押不足的 CDP 头寸 | 清算奖励 |
| 抵押品置换 (Collateral Swap) | 在不关闭头寸的情况下更换抵押品 | 避免提前还款 |
| 自借贷 (Self-Liquidation) | 借款人自己清算自己以避免惩罚 | 保留抵押品 |
1.4 MSG Chain 适配
| 参数 | 值 |
|---|---|
| Chain ID | msg-chain-1 |
| Bech32 前缀 | msg |
| 小数精度 | 18 |
| 原生代币 | umsg |
| AMM 手续费 | 0.3% (30 bps) |
| 闪电贷手续费 | 0.3% (30 bps) 或可配置 |
| 合约框架 | CosmWasm 1.x |
1.5 闪电贷 vs 传统借贷
| 特性 | 传统借贷 | 闪电贷 |
|---|---|---|
| 抵押品 | 需要 | 不需要 |
| 执行时间 | 多笔交易 | 单笔交易 |
| 风险 | 违约风险 | 无违约风险(原子回滚) |
| 资本效率 | 低 | 高(借用任意金额) |
| 使用门槛 | KYC / 白名单 | 无需许可 |
| 费用 | 利息(按时间) | 固定手续费(0.3% 左右) |
2. 闪电贷合约实现
2.1 项目结构
闪电贷相关的合约在现有 DEX 项目基础上新增:
msg-dex/
├── contracts/
│ ├── factory/ # 不变
│ ├── pair/ # 修改:添加 flash_loan 功能
│ ├── router/ # 不变
│ ├── flash-receiver/ # 新增:闪贷接收者基础合约
│ └── arbitrage-bot/ # 新增:套利示例合约
│ └── liquidator/ # 新增:清算示例合约
├── packages/
│ └── dex-types/ # 修改:添加闪贷类型
│ └── src/
│ └── lib.rs # 新增 FlashLoanMsg, FlashLoanReceiver trait
2.2 Flash Loan Receiver Trait
// packages/dex-types/src/flash_loan.rs
use cosmwasm_std::{Addr, Binary, Response, StdError, Uint128};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// Flash Loan 执行消息
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct FlashLoanMsg {
pub sender: Addr,
pub token: Addr,
pub amount: Uint128,
pub fee: Uint128,
pub data: Binary,
}
/// Flash Loan 接收者接口
/// 任何希望使用闪贷的合约必须实现此接口
pub trait FlashLoanReceiver {
/// AMM Pair 在发送代币后调用此方法
/// 实现者必须在返回前确保已归还借款 + 手续费
fn execute_operation(
&self,
sender: Addr,
token: Addr,
amount: Uint128,
fee: Uint128,
data: Binary,
) -> Result<Response, StdError>;
}
2.3 dex-types lib.rs 修改
// packages/dex-types/src/lib.rs
use cosmwasm_std::{Addr, Binary, Decimal, Response, StdError, Uint128};
use cw20::Cw20Coin;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum AssetInfo {
NativeToken { denom: String },
Token { contract_addr: Addr },
}
impl fmt::Display for AssetInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AssetInfo::NativeToken { denom } => write!(f, "{}", denom),
AssetInfo::Token { contract_addr } => write!(f, "{}", contract_addr),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Asset {
pub info: AssetInfo,
pub amount: Uint128,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PairInfo {
pub asset_infos: [AssetInfo; 2],
pub contract_addr: Addr,
pub liquidity_token: Addr,
}
// ========== Flash Loan 类型 ==========
/// Flash Loan 执行消息
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct FlashLoanMsg {
pub sender: Addr,
pub token: Addr,
pub amount: Uint128,
pub fee: Uint128,
pub data: Binary,
}
/// Flash Loan 接收者合约的 ExecuteMsg
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum FlashLoanExecuteMsg {
ExecuteOperation {
sender: String,
token: String,
amount: Uint128,
fee: Uint128,
data: Binary,
},
}
// ========== 原始类型 ==========
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum SwapOperation {
NativeSwap { offer_denom: String, ask_denom: String },
CosmWasmSwap { offer_asset_info: AssetInfo, ask_asset_info: AssetInfo },
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct CumulativePrice {
pub price0_cumulative_last: Uint128,
pub price1_cumulative_last: Uint128,
pub block_timestamp_last: u64,
}
2.4 闪贷接收者基础合约 (Flash Receiver)
// contracts/flash-receiver/Cargo.toml
[package]
name = "flash-receiver"
version.workspace = true
license.workspace = true
edition.workspace = true
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
cosmwasm-std.workspace = true
cw-storage-plus.workspace = true
cw2.workspace = true
serde.workspace = true
schemars.workspace = true
thiserror.workspace = true
cosmwasm-schema.workspace = true
dex-types = { path = "../../packages/dex-types" }
[dev-dependencies]
cw-multi-test.workspace = true
// contracts/flash-receiver/src/msg.rs
use cosmwasm_std::{Addr, Binary, Uint128};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
pub owner: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum ExecuteMsg {
ExecuteOperation {
sender: String,
token: String,
amount: Uint128,
fee: Uint128,
data: Binary,
},
Withdraw {
token: String,
amount: Uint128,
recipient: Option<String>,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum QueryMsg {
Owner {},
}
// contracts/flash-receiver/src/state.rs
use cosmwasm_std::Addr;
use cw_storage_plus::Item;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
pub owner: Addr,
}
pub const CONFIG: Item<Config> = Item::new("config");
// contracts/flash-receiver/src/error.rs
use cosmwasm_std::StdError;
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),
#[error("Unauthorized")]
Unauthorized {},
#[error("Flash loan execution failed: {msg}")]
ExecutionFailed { msg: String },
}
// contracts/flash-receiver/src/contract.rs
use cosmwasm_std::{
entry_point, to_binary, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env,
MessageInfo, Response, StdError, Uint128, WasmMsg,
};
use cw2::set_contract_version;
use cw20::Cw20ExecuteMsg;
use dex_types::FlashLoanExecuteMsg;
use crate::error::ContractError;
use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
use crate::state::{Config, CONFIG};
const CONTRACT_NAME: &str = "msg-dex-flash-receiver";
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: deps.api.addr_validate(&msg.owner)? };
CONFIG.save(deps.storage, &config)?;
Ok(Response::new()
.add_attribute("method", "instantiate")
.add_attribute("owner", msg.owner))
}
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::ExecuteOperation { sender, token, amount, fee, data } => {
execute_operation(deps, env, info, sender, token, amount, fee, data)
}
ExecuteMsg::Withdraw { token, amount, recipient } => {
execute_withdraw(deps, env, info, token, amount, recipient)
}
}
}
/// 闪贷接收者核心逻辑
/// 子类合约应覆盖此方法以实现自定义逻辑
pub fn execute_operation(
_deps: DepsMut,
_env: Env,
_info: MessageInfo,
_sender: String,
_token: String,
_amount: Uint128,
_fee: Uint128,
_data: Binary,
) -> Result<Response, ContractError> {
Err(ContractError::ExecutionFailed {
msg: "Not implemented: override in derived contract".to_string(),
})
}
/// 提取合约中累积的代币(例如利润)
pub fn execute_withdraw(
deps: DepsMut,
_env: Env,
info: MessageInfo,
token: String,
amount: Uint128,
recipient: Option<String>,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
if info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
let to = recipient.unwrap_or_else(|| info.sender.to_string());
let token_addr = deps.api.addr_validate(&token)?;
let msg: CosmosMsg = if token == "umsg" || token.starts_with("ibc/") {
CosmosMsg::Bank(BankMsg::Send {
to_address: to,
assets: vec![Coin { denom: token.clone(), amount }],
})
} else {
CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: token_addr.to_string(),
msg: to_binary(&Cw20ExecuteMsg::Transfer { recipient: to, amount })?,
funds: vec![],
})
};
Ok(Response::new()
.add_message(msg)
.add_attribute("method", "withdraw")
.add_attribute("token", token)
.add_attribute("amount", amount))
}
#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result<Binary, ContractError> {
match msg {
QueryMsg::Owner {} => {
let config = CONFIG.load(deps.storage)?;
Ok(to_binary(&config.owner)?)
}
}
}
// contracts/flash-receiver/src/lib.rs
pub mod contract;
pub mod error;
pub mod msg;
pub mod state;
pub use crate::error::ContractError;
2.5 闪贷接收者合约测试
// contracts/flash-receiver/tests/integration.rs
use cosmwasm_std::{Addr, Binary, Empty, Uint128};
use cw_multi_test::{App, Contract, ContractWrapper, Executor};
use flash_receiver::contract::{execute, instantiate, query};
use flash_receiver::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
fn mock_receiver() -> Box<dyn Contract<Empty>> {
Box::new(ContractWrapper::new(
|d, e, i, m: ExecuteMsg| execute(d, e, i, m),
|d, e, i, m: InstantiateMsg| instantiate(d, e, i, m),
|d, e, m: QueryMsg| query(d, e, m),
))
}
#[test]
fn test_receiver_instantiate() {
let mut app = App::default();
let code_id = app.store_code(mock_receiver());
let addr = app.instantiate_contract(
code_id,
Addr::unchecked("owner"),
&InstantiateMsg { owner: "owner".to_string() },
&[], "FlashReceiver",
).unwrap();
let owner: Addr = app.wrap().query_wasm_smart(&addr, &QueryMsg::Owner {}).unwrap();
assert_eq!(owner, "owner");
}
#[test]
fn test_receiver_execute_operation_not_implemented() {
let mut app = App::default();
let code_id = app.store_code(mock_receiver());
let addr = app.instantiate_contract(
code_id, Addr::unchecked("owner"),
&InstantiateMsg { owner: "owner".to_string() },
&[], "FlashReceiver",
).unwrap();
let err = app.execute_contract(
Addr::unchecked("pair"), addr,
&ExecuteMsg::ExecuteOperation {
sender: "user".to_string(),
token: "umsg".to_string(),
amount: Uint128::new(1000),
fee: Uint128::new(3),
data: Binary::default(),
},
&[],
).unwrap_err();
assert!(err.to_string().contains("Not implemented"));
}
#[test]
fn test_receiver_withdraw_unauthorized() {
let mut app = App::default();
let code_id = app.store_code(mock_receiver());
let addr = app.instantiate_contract(
code_id, Addr::unchecked("owner"),
&InstantiateMsg { owner: "owner".to_string() },
&[], "FlashReceiver",
).unwrap();
let err = app.execute_contract(
Addr::unchecked("attacker"), addr,
&ExecuteMsg::Withdraw {
token: "umsg".to_string(),
amount: Uint128::new(1000),
recipient: None,
},
&[],
).unwrap_err();
assert!(err.to_string().contains("Unauthorized"));
}
3. AMM 集成
3.1 Pair 合约修改概述
在已有 Pair 合约的基础上,需要添加以下支持:
- ExecuteMsg 新增
FlashLoan变体 - 新增
execute_flash_loan函数 - 新增 flash loan 状态存储(用于验证偿还)
- 新增查询接口
FlashLoanFee
3.2 Pair msg.rs 修改
// contracts/pair/src/msg.rs (新增内容)
use cosmwasm_std::{Addr, Binary, Decimal, Uint128};
use dex_types::Asset;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
pub asset_infos: [dex_types::AssetInfo; 2],
pub factory_addr: String,
pub lp_token_code_id: u64,
pub fee_bps: u16,
pub flash_loan_fee_bps: Option<u16>,
pub flash_loan_enabled: Option<bool>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum ExecuteMsg {
Swap { offer_asset: Asset, belief_price: Option<Decimal>, max_spread: Option<Decimal>, to: Option<String> },
ProvideLiquidity { assets: [Asset; 2], slippage_tolerance: Option<Decimal>, auto_stake: Option<String>, receiver: Option<String> },
WithdrawLiquidity { amount: Uint128, min_assets_to_withdraw: Option<[Asset; 2]> },
FlashLoan {
token: String,
amount: Uint128,
receiver: String,
data: Binary,
},
FlashLoanVerifyBalance {
expected_balance: Uint128,
token: String,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum QueryMsg {
Pool {},
Simulation { offer_asset: Asset },
ReverseSimulation { ask_asset: Asset },
PairInfo {},
FlashLoanConfig {},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct FlashLoanConfigResponse {
pub flash_loan_fee_bps: u16,
pub max_flash_loan_ratio: String,
pub enabled: bool,
}
3.3 Pair state.rs 修改
// contracts/pair/src/state.rs (新增内容)
use cosmwasm_std::{Addr, Decimal, Uint128};
use cw_storage_plus::Item;
use dex_types::AssetInfo;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
pub factory_addr: Addr,
pub asset_infos: [AssetInfo; 2],
pub lp_token_addr: Addr,
pub fee_bps: u16,
pub flash_loan_fee_bps: u16,
pub flash_loan_enabled: bool,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PoolState {
pub reserve_0: Uint128,
pub reserve_1: Uint128,
pub total_liquidity: Uint128,
pub price0_cumulative_last: Uint128,
pub price1_cumulative_last: Uint128,
pub block_timestamp_last: u64,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct FlashLoanState {
pub token: Addr,
pub amount: Uint128,
pub repay_required: Uint128,
pub active: bool,
}
pub const CONFIG: Item<Config> = Item::new("config");
pub const POOL_STATE: Item<PoolState> = Item::new("pool_state");
pub const FLASH_LOAN_STATE: Item<FlashLoanState> = Item::new("flash_loan_state");
pub const MINIMUM_LIQUIDITY: Uint128 = Uint128::new(1000);
pub const DEFAULT_FLASH_LOAN_FEE_BPS: u16 = 30;
pub const DEFAULT_MAX_FLASH_LOAN_RATIO: Decimal = Decimal::percent(50);
3.4 Pair error.rs 新增错误
#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),
#[error("Unauthorized")]
Unauthorized {},
#[error("Insufficient liquidity: {available} < {required}")]
InsufficientLiquidity { available: Uint128, required: Uint128 },
#[error("Insufficient output amount: {available} < {required}")]
InsufficientOutputAmount { available: Uint128, required: Uint128 },
#[error("Slippage tolerance exceeded: {slippage} > {tolerance}")]
SlippageExceeded { slippage: String, tolerance: String },
#[error("Max spread exceeded: {spread} > {max_spread}")]
MaxSpreadExceeded { spread: String, max_spread: String },
#[error("Minimum liquidity not met")]
MinimumLiquidityNotMet {},
#[error("Invalid offer asset")]
InvalidOfferAsset {},
#[error("Asset mismatch")]
AssetMismatch {},
#[error("Zero amount not allowed")]
ZeroAmount {},
#[error("Doubling assets not allowed")]
DoublingAssets {},
#[error("Native token balance mismatch")]
NativeBalanceMismatch {},
#[error("CW20 balance mismatch")]
Cw20BalanceMismatch {},
#[error("Flash loan not enabled")]
FlashLoanNotEnabled {},
#[error("Flash loan amount exceeds maximum: {amount} > {max}")]
FlashLoanAmountExceeded { amount: Uint128, max: Uint128 },
#[error("Flash loan already active (reentrancy detected)")]
FlashLoanAlreadyActive {},
#[error("Flash loan repayment not received: expected {expected}, received {received}")]
FlashLoanRepaymentNotReceived { expected: Uint128, received: Uint128 },
#[error("Flash loan receiver returned error: {msg}")]
FlashLoanReceiverError { msg: String },
}
3.5 Pair contract.rs 闪电贷核心实现
// contracts/pair/src/contract.rs (新增和修改的部分)
use crate::state::{
Config, PoolState, FlashLoanState,
CONFIG, POOL_STATE, FLASH_LOAN_STATE,
MINIMUM_LIQUIDITY, DEFAULT_FLASH_LOAN_FEE_BPS, DEFAULT_MAX_FLASH_LOAN_RATIO,
};
pub const REPLY_CREATE_LP_TOKEN: u64 = 1;
pub const REPLY_FLASH_LOAN_VERIFY: u64 = 2;
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::Swap { offer_asset, belief_price, max_spread, to } => {
execute_swap(deps, env, info, offer_asset, belief_price, max_spread, to)
}
ExecuteMsg::ProvideLiquidity { assets, slippage_tolerance, auto_stake, receiver } => {
execute_provide_liquidity(deps, env, info, assets, slippage_tolerance, receiver)
}
ExecuteMsg::WithdrawLiquidity { amount, min_assets_to_withdraw } => {
execute_withdraw_liquidity(deps, env, info, amount, min_assets_to_withdraw)
}
ExecuteMsg::FlashLoan { token, amount, receiver, data } => {
execute_flash_loan(deps, env, info, token, amount, receiver, data)
}
ExecuteMsg::FlashLoanVerifyBalance { expected_balance, token } => {
execute_flash_loan_verify_balance(deps, env, info, expected_balance, token)
}
}
}
/// 安全版闪电贷 - 使用余额比对验证还款
pub fn execute_flash_loan(
deps: DepsMut,
env: Env,
_info: MessageInfo,
token: String,
amount: Uint128,
receiver: String,
data: Binary,
) -> Result<Response, ContractError> {
if amount.is_zero() {
return Err(ContractError::ZeroAmount {});
}
let config = CONFIG.load(deps.storage)?;
if !config.flash_loan_enabled {
return Err(ContractError::FlashLoanNotEnabled {});
}
let pool = POOL_STATE.load(deps.storage)?;
let token_addr = deps.api.addr_validate(&token)?;
let (reserve, idx) = get_token_reserve_and_index(
deps.as_ref(), &config, &token_addr, &token
)?;
let max_amount = reserve * Uint128::from(DEFAULT_MAX_FLASH_LOAN_RATIO.numerator())
/ Uint128::from(DEFAULT_MAX_FLASH_LOAN_RATIO.denominator());
if amount > max_amount {
return Err(ContractError::FlashLoanAmountExceeded { amount, max: max_amount });
}
let fee = amount * Uint128::from(config.flash_loan_fee_bps as u64)
/ Uint128::from(10000u64);
let repay_required = amount + fee;
let balance_before = query_token_balance(
&deps.querier, &env.contract.address, &token, &token_addr
)?;
if FLASH_LOAN_STATE.may_load(deps.storage)?.map(|s| s.active).unwrap_or(false) {
return Err(ContractError::FlashLoanAlreadyActive {});
}
FLASH_LOAN_STATE.save(deps.storage, &FlashLoanState {
token: token_addr.clone(),
amount,
repay_required,
active: true,
})?;
let transfer_msg = build_transfer_msg(
&config, &token, &token_addr, &receiver, amount, idx
)?;
let execute_msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: receiver.clone(),
msg: to_binary(&dex_types::FlashLoanExecuteMsg::ExecuteOperation {
sender: env.contract.address.to_string(),
token: token.clone(),
amount,
fee,
data,
})?,
funds: vec![],
});
let verify_msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: env.contract.address.to_string(),
msg: to_binary(&ExecuteMsg::FlashLoanVerifyBalance {
expected_balance: balance_before + repay_required,
token: token.clone(),
})?,
funds: vec![],
});
let mut new_pool = pool.clone();
if idx == 0 {
new_pool.reserve_0 = pool.reserve_0.checked_sub(amount).map_err(|_| {
ContractError::InsufficientLiquidity { available: pool.reserve_0, required: amount }
})?;
} else {
new_pool.reserve_1 = pool.reserve_1.checked_sub(amount).map_err(|_| {
ContractError::InsufficientLiquidity { available: pool.reserve_1, required: amount }
})?;
}
POOL_STATE.save(deps.storage, &new_pool)?;
Ok(Response::new()
.add_message(transfer_msg)
.add_message(execute_msg)
.add_message(verify_msg)
.add_attribute("method", "flash_loan")
.add_attribute("token", token)
.add_attribute("amount", amount)
.add_attribute("fee", fee)
.add_attribute("repay_required", repay_required)
.add_attribute("receiver", receiver))
}
/// 验证闪贷还款
pub fn execute_flash_loan_verify_balance(
deps: DepsMut,
env: Env,
_info: MessageInfo,
expected_balance: Uint128,
token: String,
) -> Result<Response, ContractError> {
let flash_state = FLASH_LOAN_STATE.load(deps.storage)?;
if !flash_state.active {
return Err(ContractError::Std(StdError::generic_err("No active flash loan")));
}
let token_addr = deps.api.addr_validate(&token)?;
let current_balance = query_token_balance(
&deps.querier, &env.contract.address, &token, &token_addr
)?;
if current_balance < expected_balance {
return Err(ContractError::FlashLoanRepaymentNotReceived {
expected: expected_balance,
received: current_balance,
});
}
let balance_before_borrow = expected_balance - flash_state.repay_required;
let actual_fee = current_balance - balance_before_borrow;
let mut pool = POOL_STATE.load(deps.storage)?;
let config = CONFIG.load(deps.storage)?;
let idx = get_token_index(&config, &token_addr, &token)?;
if idx == 0 {
pool.reserve_0 = pool.reserve_0 + actual_fee;
} else {
pool.reserve_1 = pool.reserve_1 + actual_fee;
}
POOL_STATE.save(deps.storage, &pool)?;
FLASH_LOAN_STATE.save(deps.storage, &FlashLoanState {
token: flash_state.token,
amount: flash_state.amount,
repay_required: Uint128::zero(),
active: false,
})?;
Ok(Response::new()
.add_attribute("method", "flash_loan_verify")
.add_attribute("status", "success")
.add_attribute("fee_collected", actual_fee))
}
#[entry_point]
pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result<Response, ContractError> {
match msg.id {
REPLY_CREATE_LP_TOKEN => {
let lp_token_addr = msg.result.into_result()
.map_err(|e| ContractError::Std(StdError::generic_err(e)))?
.get_contract_address()
.ok_or_else(|| ContractError::Std(StdError::generic_err("No contract address")))?;
let mut config = CONFIG.load(deps.storage)?;
config.lp_token_addr = deps.api.addr_validate(&lp_token_addr)?;
CONFIG.save(deps.storage, &config)?;
Ok(Response::new().add_attribute("lp_token_addr", lp_token_addr))
}
REPLY_FLASH_LOAN_VERIFY => {
let result = msg.result.into_result()
.map_err(|e| ContractError::FlashLoanReceiverError { msg: e })?;
Ok(Response::new()
.add_attribute("method", "flash_loan_reply")
.add_attribute("status", "verified"))
}
_ => Err(ContractError::Std(StdError::generic_err(format!(
"Unknown reply id: {}", msg.id
)))),
}
}
#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result<Binary, ContractError> {
match msg {
QueryMsg::Pool {} => Ok(to_binary(&query_pool(deps)?)?),
QueryMsg::Simulation { offer_asset } => Ok(to_binary(&query_simulation(deps, offer_asset)?)?),
QueryMsg::ReverseSimulation { ask_asset } => Ok(to_binary(&query_reverse_simulation(deps, ask_asset)?)?),
QueryMsg::PairInfo {} => Ok(to_binary(&query_pair_info(deps)?)?),
QueryMsg::FlashLoanConfig {} => Ok(to_binary(&query_flash_loan_config(deps)?)?),
}
}
fn query_flash_loan_config(deps: Deps) -> Result<FlashLoanConfigResponse, ContractError> {
let config = CONFIG.load(deps.storage)?;
Ok(FlashLoanConfigResponse {
flash_loan_fee_bps: config.flash_loan_fee_bps,
max_flash_loan_ratio: DEFAULT_MAX_FLASH_LOAN_RATIO.to_string(),
enabled: config.flash_loan_enabled,
})
}
// ========== 辅助函数 ==========
fn query_token_balance(
querier: &QuerierWrapper,
contract_addr: &Addr,
token: &str,
token_addr: &Addr,
) -> Result<Uint128, ContractError> {
if token == "umsg" || token.starts_with("ibc/") {
let balance = querier.query_balance(contract_addr, token)?;
Ok(balance.amount)
} else {
let balance: cw20::BalanceResponse = querier.query_wasm_smart(
token_addr,
&Cw20QueryMsg::Balance {
address: contract_addr.to_string(),
},
)?;
Ok(balance.balance)
}
}
fn get_token_reserve_and_index(
deps: Deps,
config: &Config,
token_addr: &Addr,
token: &str,
) -> Result<(Uint128, usize), ContractError> {
let pool = POOL_STATE.load(deps.storage)?;
for i in 0..2 {
match &config.asset_infos[i] {
AssetInfo::NativeToken { denom } if denom == token => {
return Ok((if i == 0 { pool.reserve_0 } else { pool.reserve_1 }, i));
}
AssetInfo::Token { contract_addr } if contract_addr == token_addr => {
return Ok((if i == 0 { pool.reserve_0 } else { pool.reserve_1 }, i));
}
_ => {}
}
}
Err(ContractError::InvalidOfferAsset {})
}
fn get_token_index(
config: &Config,
token_addr: &Addr,
token: &str,
) -> Result<usize, ContractError> {
for i in 0..2 {
match &config.asset_infos[i] {
AssetInfo::NativeToken { denom } if denom == token => return Ok(i),
AssetInfo::Token { contract_addr } if contract_addr == token_addr => return Ok(i),
_ => {}
}
}
Err(ContractError::InvalidOfferAsset {})
}
fn build_transfer_msg(
config: &Config,
token: &str,
token_addr: &Addr,
receiver: &str,
amount: Uint128,
_idx: usize,
) -> Result<CosmosMsg, ContractError> {
if token == "umsg" || token.starts_with("ibc/") {
Ok(CosmosMsg::Bank(BankMsg::Send {
to_address: receiver.to_string(),
assets: vec![Coin { denom: token.to_string(), amount }],
}))
} else {
Ok(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: token_addr.to_string(),
msg: to_binary(&Cw20ExecuteMsg::Transfer {
recipient: receiver.to_string(),
amount,
})?,
funds: vec![],
}))
}
}
3.6 闪电贷集成测试
// contracts/pair/tests/flash_loan_integration.rs
use cosmwasm_std::{Addr, Binary, Coin, Empty, Uint128};
use cw_multi_test::{App, Contract, ContractWrapper, Executor};
use dex_types::{Asset, AssetInfo};
use pair::contract::{execute, instantiate, query, reply};
use pair::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, FlashLoanConfigResponse};
fn mock_pair() -> Box<dyn Contract<Empty>> {
Box::new(ContractWrapper::new(
|d, e, i, m: ExecuteMsg| execute(d, e, i, m),
|d, e, i, m: InstantiateMsg| instantiate(d, e, i, m),
|d, e, m: QueryMsg| query(d, e, m),
).with_reply(|d, e, m| reply(d, e, m)))
}
fn mock_cw20() -> Box<dyn Contract<Empty>> {
Box::new(ContractWrapper::new(
|d, e, i, m: cw20_base::msg::ExecuteMsg| cw20_base::contract::execute(d, e, i, m),
|d, e, i, m: cw20_base::msg::InstantiateMsg| cw20_base::contract::instantiate(d, e, i, m),
|d, e, m: cw20_base::msg::QueryMsg| cw20_base::contract::query(d, e, m),
))
}
fn setup_pair_app() -> (App, Addr) {
let mut app = App::new(|router, _, storage| {
router.bank.init_balance(storage, &Addr::unchecked("user"),
vec![
Coin { denom: "umsg".to_string(), amount: Uint128::new(1_000_000_000_000) },
Coin { denom: "uusdc".to_string(), amount: Uint128::new(1_000_000_000_000) },
],
).unwrap();
});
let pair_id = app.store_code(mock_pair());
let cw20_id = app.store_code(mock_cw20());
let pair = app.instantiate_contract(
pair_id, Addr::unchecked("factory"),
&InstantiateMsg {
asset_infos: [
AssetInfo::NativeToken { denom: "umsg".to_string() },
AssetInfo::NativeToken { denom: "uusdc".to_string() },
],
factory_addr: "factory".to_string(),
lp_token_code_id: cw20_id,
fee_bps: 30,
flash_loan_fee_bps: Some(30),
flash_loan_enabled: Some(true),
},
&[], "MSG-DEX-Pair-Flash",
).unwrap();
app.execute_contract(
Addr::unchecked("user"), pair.clone(),
&ExecuteMsg::ProvideLiquidity {
assets: [
Asset { info: AssetInfo::NativeToken { denom: "umsg".to_string() }, amount: Uint128::new(1_000_000_000) },
Asset { info: AssetInfo::NativeToken { denom: "uusdc".to_string() }, amount: Uint128::new(1_000_000_000) },
],
slippage_tolerance: None, auto_stake: None, receiver: None,
},
&[
Coin { denom: "umsg".to_string(), amount: Uint128::new(1_000_000_000) },
Coin { denom: "uusdc".to_string(), amount: Uint128::new(1_000_000_000) },
],
).unwrap();
(app, pair)
}
#[test]
fn test_flash_loan_config_query() {
let (app, pair) = setup_pair_app();
let config: FlashLoanConfigResponse = app.wrap().query_wasm_smart(
&pair, &QueryMsg::FlashLoanConfig {},
).unwrap();
assert!(config.enabled);
assert_eq!(config.flash_loan_fee_bps, 30);
}
#[test]
fn test_flash_loan_zero_amount_fails() {
let (mut app, pair) = setup_pair_app();
let err = app.execute_contract(
Addr::unchecked("user"), pair,
&ExecuteMsg::FlashLoan {
token: "umsg".to_string(),
amount: Uint128::zero(),
receiver: "receiver".to_string(),
data: Binary::default(),
},
&[],
).unwrap_err();
assert!(err.to_string().contains("Zero amount"));
}
#[test]
fn test_flash_loan_disabled_fails() {
let mut app = App::default();
let pair_id = app.store_code(mock_pair());
let cw20_id = app.store_code(mock_cw20());
let pair = app.instantiate_contract(
pair_id, Addr::unchecked("factory"),
&InstantiateMsg {
asset_infos: [
AssetInfo::NativeToken { denom: "umsg".to_string() },
AssetInfo::NativeToken { denom: "uusdc".to_string() },
],
factory_addr: "factory".to_string(),
lp_token_code_id: cw20_id,
fee_bps: 30,
flash_loan_fee_bps: Some(30),
flash_loan_enabled: Some(false),
},
&[], "MSG-DEX-Pair-NoFlash",
).unwrap();
let err = app.execute_contract(
Addr::unchecked("user"), pair,
&ExecuteMsg::FlashLoan {
token: "umsg".to_string(),
amount: Uint128::new(1000),
receiver: "receiver".to_string(),
data: Binary::default(),
},
&[],
).unwrap_err();
assert!(err.to_string().contains("Flash loan not enabled"));
}
#[test]
fn test_flash_loan_invalid_token_fails() {
let (mut app, pair) = setup_pair_app();
let err = app.execute_contract(
Addr::unchecked("user"), pair,
&ExecuteMsg::FlashLoan {
token: "uinvalid".to_string(),
amount: Uint128::new(1000),
receiver: "receiver".to_string(),
data: Binary::default(),
},
&[],
).unwrap_err();
assert!(err.to_string().contains("Invalid offer asset"));
}
#[test]
fn test_flash_loan_exceeds_max_amount_fails() {
let (mut app, pair) = setup_pair_app();
let err = app.execute_contract(
Addr::unchecked("user"), pair,
&ExecuteMsg::FlashLoan {
token: "umsg".to_string(),
amount: Uint128::new(1_000_000_000),
receiver: "receiver".to_string(),
data: Binary::default(),
},
&[],
).unwrap_err();
assert!(err.to_string().contains("exceeds maximum"));
}
4. 套利示例合约
4.1 跨池套利原理
[DEX A: MSG/USDC] [DEX B: MSG/USDC]
MSG: 100,000 MSG: 100,000
USDC: 95,000 (低价) USDC: 105,000 (高价)
1 MSG = 0.95 USDC 1 MSG = 1.05 USDC
套利流程:
1. 从 DEX A 闪电贷 10,000 MSG
2. 在 DEX B 用 10,000 MSG 兑换 10,500 USDC (高价卖出)
3. 在 DEX A 用 10,500 USDC 兑换 11,052 MSG (低价买入)
4. 归还 DEX A 10,030 MSG (借款 + 0.3% 手续费)
5. 利润:11,052 - 10,030 = 1,022 MSG
4.2 套利机器人合约
// contracts/arbitrage-bot/Cargo.toml
[package]
name = "arbitrage-bot"
version.workspace = true
license.workspace = true
edition.workspace = true
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
cosmwasm-std.workspace = true
cw-storage-plus.workspace = true
cw2.workspace = true
cw20.workspace = true
serde.workspace = true
schemars.workspace = true
thiserror.workspace = true
cosmwasm-schema.workspace = true
dex-types = { path = "../../packages/dex-types" }
uint.workspace = true
[dev-dependencies]
cw-multi-test.workspace = true
// contracts/arbitrage-bot/src/msg.rs
use cosmwasm_std::{Addr, Binary, Decimal, Uint128};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
pub owner: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ArbitrageOp {
pub flash_loan_pair: String,
pub borrow_token: String,
pub borrow_amount: Uint128,
pub sell_pair: String,
pub sell_token: String,
pub buy_pair: String,
pub buy_token: String,
pub min_profit: Uint128,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum ExecuteMsg {
ExecuteArbitrage { op: ArbitrageOp },
ExecuteOperation {
sender: String,
token: String,
amount: Uint128,
fee: Uint128,
data: Binary,
},
Withdraw { token: String, amount: Uint128 },
UpdateConfig { owner: Option<String> },
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum QueryMsg {
Config {},
Profit {},
}
// contracts/arbitrage-bot/src/state.rs
use cosmwasm_std::Addr;
use cw_storage_plus::Item;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
pub owner: Addr,
}
pub const CONFIG: Item<Config> = Item::new("config");
// contracts/arbitrage-bot/src/error.rs
use cosmwasm_std::StdError;
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),
#[error("Unauthorized")]
Unauthorized {},
#[error("Arbitrage failed: {msg}")]
ArbitrageFailed { msg: String },
#[error("Insufficient profit: profit={profit}, min_profit={min_profit}")]
InsufficientProfit { profit: Uint128, min_profit: Uint128 },
#[error("Invalid arbitrage operation: {msg}")]
InvalidOperation { msg: String },
}
// contracts/arbitrage-bot/src/contract.rs
use cosmwasm_std::{
entry_point, to_binary, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env,
MessageInfo, Response, StdError, Uint128, WasmMsg,
};
use cw2::set_contract_version;
use cw20::{Cw20ExecuteMsg, Cw20QueryMsg};
use dex_types::{Asset, AssetInfo, FlashLoanExecuteMsg};
use crate::error::ContractError;
use crate::msg::{ArbitrageOp, ExecuteMsg, InstantiateMsg, QueryMsg};
use crate::state::{Config, CONFIG};
const CONTRACT_NAME: &str = "msg-dex-arbitrage-bot";
const CONTRACT_VERSION: &str = "1.0.0";
const DEFAULT_SLIPPAGE: &str = "0.05";
#[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: deps.api.addr_validate(&msg.owner)? };
CONFIG.save(deps.storage, &config)?;
Ok(Response::new()
.add_attribute("method", "instantiate")
.add_attribute("owner", msg.owner))
}
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::ExecuteArbitrage { op } => {
execute_arbitrage(deps, env, info, op)
}
ExecuteMsg::ExecuteOperation { sender, token, amount, fee, data } => {
execute_operation(deps, env, info, sender, token, amount, fee, data)
}
ExecuteMsg::Withdraw { token, amount } => {
execute_withdraw(deps, env, info, token, amount)
}
ExecuteMsg::UpdateConfig { owner } => {
execute_update_config(deps, info, owner)
}
}
}
pub fn execute_arbitrage(
deps: DepsMut,
env: Env,
_info: MessageInfo,
op: ArbitrageOp,
) -> Result<Response, ContractError> {
if op.borrow_amount.is_zero() {
return Err(ContractError::InvalidOperation {
msg: "Borrow amount cannot be zero".to_string(),
});
}
if op.sell_pair.is_empty() || op.buy_pair.is_empty() {
return Err(ContractError::InvalidOperation {
msg: "Pair addresses required".to_string(),
});
}
let op_data = to_binary(&op)?;
Ok(Response::new()
.add_message(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: op.flash_loan_pair.clone(),
msg: to_binary(&ExecuteMsg::ExecuteOperation {
sender: env.contract.address.to_string(),
token: op.borrow_token.clone(),
amount: op.borrow_amount,
fee: Uint128::zero(),
data: op_data,
})?,
funds: vec![],
}))
.add_attribute("method", "execute_arbitrage")
.add_attribute("borrow_token", op.borrow_token)
.add_attribute("borrow_amount", op.borrow_amount))
}
pub fn execute_operation(
deps: DepsMut,
env: Env,
_info: MessageInfo,
sender: String,
token: String,
amount: Uint128,
fee: Uint128,
data: Binary,
) -> Result<Response, ContractError> {
let op: ArbitrageOp = cosmwasm_std::from_binary(&data)
.map_err(|e| ContractError::InvalidOperation {
msg: format!("Failed to decode arbitrage op: {}", e),
})?;
let repay_amount = amount + fee;
let offer_asset_info = if token == "umsg" || token.starts_with("ibc/") {
AssetInfo::NativeToken { denom: token.clone() }
} else {
AssetInfo::Token { contract_addr: deps.api.addr_validate(&token)? }
};
let sell_swap_msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: op.sell_pair.clone(),
msg: to_binary(&dex_types::SwapExecuteMsg {
offer_asset: Asset { info: offer_asset_info.clone(), amount },
belief_price: None,
max_spread: Some(cosmwasm_std::Decimal::from_str(DEFAULT_SLIPPAGE).unwrap()),
to: Some(env.contract.address.to_string()),
})?,
funds: if token == "umsg" || token.starts_with("ibc/") {
vec![Coin { denom: token.clone(), amount }]
} else {
vec![]
},
});
let buy_swap_msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: op.buy_pair.clone(),
msg: to_binary(&dex_types::SwapExecuteMsg {
offer_asset: Asset {
info: AssetInfo::NativeToken { denom: "uusdc".to_string() },
amount: Uint128::zero(),
},
belief_price: None,
max_spread: Some(cosmwasm_std::Decimal::from_str(DEFAULT_SLIPPAGE).unwrap()),
to: Some(env.contract.address.to_string()),
})?,
funds: vec![],
});
let repay_msg = if token == "umsg" || token.starts_with("ibc/") {
CosmosMsg::Bank(BankMsg::Send {
to_address: sender.clone(),
assets: vec![Coin { denom: token.clone(), amount: repay_amount }],
})
} else {
let token_addr = deps.api.addr_validate(&token)?;
CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: token_addr.to_string(),
msg: to_binary(&Cw20ExecuteMsg::Transfer {
recipient: sender,
amount: repay_amount,
})?,
funds: vec![],
})
};
Ok(Response::new()
.add_message(sell_swap_msg)
.add_message(buy_swap_msg)
.add_message(repay_msg)
.add_attribute("method", "execute_operation")
.add_attribute("token", token)
.add_attribute("amount", amount)
.add_attribute("fee", fee)
.add_attribute("repay_amount", repay_amount))
}
pub fn execute_withdraw(
deps: DepsMut,
_env: Env,
info: MessageInfo,
token: String,
amount: Uint128,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
if info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
let msg: CosmosMsg = if token == "umsg" || token.starts_with("ibc/") {
CosmosMsg::Bank(BankMsg::Send {
to_address: info.sender.to_string(),
assets: vec![Coin { denom: token, amount }],
})
} else {
let token_addr = deps.api.addr_validate(&token)?;
CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: token_addr.to_string(),
msg: to_binary(&Cw20ExecuteMsg::Transfer {
recipient: info.sender.to_string(),
amount,
})?,
funds: vec![],
})
};
Ok(Response::new()
.add_message(msg)
.add_attribute("method", "withdraw")
.add_attribute("token", token)
.add_attribute("amount", amount))
}
pub fn execute_update_config(
deps: DepsMut,
info: MessageInfo,
owner: Option<String>,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
if let Some(owner) = owner {
config.owner = deps.api.addr_validate(&owner)?;
}
CONFIG.save(deps.storage, &config)?;
Ok(Response::new()
.add_attribute("method", "update_config")
.add_attribute("owner", config.owner))
}
#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result<Binary, ContractError> {
match msg {
QueryMsg::Config {} => Ok(to_binary(&CONFIG.load(deps.storage)?)?),
QueryMsg::Profit {} => Ok(to_binary("Not available")?),
}
}
// contracts/arbitrage-bot/src/lib.rs
pub mod contract;
pub mod error;
pub mod msg;
pub mod state;
pub use crate::error::ContractError;
4.3 套利合约测试
// contracts/arbitrage-bot/tests/integration.rs
use cosmwasm_std::{Addr, Binary, Empty, Uint128};
use cw_multi_test::{App, Contract, ContractWrapper, Executor};
use arbitrage_bot::contract::{execute, instantiate, query};
use arbitrage_bot::msg::{ArbitrageOp, ExecuteMsg, InstantiateMsg, QueryMsg};
fn mock_bot() -> Box<dyn Contract<Empty>> {
Box::new(ContractWrapper::new(
|d, e, i, m: ExecuteMsg| execute(d, e, i, m),
|d, e, i, m: InstantiateMsg| instantiate(d, e, i, m),
|d, e, m: QueryMsg| query(d, e, m),
))
}
#[test]
fn test_bot_instantiate() {
let mut app = App::default();
let code_id = app.store_code(mock_bot());
let addr = app.instantiate_contract(
code_id, Addr::unchecked("owner"),
&InstantiateMsg { owner: "owner".to_string() },
&[], "ArbitrageBot",
).unwrap();
let config: arbitrage_bot::state::Config = app.wrap()
.query_wasm_smart(&addr, &QueryMsg::Config {}).unwrap();
assert_eq!(config.owner, "owner");
}
#[test]
fn test_bot_withdraw_unauthorized() {
let mut app = App::default();
let code_id = app.store_code(mock_bot());
let addr = app.instantiate_contract(
code_id, Addr::unchecked("owner"),
&InstantiateMsg { owner: "owner".to_string() },
&[], "ArbitrageBot",
).unwrap();
let err = app.execute_contract(
Addr::unchecked("hacker"), addr,
&ExecuteMsg::Withdraw {
token: "umsg".to_string(),
amount: Uint128::new(1000),
},
&[],
).unwrap_err();
assert!(err.to_string().contains("Unauthorized"));
}
#[test]
fn test_bot_execute_arbitrage_zero_amount_fails() {
let mut app = App::default();
let code_id = app.store_code(mock_bot());
let addr = app.instantiate_contract(
code_id, Addr::unchecked("owner"),
&InstantiateMsg { owner: "owner".to_string() },
&[], "ArbitrageBot",
).unwrap();
let err = app.execute_contract(
Addr::unchecked("owner"), addr,
&ExecuteMsg::ExecuteArbitrage {
op: ArbitrageOp {
flash_loan_pair: "pair".to_string(),
borrow_token: "umsg".to_string(),
borrow_amount: Uint128::zero(),
sell_pair: "sell".to_string(),
sell_token: "umsg".to_string(),
buy_pair: "buy".to_string(),
buy_token: "umsg".to_string(),
min_profit: Uint128::zero(),
},
},
&[],
).unwrap_err();
assert!(err.to_string().contains("Borrow amount cannot be zero"));
}
#[test]
fn test_bot_update_config() {
let mut app = App::default();
let code_id = app.store_code(mock_bot());
let addr = app.instantiate_contract(
code_id, Addr::unchecked("owner"),
&InstantiateMsg { owner: "owner".to_string() },
&[], "ArbitrageBot",
).unwrap();
app.execute_contract(
Addr::unchecked("owner"), addr.clone(),
&ExecuteMsg::UpdateConfig { owner: Some("new_owner".to_string()) },
&[],
).unwrap();
let config: arbitrage_bot::state::Config = app.wrap()
.query_wasm_smart(&addr, &QueryMsg::Config {}).unwrap();
assert_eq!(config.owner, "new_owner");
}
5. 清算示例合约
5.1 清算流程
清算(Liquidation)是 DeFi 中的核心机制。当一个 CDP 头寸的抵押率低于阈值时,清算人可以偿还部分债务,获得抵押品(通常有折扣奖励)。
闪电贷在清算中的作用:
1. 闪电贷借入大量稳定币(如 USDC)
2. 偿还不健康的 CDP 债务
3. 获得抵押品(如 MSG,有清算折扣)
4. 将抵押品在 AMM 上卖出换回稳定币
5. 归还闪电贷借款 + 手续费
6. 保留剩余利润
┌─────────────────┐
│ 闪电贷 USDC │
│ 1,000,000 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 偿还 CDP 债务 │
│ 1,000,000 USDC │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 获得抵押品 MSG │
│ (折扣 10%) │
│ = 1,111 MSG │
└────────┬────────┘
│
▼
┌─────────────────┐
│ AMM 卖出 MSG │
│ 换回 1,050,000 │
│ USDC │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 归还闪贷 1,000,300│
│ 利润: 49,700 │
│ USDC │
└─────────────────┘
5.2 清算合约
// contracts/liquidator/Cargo.toml
[package]
name = "liquidator"
version.workspace = true
license.workspace = true
edition.workspace = true
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
cosmwasm-std.workspace = true
cw-storage-plus.workspace = true
cw2.workspace = true
cw20.workspace = true
serde.workspace = true
schemars.workspace = true
thiserror.workspace = true
cosmwasm-schema.workspace = true
dex-types = { path = "../../packages/dex-types" }
uint.workspace = true
[dev-dependencies]
cw-multi-test.workspace = true
// contracts/liquidator/src/msg.rs
use cosmwasm_std::{Addr, Binary, Decimal, Uint128};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
pub owner: String,
pub cdp_contract: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct LiquidationOp {
pub flash_loan_pair: String,
pub borrow_token: String,
pub borrow_amount: Uint128,
pub cdp_position_id: String,
pub swap_pair: String,
pub collateral_token: String,
pub min_profit: Uint128,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum ExecuteMsg {
ExecuteLiquidation { op: LiquidationOp },
ExecuteOperation {
sender: String,
token: String,
amount: Uint128,
fee: Uint128,
data: Binary,
},
Withdraw { token: String, amount: Uint128 },
UpdateConfig { owner: Option<String>, cdp_contract: Option<String> },
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum QueryMsg {
Config {},
}
// contracts/liquidator/src/state.rs
use cosmwasm_std::Addr;
use cw_storage_plus::Item;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
pub owner: Addr,
pub cdp_contract: Addr,
}
pub const CONFIG: Item<Config> = Item::new("config");
// contracts/liquidator/src/error.rs
use cosmwasm_std::StdError;
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),
#[error("Unauthorized")]
Unauthorized {},
#[error("Liquidation failed: {msg}")]
LiquidationFailed { msg: String },
#[error("Insufficient profit: {profit} < {min_profit}")]
InsufficientProfit { profit: String, min_profit: String },
}
// contracts/liquidator/src/contract.rs
use cosmwasm_std::{
entry_point, to_binary, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env,
MessageInfo, Response, StdError, Uint128, WasmMsg,
};
use cw2::set_contract_version;
use cw20::{Cw20ExecuteMsg, Cw20QueryMsg};
use dex_types::{Asset, AssetInfo, FlashLoanExecuteMsg};
use crate::error::ContractError;
use crate::msg::{ExecuteMsg, InstantiateMsg, LiquidationOp, QueryMsg};
use crate::state::{Config, CONFIG};
const CONTRACT_NAME: &str = "msg-dex-liquidator";
const CONTRACT_VERSION: &str = "1.0.0";
const DEFAULT_SLIPPAGE: &str = "0.05";
#[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: deps.api.addr_validate(&msg.owner)?,
cdp_contract: deps.api.addr_validate(&msg.cdp_contract)?,
};
CONFIG.save(deps.storage, &config)?;
Ok(Response::new()
.add_attribute("method", "instantiate")
.add_attribute("owner", msg.owner)
.add_attribute("cdp_contract", msg.cdp_contract))
}
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::ExecuteLiquidation { op } => {
execute_liquidation(deps, env, info, op)
}
ExecuteMsg::ExecuteOperation { sender, token, amount, fee, data } => {
execute_operation(deps, env, info, sender, token, amount, fee, data)
}
ExecuteMsg::Withdraw { token, amount } => {
execute_withdraw(deps, env, info, token, amount)
}
ExecuteMsg::UpdateConfig { owner, cdp_contract } => {
execute_update_config(deps, info, owner, cdp_contract)
}
}
}
pub fn execute_liquidation(
deps: DepsMut,
env: Env,
_info: MessageInfo,
op: LiquidationOp,
) -> Result<Response, ContractError> {
if op.borrow_amount.is_zero() {
return Err(ContractError::LiquidationFailed {
msg: "Borrow amount cannot be zero".to_string(),
});
}
if op.cdp_position_id.is_empty() {
return Err(ContractError::LiquidationFailed {
msg: "CDP position ID required".to_string(),
});
}
let op_data = to_binary(&op)?;
Ok(Response::new()
.add_message(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: op.flash_loan_pair.clone(),
msg: to_binary(&ExecuteMsg::ExecuteOperation {
sender: env.contract.address.to_string(),
token: op.borrow_token.clone(),
amount: op.borrow_amount,
fee: Uint128::zero(),
data: op_data,
})?,
funds: vec![],
}))
.add_attribute("method", "execute_liquidation")
.add_attribute("cdp_position", op.cdp_position_id)
.add_attribute("borrow_amount", op.borrow_amount))
}
pub fn execute_operation(
deps: DepsMut,
env: Env,
_info: MessageInfo,
sender: String,
token: String,
amount: Uint128,
fee: Uint128,
data: Binary,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let op: LiquidationOp = cosmwasm_std::from_binary(&data)
.map_err(|e| ContractError::LiquidationFailed {
msg: format!("Failed to decode liquidation op: {}", e),
})?;
let repay_amount = amount + fee;
let liquidate_msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: config.cdp_contract.to_string(),
msg: to_binary(&CdpExecuteMsg::Liquidate {
position_id: op.cdp_position_id.clone(),
amount,
})?,
funds: if token == "umsg" || token.starts_with("ibc/") {
vec![Coin { denom: token.clone(), amount }]
} else {
vec![]
},
});
let swap_msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: op.swap_pair.clone(),
msg: to_binary(&dex_types::SwapExecuteMsg {
offer_asset: Asset {
info: AssetInfo::NativeToken { denom: op.collateral_token.clone() },
amount: Uint128::zero(),
},
belief_price: None,
max_spread: Some(cosmwasm_std::Decimal::from_str(DEFAULT_SLIPPAGE).unwrap()),
to: Some(env.contract.address.to_string()),
})?,
funds: vec![],
});
let repay_msg = if token == "umsg" || token.starts_with("ibc/") {
CosmosMsg::Bank(BankMsg::Send {
to_address: sender.clone(),
assets: vec![Coin { denom: token.clone(), amount: repay_amount }],
})
} else {
let token_addr = deps.api.addr_validate(&token)?;
CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: token_addr.to_string(),
msg: to_binary(&Cw20ExecuteMsg::Transfer {
recipient: sender,
amount: repay_amount,
})?,
funds: vec![],
})
};
Ok(Response::new()
.add_message(liquidate_msg)
.add_message(swap_msg)
.add_message(repay_msg)
.add_attribute("method", "execute_operation")
.add_attribute("cdp_position", op.cdp_position_id)
.add_attribute("borrow_amount", amount)
.add_attribute("repay_amount", repay_amount))
}
mod cdp_msgs {
use cosmwasm_std::Uint128;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum CdpExecuteMsg {
Liquidate { position_id: String, amount: Uint128 },
}
}
use cdp_msgs::CdpExecuteMsg;
pub fn execute_withdraw(
deps: DepsMut,
_env: Env,
info: MessageInfo,
token: String,
amount: Uint128,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
if info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
let msg: CosmosMsg = if token == "umsg" || token.starts_with("ibc/") {
CosmosMsg::Bank(BankMsg::Send {
to_address: info.sender.to_string(),
assets: vec![Coin { denom: token, amount }],
})
} else {
let token_addr = deps.api.addr_validate(&token)?;
CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: token_addr.to_string(),
msg: to_binary(&Cw20ExecuteMsg::Transfer {
recipient: info.sender.to_string(),
amount,
})?,
funds: vec![],
})
};
Ok(Response::new()
.add_message(msg)
.add_attribute("method", "withdraw")
.add_attribute("token", token)
.add_attribute("amount", amount))
}
pub fn execute_update_config(
deps: DepsMut,
info: MessageInfo,
owner: Option<String>,
cdp_contract: Option<String>,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.owner {
return Err(ContractError::Unauthorized {});
}
if let Some(owner) = owner { config.owner = deps.api.addr_validate(&owner)?; }
if let Some(cdp) = cdp_contract { config.cdp_contract = deps.api.addr_validate(&cdp)?; }
CONFIG.save(deps.storage, &config)?;
Ok(Response::new().add_attribute("method", "update_config"))
}
#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result<Binary, ContractError> {
match msg { QueryMsg::Config {} => Ok(to_binary(&CONFIG.load(deps.storage)?)?) }
}
// contracts/liquidator/src/lib.rs
pub mod contract;
pub mod error;
pub mod msg;
pub mod state;
pub use crate::error::ContractError;
5.3 清算合约测试
// contracts/liquidator/tests/integration.rs
use cosmwasm_std::{Addr, Empty, Uint128};
use cw_multi_test::{App, Contract, ContractWrapper, Executor};
use liquidator::contract::{execute, instantiate, query};
use liquidator::msg::{ExecuteMsg, InstantiateMsg, LiquidationOp, QueryMsg};
fn mock_liquidator() -> Box<dyn Contract<Empty>> {
Box::new(ContractWrapper::new(
|d, e, i, m: ExecuteMsg| execute(d, e, i, m),
|d, e, i, m: InstantiateMsg| instantiate(d, e, i, m),
|d, e, m: QueryMsg| query(d, e, m),
))
}
#[test]
fn test_liquidator_instantiate() {
let mut app = App::default();
let code_id = app.store_code(mock_liquidator());
let addr = app.instantiate_contract(code_id, Addr::unchecked("owner"),
&InstantiateMsg { owner: "owner".to_string(), cdp_contract: "cdp_contract".to_string() },
&[], "Liquidator",
).unwrap();
let config: liquidator::state::Config = app.wrap()
.query_wasm_smart(&addr, &QueryMsg::Config {}).unwrap();
assert_eq!(config.owner, "owner");
assert_eq!(config.cdp_contract, "cdp_contract");
}
#[test]
fn test_liquidator_zero_amount_fails() {
let mut app = App::default();
let code_id = app.store_code(mock_liquidator());
let addr = app.instantiate_contract(code_id, Addr::unchecked("owner"),
&InstantiateMsg { owner: "owner".to_string(), cdp_contract: "cdp".to_string() },
&[], "Liquidator",
).unwrap();
let err = app.execute_contract(Addr::unchecked("owner"), addr,
&ExecuteMsg::ExecuteLiquidation {
op: LiquidationOp {
flash_loan_pair: "pair".to_string(), borrow_token: "uusdc".to_string(),
borrow_amount: Uint128::zero(), cdp_position_id: "pos1".to_string(),
swap_pair: "swap".to_string(), collateral_token: "umsg".to_string(),
min_profit: Uint128::zero(),
},
}, &[],
).unwrap_err();
assert!(err.to_string().contains("Borrow amount cannot be zero"));
}
#[test]
fn test_liquidator_withdraw_unauthorized() {
let mut app = App::default();
let code_id = app.store_code(mock_liquidator());
let addr = app.instantiate_contract(code_id, Addr::unchecked("owner"),
&InstantiateMsg { owner: "owner".to_string(), cdp_contract: "cdp".to_string() },
&[], "Liquidator",
).unwrap();
let err = app.execute_contract(Addr::unchecked("hacker"), addr,
&ExecuteMsg::Withdraw { token: "umsg".to_string(), amount: Uint128::new(1000) },
&[],
).unwrap_err();
assert!(err.to_string().contains("Unauthorized"));
}
6. TypeScript 集成
6.1 使用 CosmJS 执行闪电贷
// scripts/flash-loan-client.ts
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { GasPrice, Coin, StdFee } from "@cosmjs/stargate";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
import { toBinary } from "@cosmjs/cosmwasm-stargate";
export class FlashLoanClient {
private client: SigningCosmWasmClient;
private sender: string;
constructor(client: SigningCosmWasmClient, sender: string) {
this.client = client;
this.sender = sender;
}
async getFlashLoanConfig(pairAddr: string): Promise<{
flash_loan_fee_bps: number;
max_flash_loan_ratio: string;
enabled: boolean;
}> {
return this.client.queryContractSmart(pairAddr, { flash_loan_config: {} });
}
async flashLoan(
pairAddr: string,
token: string,
amount: string,
receiver: string,
data: Uint8Array,
fee?: StdFee,
): Promise<string> {
const tx = await this.client.execute(
this.sender, pairAddr,
{
flash_loan: { token, amount, receiver, data: toBinary(data) },
},
fee ?? "auto", "Flash Loan",
);
return tx.transactionHash;
}
async simulateFlashLoan(
pairAddr: string,
token: string,
amount: string,
): Promise<{ fee: string; repay_required: string }> {
const config = await this.getFlashLoanConfig(pairAddr);
const feeBps = config.flash_loan_fee_bps;
const amountBig = BigInt(amount);
const fee = amountBig * BigInt(feeBps) / BigInt(10000);
const repayRequired = amountBig + fee;
return { fee: fee.toString(), repay_required: repayRequired.toString() };
}
}
async function main() {
const mnemonic = process.env.MNEMONIC!;
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: "msg" });
const [account] = await wallet.getAccounts();
const client = await SigningCosmWasmClient.connectWithSigner(
"https://rpc.msgchain.org", wallet,
{ gasPrice: GasPrice.fromString("1000000000attoMSG") },
);
const flashLoan = new FlashLoanClient(client, account.address);
const pairAddr = "msg1pair...";
const config = await flashLoan.getFlashLoanConfig(pairAddr);
console.log("Flash loan config:", config);
const borrowAmount = "1000000000000000000"; // 1 MSG
const sim = await flashLoan.simulateFlashLoan(pairAddr, "umsg", borrowAmount);
console.log("Fee:", sim.fee, "Repay:", sim.repay_required);
const receiverAddr = "msg1receiver...";
const data = new TextEncoder().encode("arbitrage operation data");
const txHash = await flashLoan.flashLoan(pairAddr, "umsg", borrowAmount, receiverAddr, data);
console.log("Flash loan tx:", txHash);
}
main().catch(console.error);
6.2 构建接收者合约调用
// scripts/receiver-builder.ts
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { GasPrice } from "@cosmjs/stargate";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
export class FlashReceiverBuilder {
private client: SigningCosmWasmClient;
private sender: string;
constructor(client: SigningCosmWasmClient, sender: string) {
this.client = client;
this.sender = sender;
}
async deployReceiver(codeId: number, owner: string, label: string): Promise<string> {
const addr = await this.client.instantiate(this.sender, codeId, { owner }, label, "auto");
return addr;
}
async withdraw(receiverAddr: string, token: string, amount: string): Promise<string> {
const tx = await this.client.execute(this.sender, receiverAddr,
{ withdraw: { token, amount, recipient: null } }, "auto", "Withdraw Profit",
);
return tx.transactionHash;
}
}
async function deployReceiver() {
const mnemonic = process.env.MNEMONIC!;
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: "msg" });
const [account] = await wallet.getAccounts();
const client = await SigningCosmWasmClient.connectWithSigner(
"https://rpc.msgchain.org", wallet,
{ gasPrice: GasPrice.fromString("1000000000attoMSG") },
);
const builder = new FlashReceiverBuilder(client, account.address);
const receiverAddr = await builder.deployReceiver(123, account.address, "Flash Receiver v1");
console.log("Receiver deployed at:", receiverAddr);
}
deployReceiver().catch(console.error);
6.3 估算 Gas
// scripts/estimate-gas.ts
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { GasPrice } from "@cosmjs/stargate";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
export async function estimateFlashLoanGas(
pairAddr: string, token: string, amount: string,
receiverAddr: string, data: Uint8Array,
): Promise<{ gasEstimated: number; fee: string; recommendation: string }> {
const mnemonic = process.env.MNEMONIC!;
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: "msg" });
const [account] = await wallet.getAccounts();
const client = await SigningCosmWasmClient.connectWithSigner(
"https://rpc.msgchain.org", wallet,
{ gasPrice: GasPrice.fromString("1000000000attoMSG") },
);
const result = await client.simulate(account.address, [{
typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract",
value: {
sender: account.address,
contract: pairAddr,
msg: Buffer.from(JSON.stringify({
flash_loan: { token, amount, receiver: receiverAddr, data: Buffer.from(data).toString("base64") },
})).toString("base64"),
funds: [],
},
}], "");
const gasEstimated = result;
const gasAdjusted = Math.ceil(gasEstimated * 1.3);
const gasPrice = GasPrice.fromString("1000000000attoMSG");
const feeAmount = gasPrice.amount * gasAdjusted;
return {
gasEstimated,
fee: feeAmount.toString(),
recommendation: gasAdjusted > 5_000_000 ? "High gas! Consider optimizing" : "Normal gas range",
};
}
async function main() {
const result = await estimateFlashLoanGas(
"msg1pair...", "umsg", "1000000000000000000", "msg1receiver...",
new TextEncoder().encode("arbitrage"),
);
console.log("Gas estimation:", result);
}
main().catch(console.error);
6.4 完整的套利执行脚本
// scripts/arbitrage-executor.ts
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { GasPrice } from "@cosmjs/stargate";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
interface ArbitrageConfig {
flashLoanPair: string; sellPair: string; buyPair: string;
borrowToken: string; collateralToken: string;
borrowAmount: string; minProfit: string; botContract: string;
}
export class ArbitrageExecutor {
private client: SigningCosmWasmClient;
private sender: string;
constructor(client: SigningCosmWasmClient, sender: string) {
this.client = client; this.sender = sender;
}
async detectArbitrage(pairA: string, pairB: string, tokenIn: string, amount: string) {
try {
const poolA: any = await this.client.queryContractSmart(pairA, { pool: {} });
const poolB: any = await this.client.queryContractSmart(pairB, { pool: {} });
const priceA = parseFloat(poolA.assets[1].amount) / parseFloat(poolA.assets[0].amount);
const priceB = parseFloat(poolB.assets[1].amount) / parseFloat(poolB.assets[0].amount);
const simA: any = await this.client.queryContractSmart(pairA, {
simulation: { offer_asset: { info: { native_token: { denom: tokenIn } }, amount } },
});
const simB: any = await this.client.queryContractSmart(pairB, {
simulation: { offer_asset: { info: { native_token: { denom: tokenIn } }, amount } },
});
const returnFromA = parseFloat(simA.return_amount);
const returnFromB = parseFloat(simB.return_amount);
const profit = Math.abs(returnFromB - returnFromA);
const fee = parseFloat(amount) * 0.003;
return {
profitable: profit > fee + parseFloat(amount) * 0.001,
profit: (profit - fee).toFixed(6), priceA: priceA.toFixed(6), priceB: priceB.toFixed(6),
};
} catch (e) { console.error(e); return null; }
}
async executeArbitrage(config: ArbitrageConfig): Promise<string> {
const tx = await this.client.execute(this.sender, config.botContract, {
execute_arbitrage: {
op: {
flash_loan_pair: config.flashLoanPair, borrow_token: config.borrowToken,
borrow_amount: config.borrowAmount, sell_pair: config.sellPair,
sell_token: config.borrowToken, buy_pair: config.buyPair,
buy_token: config.borrowToken, min_profit: config.minProfit,
},
},
}, "auto", "Arbitrage");
return tx.transactionHash;
}
}
async function main() {
const mnemonic = process.env.MNEMONIC!;
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: "msg" });
const [account] = await wallet.getAccounts();
const client = await SigningCosmWasmClient.connectWithSigner(
"https://rpc.msgchain.org", wallet, { gasPrice: GasPrice.fromString("1000000000attoMSG") },
);
const executor = new ArbitrageExecutor(client, account.address);
const result = await executor.detectArbitrage("msg1pairA...", "msg1pairB...", "umsg", "1000000000000000000");
if (result && result.profitable) {
console.log("Arbitrage detected!", result);
const txHash = await executor.executeArbitrage({
flashLoanPair: "msg1pairA...", sellPair: "msg1pairB...", buyPair: "msg1pairA...",
borrowToken: "umsg", collateralToken: "uusdc", borrowAmount: "10000000000000000000",
minProfit: "10000000000000000", botContract: "msg1arbibot...",
});
console.log("Arbitrage tx:", txHash);
} else {
console.log("No profitable arbitrage found", result);
}
}
main().catch(console.error);
6.5 前端集成
// frontend/src/utils/flashLoanClient.ts
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { StdFee } from "@cosmjs/stargate";
export class FlashLoanFrontendClient {
private client: SigningCosmWasmClient;
private sender: string;
constructor(client: SigningCosmWasmClient, sender: string) {
this.client = client; this.sender = sender;
}
async getConfig(pairAddr: string) {
return this.client.queryContractSmart(pairAddr, { flash_loan_config: {} });
}
async getPool(pairAddr: string) {
return this.client.queryContractSmart(pairAddr, { pool: {} });
}
async estimateFee(pairAddr: string, amount: string): Promise<string> {
const config = await this.getConfig(pairAddr);
const amountBig = BigInt(amount);
const fee = amountBig * BigInt(config.flash_loan_fee_bps) / BigInt(10000);
return fee.toString();
}
async executeFlashLoan(
pairAddr: string, token: string, amount: string,
receiverAddr: string, data: string, fee?: StdFee,
): Promise<string> {
const tx = await this.client.execute(this.sender, pairAddr, {
flash_loan: { token, amount, receiver: receiverAddr, data },
}, fee ?? "auto", "Flash Loan");
return tx.transactionHash;
}
}
7. 安全考虑
7.1 重入攻击防护
重入攻击是闪贷实现中最危险的安全漏洞。攻击者可能在 execute_operation 回调期间再次调用 flash_loan,形成递归借贷。
防护措施:
// 使用状态锁防止重入
pub fn execute_flash_loan(...) -> Result<Response, ContractError> {
let flash_state = FLASH_LOAN_STATE.may_load(deps.storage)?;
if flash_state.map(|s| s.active).unwrap_or(false) {
return Err(ContractError::FlashLoanAlreadyActive {});
}
FLASH_LOAN_STATE.save(deps.storage, &FlashLoanState {
token: token_addr.clone(), amount, repay_required, active: true,
})?;
// ... send tokens, call receiver ...
}
| 防护层 | 描述 |
|---|---|
| 状态锁 | flash_state.active 防止同一合约的递归闪贷 |
| 余额验证 | 通过实际余额变化而非信任接收者返回值来验证还款 |
| Checks-Effects-Interactions | 所有状态更新在外部调用之前完成 |
| SubMsg 隔离 | 使用 reply_on_success 捕获接收者错误 |
7.2 还款验证
核心原则:绝不信任接收者合约的返回值,始终通过余额检查验证还款。
pub fn verify_repayment(deps: &DepsMut, env: &Env, token: &str, expected: Uint128) -> Result<(), ContractError> {
let balance = if token == "umsg" || token.starts_with("ibc/") {
deps.querier.query_balance(&env.contract.address, token)?.amount
} else {
let addr = deps.api.addr_validate(token)?;
let resp: cw20::BalanceResponse = deps.querier.query_wasm_smart(
&addr, &Cw20QueryMsg::Balance { address: env.contract.address.to_string() },
)?;
resp.balance
};
if balance < expected {
return Err(ContractError::FlashLoanRepaymentNotReceived { expected, received: balance });
}
Ok(())
}
7.3 原子执行保证
闪电贷的原子性由 CosmWasm 的交易执行模型保证:
单笔交易
├── flash_loan()
│ ├── 转账给 receiver
│ ├── receiver.execute_operation()
│ │ ├── 使用借来的资金
│ │ └── 归还借款 + 手续费
│ └── 验证余额
└── 如果任何步骤失败 → 全部回滚
如果 execute_operation 中的任何消息失败,或最终余额检查失败,整个交易自动回滚。
7.4 闪电贷费用经济学
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 闪贷费率 | 0.3% (30 bps) | 与 Swap 手续费一致 |
| 最大借款比例 | 50% 流动性 | 防止 LP 流动性被完全抽走 |
| 最低费率 | 0.05% | 确保攻击无利可图 |
费用计算:
fee = 1_000_000 * 30 / 10000 = 3_000 USDC
repay_required = 1_000_000 + 3_000 = 1_003_000 USDC
7.5 已知攻击向量与防护
| 攻击向量 | 描述 | 防护措施 |
|---|---|---|
| 重入攻击 | 在 execute_operation 中再次调用 flash_loan |
状态锁 active: bool |
| 虚假还款 | 接收者返回成功但不实际转回代币 | 余额检查而非信任返回值 |
| 价格操纵 | 通过大额借贷操纵 AMM 价格 | 最大借款比例限制(50%) |
| 闪电贷攻击 | 多层嵌套闪贷耗尽流动性 | 状态锁 + 每池单次闪贷 |
| 手续费逃逸 | 试图绕过手续费 | 在转出前计算并记录应还金额 |
| 递归调用 | 接收者合约递归调用自身 | CosmWasm 的 Gas 限制天然阻止 |
| 时间窗口攻击 | 多笔交易组合利用闪贷 | 原子执行保证 |
| 预言机操纵 | 通过闪贷操纵价格预言机 | 使用 TWAP 而非即时价格 |
7.6 数学边界测试
// contracts/pair/tests/flash_loan_math_tests.rs
use cosmwasm_std::Uint128;
#[test]
fn test_flash_loan_fee_calculation() {
let fee_bps: u16 = 30;
let amount = Uint128::new(1_000_000);
let fee = amount * Uint128::from(fee_bps as u64) / Uint128::from(10000u64);
assert_eq!(fee, Uint128::new(3_000));
let large = Uint128::new(1_000_000_000_000_000_000u128);
let large_fee = large * Uint128::from(fee_bps as u64) / Uint128::from(10000u64);
assert_eq!(large_fee, Uint128::new(3_000_000_000_000_000_000u128));
let min = Uint128::new(1);
let min_fee = min * Uint128::from(fee_bps as u64) / Uint128::from(10000u64);
assert_eq!(min_fee, Uint128::zero());
let zero_bps: u16 = 0;
let fee_zero = amount * Uint128::from(zero_bps as u64) / Uint128::from(10000u64);
assert_eq!(fee_zero, Uint128::zero());
let max_bps: u16 = 10000;
let fee_full = amount * Uint128::from(max_bps as u64) / Uint128::from(10000u64);
assert_eq!(fee_full, amount);
}
#[test]
fn test_flash_loan_max_amount() {
let reserve = Uint128::new(1_000_000);
let max_ratio = cosmwasm_std::Decimal::percent(50);
let max_amount = reserve * max_ratio.numerator() / Uint128::from(max_ratio.denominator());
assert_eq!(max_amount, Uint128::new(500_000));
}
#[test]
fn test_flash_loan_repay_overflow_safe() {
let amount = Uint128::new(1_000_000);
let fee = Uint128::new(3_000);
let repay = amount.checked_add(fee).unwrap();
assert_eq!(repay, Uint128::new(1_003_000));
assert!(Uint128::MAX.checked_add(Uint128::new(1)).is_err());
}
7.7 安全清单
## 闪贷部署安全检查清单
### 合约层
- [ ] FLASH_LOAN_STATE 使用 active 锁防止重入
- [ ] 所有金额计算使用 checked_* 方法防溢出
- [ ] 还款验证使用余额检查而非接收者返回值
- [ ] 最大借款比例限制(默认 50%)
- [ ] 闪贷费率硬编码或由 owner 控制
- [ ] 接收者合约错误导致交易回滚
- [ ] 不支持空金额闪贷
- [ ] 不支持无效 token 闪贷
- [ ] 不支持未启用闪贷的 Pair
### 接收者合约层
- [ ] execute_operation 必须是可重入安全的
- [ ] 所有外部调用在状态更新之后
- [ ] 利润提取有权限控制
- [ ] 不持有永久资金(用完即还)
### 操作层
- [ ] 部署前使用 cw-multi-test 完整测试
- [ ] 主网部署前在测试网验证
- [ ] 监控异常 flash_loan 调用频率
- [ ] 设置合理的 gas 限制防止无限循环
### 审计
- [ ] 合约代码经过专业审计
- [ ] 测试覆盖率达到关键路径
- [ ] 数学运算经过边界值测试
- [ ] 重入攻击场景测试
8. 附录
A. 闪贷流程序列图
┌──────┐ ┌──────────┐ ┌──────────────┐ ┌──────────┐
│ User │ │ AMM Pair │ │ Receiver │ │ DEX │
│ │ │ (Flash) │ │ (Arb Bot) │ │ (Market) │
└──┬───┘ └────┬─────┘ └──────┬───────┘ └────┬─────┘
│ │ │ │
│ flash_loan() │ │ │
│─────────────>│ │ │
│ │ transfer token │ │
│ │─────────────────>│ │
│ │ │ │
│ │ execute_op(data) │ │
│ │─────────────────>│ │
│ │ │ swap(sell) │
│ │ │──────────────────>│
│ │ │ return USDC │
│ │ │<──────────────────│
│ │ │ │
│ │ │ swap(buy) │
│ │ │──────────────────>│
│ │ │ return MSG │
│ │ │<──────────────────│
│ │ │ │
│ │ repay (MSG) │ │
│ │<─────────────────│ │
│ │ │ │
│ │ verify balance │ │
│ │ (or revert) │ │
│ │─────────────────> │
│ │ │ │
│ return │ │ │
│<─────────────│ │ │
│ │ │ │
B. 气体消耗参考
| 操作 | 估计 Gas | 费用 (1,000,000,000 attoMSG/gas) |
|---|---|---|
| 闪电贷(10,000 USDC) | ~800,000 | 20,000 umsg |
| 闪电贷 + 一次交换 | ~1,200,000 | 30,000 umsg |
| 闪电贷 + 两次交换 | ~1,500,000 | 37,500 umsg |
| 闪电贷 + 清算 + 交换 | ~2,000,000 | 50,000 umsg |
C. Python 模拟脚本
# scripts/simulate_flash_loan.py
"""
MSG Chain 闪电贷模拟器
用于在本地测试闪贷逻辑和收益分析
"""
from dataclasses import dataclass
from decimal import Decimal, getcontext
getcontext().prec = 78
@dataclass
class PoolState:
reserve_0: Decimal
reserve_1: Decimal
def k(self) -> Decimal:
return self.reserve_0 * self.reserve_1
def swap(self, amount_in: Decimal, token_in: int, fee_bps: int = 30) -> Decimal:
fee_mult = Decimal(10000 - fee_bps) / Decimal(10000)
if token_in == 0:
amt_w_fee = amount_in * fee_mult
numerator = amt_w_fee * self.reserve_1
denominator = self.reserve_0 + amt_w_fee
amount_out = numerator / denominator
self.reserve_0 += amount_in
self.reserve_1 -= amount_out
return amount_out
else:
amt_w_fee = amount_in * fee_mult
numerator = amt_w_fee * self.reserve_0
denominator = self.reserve_1 + amt_w_fee
amount_out = numerator / denominator
self.reserve_1 += amount_in
self.reserve_0 -= amount_out
return amount_out
def simulate_arbitrage(pool_a, pool_b, borrow_amount, flash_fee_bps=30, swap_fee_bps=30):
print(f"Pool A: MSG={pool_a.reserve_0:.4f}, USDC={pool_a.reserve_1:.4f}")
print(f"Pool B: MSG={pool_b.reserve_0:.4f}, USDC={pool_b.reserve_1:.4f}")
price_a = pool_a.reserve_1 / pool_a.reserve_0
price_b = pool_b.reserve_1 / pool_b.reserve_0
print(f"Prices: A={price_a:.6f}, B={price_b:.6f}")
if price_b > price_a:
sell_pool, buy_pool = pool_b, pool_a
else:
sell_pool, buy_pool = pool_a, pool_b
fee = borrow_amount * Decimal(flash_fee_bps) / Decimal(10000)
repay = borrow_amount + fee
print(f"Borrow: {borrow_amount:.4f}, Fee: {fee:.6f}, Repay: {repay:.6f}")
usdc = sell_pool.swap(borrow_amount, 0, swap_fee_bps)
print(f"Sold MSG -> {usdc:.4f} USDC")
usdc_use = usdc * Decimal("0.99")
fee_mult = Decimal(10000 - swap_fee_bps) / Decimal(10000)
amt_w_fee = usdc_use * fee_mult
msg_bought = amt_w_fee * buy_pool.reserve_0 / (buy_pool.reserve_1 + amt_w_fee)
buy_pool.swap(usdc_use, 1, swap_fee_bps)
print(f"Bought MSG: {msg_bought:.4f}")
if msg_bought >= repay:
profit = msg_bought - repay
print(f"Profit: {profit:.6f} MSG (${profit * price_a:.2f})")
return {"success": True, "profit": float(profit)}
else:
shortfall = repay - msg_bought
print(f"LOSS: shortfall {shortfall:.6f}")
return {"success": False, "shortfall": float(shortfall)}
if __name__ == "__main__":
a = PoolState(Decimal("100000"), Decimal("95000"))
b = PoolState(Decimal("100000"), Decimal("105000"))
result = simulate_arbitrage(a, b, Decimal("10000"))
D. 部署工作流
# 1. 构建所有合约(包含闪贷修改)
cd msg-dex
cargo wasm -p pair
cargo wasm -p flash-receiver
cargo wasm -p arbitrage-bot
cargo wasm -p liquidator
# 2. 运行测试
cargo test -p pair flash_loan
cargo test -p flash-receiver
cargo test -p arbitrage-bot
cargo test -p liquidator
# 3. 优化 WASM
for contract in pair flash-receiver arbitrage-bot liquidator; do
wasm-opt -Os target/wasm32-unknown-unknown/release/${contract}.wasm \
-o artifacts/${contract}.wasm
done
# 4. 部署 Pair(启用闪贷)
msgd tx wasm instantiate $PAIR_CODE_ID \
'{"asset_infos":[{"native_token":{"denom":"umsg"}},{"native_token":{"denom":"uusdc"}}],
"factory_addr":"msg1factory...","lp_token_code_id":$LP_CODE_ID,
"fee_bps":30,"flash_loan_fee_bps":30,"flash_loan_enabled":true}' \
--from deployer --label "MSG-DEX-Pair-Flash" --gas-prices 1000000000attoMSG --gas auto -y
# 5. 部署闪贷接收者合约
msgd tx wasm instantiate $RECEIVER_CODE_ID \
'{"owner":"msg1owner..."}' \
--from deployer --label "Flash-Receiver-v1" --gas-prices 1000000000attoMSG --gas auto -y
# 6. 部署套利机器人
msgd tx wasm instantiate $BOT_CODE_ID \
'{"owner":"msg1owner..."}' \
--from deployer --label "Arbitrage-Bot-v1" --gas-prices 1000000000attoMSG --gas auto -y
本文档为 MSG Chain AMM DEX 的闪电贷功能完整实现指南
依赖基础 AMM 实现参见 AMM_DEX完整实现.md
本文档基于 MSG Chain 代码库核实的技术事实。
白皮书系统: https://msgchain.org/whitepaper/
