dApp Docs/闪电贷FlashLoan实现
Development reference. Not independently verified for production.

MSG Chain AMM 闪电贷 (Flash Loan) 完整实现指南

基于 CosmWasm 的闪贷实现 — 无抵押借贷,单笔交易内完成借出与归还
适用链:MSG Chain (msg-chain-1) | Bech32: msg | 精度:18 位小数
主网状态:No-Go
依赖:AMM DEX Pair 合约(参见 AMM_DEX完整实现.md)


目录

  1. 概述
  2. 闪电贷合约实现
  3. AMM 集成
  4. 套利示例合约
  5. 清算示例合约
  6. TypeScript 集成
  7. 安全考虑
  8. 附录

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 合约的基础上,需要添加以下支持:

  1. ExecuteMsg 新增 FlashLoan 变体
  2. 新增 execute_flash_loan 函数
  3. 新增 flash loan 状态存储(用于验证偿还)
  4. 新增查询接口 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/