dApp Docs/AMM V3(集中流动性)实现
Development reference. Not independently verified for production.

MSG Chain AMM V3 集中流动性做市商实现指南

基于 CosmWasm 的 Uniswap V3 风格集中流动性自动化做市商(CLAMM)完整实现
适用链:MSG Chain (msg-chain-1) | 共识:Round-Robin + DAR | 签名:Dilithium-5
代币精度:18 位小数 | Gas 价格:1,000,000,000 attoMSG/gas
作者:MSG Chain 开发团队
主网状态:No-Go


目录

  1. 概述与架构设计
  2. 数学原理
  3. Tick 与 Position 管理
  4. 核心交换逻辑
  5. 流动性管理
  6. 多费率层级
  7. Oracle 集成
  8. 前端集成
  9. 完整示例
  10. 附录

1. 概述与架构设计

1.1 什么是集中流动性 AMM?

传统 Uniswap V2 的恒定乘积公式 x * y = k 将流动性均匀分布在 (0, ∞) 的价格区间上。这意味着大量资金被闲置在永远不会被交易到的价格区间内。Uniswap V3 引入了集中流动性(Concentrated Liquidity),允许流动性提供者(LP)将资金集中在特定的价格范围 [pa, pb] 内,从而大幅提高资本效率。

核心思想:LP 可以选择一个价格区间来提供流动性。当价格在区间内时,LP 的资金被充分利用;当价格离开区间时,LP 的流动性变为单一资产(全部为 token0 或 token1),不再赚取交易费。

1.2 Uniswap V3 vs V2 对比

特性 Uniswap V2 (恒定乘积) Uniswap V3 (集中流动性)
定价曲线 x * y = k 全区间 (x + L/√pb)(y + L√pa) = L² 区间内
资本效率 低(资金分散在全区间) 高(可达 V2 的 4000 倍)
流动性范围 (0, ∞) [pa, pb] 自定义
价格刻度 连续 离散 Tick(如 1bps 间隔)
LP 仓位 池级别单一仓位 每个 [lowerTick, upperTick] 一个仓位
LP Token ERC-20/CW20 可替代 ERC-721/CW721 不可替代(每个仓位 NFT)
费率层级 单一费率(0.3%) 多费率(0.05%/0.30%/1.00%)
预言机 简单累积价格 增强 TWAP(支持更长时间窗口)
手续费计算 全局累加 基于 Tick 的外推增长(tick-based fee growth)

1.3 MSG Chain 特性

参数 值
Chain ID msg-chain-1
Bech32 前缀 msg
CoinType 118
原生代币 umsg(18 位小数)
Gas 价格 1,000,000,000 attoMSG/gas
出块时间 5 秒
共识机制 Round-Robin + DAR (Dynamic Awareness Routing)
签名算法 Dilithium-5(后量子密码学)
智能合约 CosmWasm 1.x
合约存储上限 128 KB(WASM 二进制)

1.4 合约架构总览

+------------------------------------------------------------------+
|                    CLAMM Frontend (React + Vite)                   |
+------------------------------------------------------------------+
|                    Router Contract (V3)                            |
|         多跳交换 / 价格限价 / 拆单 / 流动性管理                     |
+------------------+------------------+-----------------------------+
|   Pool USDC-MSG   |   Pool MSG-WETH  |   Pool USDC-WETH           |
|   V3 集中流动性    |   V3 集中流动性   |   V3 集中流动性             |
|   费率: 0.05%     |   费率: 0.30%    |   费率: 1.00%              |
|   Tick 0->...     |   Tick 0->...    |   Tick 0->...              |
+------------------+------------------+-----------------------------+
|                    Factory (V3)                                    |
|          创建 Pool / 存储费率层级 / 追踪所有 Pool                   |
+------------------+------------------+-----------------------------+
|                    NFT Descriptor                                  |
|      Position NFT 元数据(JSON on-chain SVG 生成)                  |
+------------------------------------------------------------------+
|                    MSG Chain (CosmWasm 1.x)                        |
+------------------------------------------------------------------+

1.5 核心概念

Tick(价格刻度):离散化的价格点。每个 Tick 对应一个价格 p(i) = 1.0001^i。相邻 Tick 的价格差为 0.01%(1bp)。价格以 sqrt(P) 即 √p 的形式存储和计算。

Position(仓位):LP 在一个 [lowerTick, upperTick] 区间内提供的流动性。每个仓位由 NFT 标识,记录流动性数量、已赚手续费等信息。

Swap(交换):交易在 Tick 之间进行。当价格跨越一个已初始化的 Tick 时,流动性会发生变化(流动性从该 Tick 中被移除或添加)。

1.6 MSG Chain V3 AMM 设计决策

  1. Tick 间距:支持 1、10、60、200 等 tick spacing,对应不同费率层级
  2. Token 精度:所有内部计算使用 Uint256(通过 uint crate),适应 18 位小数
  3. Gas 优化:Tick 信息存储在 cw-storage-plus 的 Map 中,使用复合 key (pool_id, tick_index)
  4. Position NFT:使用 CW721 标准表示仓位所有权
  5. Fee 计算:基于 tick 的 feeGrowthOutside 累加机制,每次跨 tick 时更新

1.7 项目结构

msg-clamm/
+-- Cargo.toml
+-- contracts/
|   +-- factory_v3/
|   |   +-- Cargo.toml
|   |   +-- src/
|   |       +-- lib.rs
|   |       +-- contract.rs
|   |       +-- state.rs
|   |       +-- msg.rs
|   |       +-- error.rs
|   +-- pool_v3/
|   |   +-- Cargo.toml
|   |   +-- src/
|   |       +-- lib.rs
|   |       +-- contract.rs
|   |       +-- state.rs
|   |       +-- msg.rs
|   |       +-- error.rs
|   |       +-- math.rs
|   |       +-- tick.rs
|   |       +-- position.rs
|   |       +-- swap.rs
|   |       +-- fee.rs
|   |       +-- oracle.rs
|   +-- nft_descriptor/
|   |   +-- Cargo.toml
|   |   +-- src/
|   |       +-- lib.rs
|   |       +-- contract.rs
|   +-- router_v3/
|       +-- Cargo.toml
|       +-- src/
|           +-- lib.rs
|           +-- contract.rs
|           +-- state.rs
|           +-- msg.rs
|           +-- error.rs
+-- packages/
|   +-- clamm-types/
|       +-- Cargo.toml
|       +-- src/
|           +-- lib.rs
|           +-- math.rs
|           +-- tick_math.rs
+-- ts-sdk/
|   +-- package.json
|   +-- src/
|       +-- index.ts
|       +-- pool.ts
|       +-- position.ts
|       +-- tick.ts
|       +-- math.ts
|       +-- router.ts
+-- scripts/
|   +-- deploy-v3.ts
+-- frontend/
    +-- package.json
    +-- tsconfig.json
    +-- src/
        +-- App.tsx
        +-- components/
        |   +-- PoolCreator.tsx
        |   +-- AddLiquidity.tsx
        |   +-- RemoveLiquidity.tsx
        |   +-- SwapPanel.tsx
        |   +-- PriceChart.tsx
        +-- hooks/
            +-- useClamm.ts

2. 数学原理

2.1 虚拟准备金(Virtual Reserves)

在 V3 中,LP 只需在 [pa, pb] 内提供流动性。合约维护虚拟准备金的概念,使得实际余额满足:

(x + L/√pb)(y + L√pa) = L²

其中:

当当前价格 p 在区间内时(pa ≤ p ≤ pb),两种代币都存在于池中:

x_real = L * (√pb - √p) / (√p * √pb)
y_real = L * (√p - √pa)

当 p = pa(价格到达下限),仓位全部为 token1:

x_real = L * (√pb - √pa) / √pb
y_real = 0

当 p = pb(价格到达上限),仓位全部为 token0:

x_real = 0
y_real = L * (√pb - √pa)

2.2 流动性计算

流动性 L 是集中流动性 AMM 的核心度量。给定价格上限 pb 和下限 pa,以及提供的 amount0 和 amount1:

从 token0 计算 L(当 p ≤ pb):

L = amount0 * √p * √pb / (√pb - √p)

从 token1 计算 L(当 p ≥ pa):

L = amount1 / (√p - √pa)

当 p 在区间内,需要同时满足两个不等式,实际 L 取两个计算结果的最小值:

L = min(
    amount0 * √p * √pb / (√pb - √p),
    amount1 / (√p - √pa)
)

实际需要的 token 数量(给定 L):

amount0_required = L * (√pb - √p) / (√p * √pb)
amount1_required = L * (√p - √pa)

2.3 Tick 系统

价格被离散化为 Tick。每个 Tick 索引 i 对应的价格为:

p(i) = 1.0001^i
sqrt_p(i) = sqrt(1.0001^i) = 1.0001^(i/2)

Tick 间距(tick spacing)确保只有 tick_index % tick_spacing == 0 的 Tick 可以被初始化。

费率层级 Tick Spacing 价格跳动
0.05% 10 ~0.10%
0.30% 60 ~0.60%
1.00% 200 ~2.00%

价格与 Tick 的转换:

// 从 tick 到 sqrtPrice (sqrt(p))
pub fn tick_to_sqrt_price(tick: i32) -> Uint256 {
    // sqrtPrice = 1.0001^(tick/2)
    // 使用 Q64.96 定点数表示
    // 1.0001^(1/2) = 1.00004999875...
    let ratio = if tick >= 0 {
        mul_by_1_0001_pow(tick) // 1.0001^tick
    } else {
        div_by_1_0001_pow(-tick)
    };
    // sqrt = sqrt(ratio)
    sqrt(ratio)
}

Tick 的数学性质:

  1. p(i) * p(-i) = 1(对称性)
  2. p(i + 1) / p(i) = 1.0001(等比数列)
  3. sqrt_p(i + 1) / sqrt_p(i) = sqrt(1.0001)

2.4 核心公式推导

假设当前价格 p(pa < p < pb),流动性 L:

从实际余额反推虚拟准备金:

虚拟 x_reserve = x_real + L/√pb
虚拟 y_reserve = y_real + L√pa

恒等式:

(x_real + L/√pb) * (y_real + L√pa) = L²

交换公式(token0 -> token1,输入 dx,输出 dy):

当价格在 pa 和 pb 之间移动时:

dy = L * (√p_after - √p_before)

其中 √p_after 由下式决定:
dx + L/√pb = L² / (y + L√pa)

实际计算步骤:

  1. 根据 dx 计算新的 √p_next
  2. 计算实际输出 dy = L * (√p_next - √p_current)
  3. 如果 √p_next 超出当前 Tick 范围,则需要跨 Tick

2.5 手续费计算模型

V3 的手续费计算基于 feeGrowth 的累加。

全局累加器:

feeGrowthGlobal0 = Σ(手续费 token0 / L)  per unit of liquidity
feeGrowthGlobal1 = Σ(手续费 token1 / L)  per unit of liquidity

Tick 级别的 feeGrowthOutside:

每个 Tick 存储 feeGrowthOutside0 和 feeGrowthOutside1,表示从该 Tick 另一侧("外侧")累积的 fee growth。

对于 tick i:

仓位内部 feeGrowth(Position 已赚手续费):

对于 [lowerTick, upperTick] 区间内的仓位:

feeGrowthInside0 = feeGrowthGlobal0 - feeGrowthBelow(lowerTick) - feeGrowthAbove(upperTick)
feeGrowthInside1 = feeGrowthGlobal1 - feeGrowthBelow(lowerTick) - feeGrowthAbove(upperTick)

仓位已赚手续费:

tokensOwed0 = liquidity * (feeGrowthInside0 - lastFeeGrowthInside0)
tokensOwed1 = liquidity * (feeGrowthInside1 - lastFeeGrowthInside1)

2.6 价格计算精度

所有内部价格使用 Q64.96 格式的 Uint256 表示 sqrtPrice:

sqrtPrice_x96 = sqrtPrice * 2^96
变量 精度 说明
sqrtPrice Q64.96 价格平方根的定点数表示
liquidity Uint128 流动性量
amount Uint128 代币数量(18 位小数)
tick i32 Tick 索引
feeGrowth Uint128 每单位流动性的累加费用

2.7 关键常数

// 数学常数
pub const MIN_SQRT_RATIO: Uint256 = Uint256::from_u128(4295128739u128);
pub const MAX_SQRT_RATIO: Uint256 = Uint256::from_u128(1461446703485210103287273052203988822378723970342u128);

// sqrt(1.0001) 在 Q64.96 中的值
pub const SQRT_1_0001: Uint256 = Uint256::from_u128(79228162514264337593543950336u128);

// Tick 范围
pub const MIN_TICK: i32 = -887272;
pub const MAX_TICK: i32 = 887272;

// 最小流动性
pub const MINIMUM_LIQUIDITY: Uint128 = Uint128::new(1000);

// 费率常量
pub const FEE_RATE_DENOMINATOR: Uint128 = Uint128::new(1_000_000);

2.8 数学运算库

// packages/clamm-types/src/math.rs
use cosmwasm_std::Uint128;
use uint::construct_uint;

construct_uint! {
    pub struct Uint256(4);
}

construct_uint! {
    pub struct Uint512(8);
}

// Q64.96 定点数:整数部分 64 位,小数部分 96 位
// sqrtPrice = Uint256 / 2^96

/// 计算两个 Q64.96 数的乘积,结果保持 Q64.96 精度
pub fn mul_div_q64_96(a: Uint256, b: Uint256) -> Uint256 {
    let product = a.checked_mul(b).unwrap();
    product >> 96
}

/// 计算 floor(sqrt(x)),x 为 Uint256
pub fn sqrt_uint256(x: Uint256) -> Uint256 {
    if x == Uint256::zero() {
        return Uint256::zero();
    }
    let mut z = (x >> 1) + Uint256::from(1);
    let mut y = x;
    loop {
        let q = x / z;
        let t = z + q;
        let z_next = t >> 1;
        if z_next >= z {
            break;
        }
        z = z_next;
        y = z;
    }
    y
}

/// 将 sqrtPrice (Q64.96) 转换为 tick
pub fn sqrt_price_to_tick(sqrt_price: Uint256) -> i32 {
    let mut lo = MIN_TICK;
    let mut hi = MAX_TICK;
    while lo < hi {
        let mid = (lo + hi + 1) / 2;
        let mid_price = tick_to_sqrt_price(mid);
        if mid_price <= sqrt_price {
            lo = mid;
        } else {
            hi = mid - 1;
        }
    }
    lo
}

/// 将 tick 转换为 sqrtPrice (Q64.96)
pub fn tick_to_sqrt_price(tick: i32) -> Uint256 {
    if tick > 0 {
        let abs_tick = tick as u32;
        let mut ratio = if abs_tick & 0x1 != 0 {
            Uint256::from(21262057326194052294356743744230968u128)
        } else {
            Uint256::from(79228162514264337593543950336u128)
        };
        if abs_tick & 0x2 != 0 {
            ratio = (ratio * Uint256::from(804169720718113877238370874603260u128)) >> 128;
        }
        if abs_tick & 0x4 != 0 {
            ratio = (ratio * Uint256::from(802856599108779779135036054749718u128)) >> 128;
        }
        if abs_tick & 0x8 != 0 {
            ratio = (ratio * Uint256::from(797350361246628876568072490621075u128)) >> 128;
        }
        if abs_tick & 0x10 != 0 {
            ratio = (ratio * Uint256::from(786381929995588107553761952002662u128)) >> 128;
        }
        if abs_tick & 0x20 != 0 {
            ratio = (ratio * Uint256::from(764784974711334370189364370048313u128)) >> 128;
        }
        if abs_tick & 0x40 != 0 {
            ratio = (ratio * Uint256::from(723018436537640062929850140646399u128)) >> 128;
        }
        if abs_tick & 0x80 != 0 {
            ratio = (ratio * Uint256::from(652608310121190206266376055010849u128)) >> 128;
        }
        if abs_tick & 0x100 != 0 {
            ratio = (ratio * Uint256::from(532280927244527283066559677119298u128)) >> 128;
        }
        if abs_tick & 0x200 != 0 {
            ratio = (ratio * Uint256::from(358660116756258056884279001389902u128)) >> 128;
        }
        if abs_tick & 0x400 != 0 {
            ratio = (ratio * Uint256::from(162530925557732498996388337167836u128)) >> 128;
        }
        if abs_tick & 0x800 != 0 {
            ratio = (ratio * Uint256::from(33396895147082460673440479379980u128)) >> 128;
        }
        ratio
    } else if tick < 0 {
        let abs_tick = (-tick) as u32;
        let mut ratio = if abs_tick & 0x1 != 0 {
            Uint256::from(37272492901916960632105065688754328u128)
        } else {
            Uint256::from(79228162514264337593543950336u128)
        };
        ratio = (Uint256::from(1u128) << 192) / ratio;
        ratio
    } else {
        Uint256::from(79228162514264337593543950336u128) // sqrt(1) = 1 * 2^96
    }
}

/// 获取 tick 对应的 sqrtPrice 的下界(向下取整到最近的 tick spacing 倍数)
pub fn tick_floor(tick: i32, tick_spacing: i32) -> i32 {
    let remainder = tick % tick_spacing;
    if remainder < 0 {
        tick - remainder - tick_spacing
    } else {
        tick - remainder
    }
}

/// 获取 tick 对应的 sqrtPrice 的上界(向上取整到最近的 tick spacing 倍数)
pub fn tick_ceil(tick: i32, tick_spacing: i32) -> i32 {
    let remainder = tick % tick_spacing;
    if remainder == 0 {
        tick
    } else if remainder > 0 {
        tick + tick_spacing - remainder
    } else {
        tick - remainder
    }
}

/// FullMul trait: Uint128 * Uint128 -> Uint256
pub trait FullMul {
    fn full_mul(self, other: Self) -> Uint256;
}

impl FullMul for Uint128 {
    fn full_mul(self, other: Uint128) -> Uint256 {
        Uint256::from(self.u128()) * Uint256::from(other.u128())
    }
}

impl Uint256 {
    /// 转换为 Uint128(截断低 128 位)
    pub fn to_uint128(&self) -> Uint128 {
        let bytes = &self.to_le_bytes()[..16];
        let mut arr = [0u8; 16];
        arr.copy_from_slice(bytes);
        Uint128::from_le_bytes(arr)
    }

    /// 从 Q64.96 转换为实际值的 Uint128
    pub fn from_q64_96(&self) -> Uint128 {
        let divisor = Uint256::from(1u128 << 96);
        let result = self / divisor;
        result.to_uint128()
    }

    /// 将 Uint128 转换为 Q64.96
    pub fn to_q64_96(val: Uint128) -> Uint256 {
        Uint256::from(val.u128()) * Uint256::from(1u128 << 96)
    }
}

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

    #[test]
    fn test_tick_to_sqrt_price_at_zero() {
        let price = tick_to_sqrt_price(0);
        assert_eq!(price, Uint256::from(79228162514264337593543950336u128));
    }

    #[test]
    fn test_tick_to_sqrt_price_positive() {
        let price = tick_to_sqrt_price(100);
        assert!(price > Uint256::from(79228162514264337593543950336u128));
    }

    #[test]
    fn test_tick_floor() {
        assert_eq!(tick_floor(105, 60), 60);
        assert_eq!(tick_floor(60, 60), 60);
        assert_eq!(tick_floor(0, 60), 0);
        assert_eq!(tick_floor(-5, 60), -60);
    }

    #[test]
    fn test_tick_ceil() {
        assert_eq!(tick_ceil(105, 60), 120);
        assert_eq!(tick_ceil(60, 60), 60);
        assert_eq!(tick_ceil(-5, 60), 0);
    }

    #[test]
    fn test_sqrt_price_to_tick_roundtrip() {
        for tick in [-887200, -10000, -100, 0, 100, 10000, 887200].iter() {
            let price = tick_to_sqrt_price(*tick);
            let recovered = sqrt_price_to_tick(price);
            assert_eq!(recovered, *tick);
        }
    }

    #[test]
    fn test_mul_div_q64_96() {
        let a = Uint256::from(79228162514264337593543950336u128);
        let b = Uint256::from(79228162514264337593543950336u128);
        let result = mul_div_q64_96(a, b);
        assert_eq!(result, Uint256::from(79228162514264337593543950336u128));
    }
}

3. Tick 与 Position 管理

3.1 Tick 数据结构

每个 Tick 存储以下信息:

// contracts/pool_v3/src/tick.rs
use cosmwasm_std::{StdResult, Storage, Uint128};
use cw_storage_plus::Map;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

/// Tick 信息
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Tick {
    /// 该 tick 上所有仓位的总流动性
    pub liquidity_gross: Uint128,
    /// 当价格从下方穿过该 tick 时,流动性净变化量
    /// 正数:跨过该 tick 时流动性增加(从下往上)
    /// 负数:跨过该 tick 时流动性减少(从上往下)
    pub liquidity_net: Int128,
    /// token0 的 fee growth outside(该 tick 外部一侧)
    pub fee_growth_outside_0: Uint128,
    /// token1 的 fee growth outside
    pub fee_growth_outside_1: Uint128,
}

/// Int128 包装(CosmWasm 原生没有 Int128,使用自定义实现)
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Int128(i128);

impl Int128 {
    pub fn new(val: i128) -> Self {
        Int128(val)
    }

    pub fn to_i128(&self) -> i128 {
        self.0
    }

    pub fn zero() -> Self {
        Int128(0)
    }
}

impl From<i128> for Int128 {
    fn from(val: i128) -> Self {
        Int128(val)
    }
}

/// Tick 信息存储(每个池子有自己的 Map)
/// key: (pool_addr, tick_index)
pub const TICKS: Map<(&Addr, i32), Tick> = Map::new("ticks");

/// 已初始化 Tick 的位图
/// 使用 u128 作为 bitmap 存储(每 128 个 tick 一个 bucket)
pub const TICK_BITMAP: Map<(i32, i32), Uint128> = Map::new("tick_bitmap");
// key: (pool_addr, word_pos) -> Uint128 bitmap
// word_pos = tick_index / 128

3.2 Tick 管理器

// contracts/pool_v3/src/tick.rs (continued)
use cosmwasm_std::Addr;

/// Tick 管理操作
pub struct TickManager;

impl TickManager {
    /// 初始化一个 tick(设置流动性)
    pub fn initialize_tick(
        storage: &mut dyn Storage,
        pool_addr: &Addr,
        tick_index: i32,
    ) -> StdResult<()> {
        let tick = Tick {
            liquidity_gross: Uint128::zero(),
            liquidity_net: Int128::zero(),
            fee_growth_outside_0: Uint128::zero(),
            fee_growth_outside_1: Uint128::zero(),
        };
        TICKS.save(storage, (pool_addr, tick_index), &tick)?;

        // 在位图中标记
        let word_pos = tick_index >> 7; // tick_index / 128
        let bit_pos = tick_index & 0x7F; // tick_index % 128
        let mut bitmap = TICK_BITMAP
            .may_load(storage, (&pool_addr, &word_pos))?
            .unwrap_or(Uint128::zero());
        bitmap |= Uint128::from(1u128) << bit_pos;
        TICK_BITMAP.save(storage, (&pool_addr, &word_pos), &bitmap)?;

        Ok(())
    }

    /// 在位图中清除 tick 标记
    pub fn clear_tick_bitmap_bit(
        storage: &mut dyn Storage,
        pool_addr: &Addr,
        tick_index: i32,
    ) -> StdResult<()> {
        let word_pos = tick_index >> 7;
        let bit_pos = tick_index & 0x7F;
        let mut bitmap = TICK_BITMAP
            .may_load(storage, (&pool_addr, &word_pos))?
            .unwrap_or(Uint128::zero());
        let mask = !(Uint128::from(1u128) << bit_pos);
        bitmap &= mask;
        TICK_BITMAP.save(storage, (&pool_addr, &word_pos), &bitmap)?;
        Ok(())
    }

    /// 更新 tick 的流动性
    pub fn update_tick(
        storage: &mut dyn Storage,
        pool_addr: &Addr,
        tick_index: i32,
        liquidity_delta: Int128,
        fee_growth_outside_0: Uint128,
        fee_growth_outside_1: Uint128,
        upper: bool,
    ) -> StdResult<()> {
        let mut tick = TICKS
            .load(storage, (pool_addr, tick_index))?;

        let delta_abs = if liquidity_delta.to_i128() >= 0 {
            Uint128::new(liquidity_delta.to_i128() as u128)
        } else {
            Uint128::new((-liquidity_delta.to_i128()) as u128)
        };

        if upper {
            tick.liquidity_net = Int128::new(
                tick.liquidity_net.to_i128() - liquidity_delta.to_i128()
            );
        } else {
            tick.liquidity_net = Int128::new(
                tick.liquidity_net.to_i128() + liquidity_delta.to_i128()
            );
        }

        if liquidity_delta.to_i128() >= 0 {
            tick.liquidity_gross = tick.liquidity_gross.checked_add(delta_abs)?;
        } else {
            tick.liquidity_gross = tick.liquidity_gross.checked_sub(delta_abs)?;
        }

        tick.fee_growth_outside_0 = fee_growth_outside_0;
        tick.fee_growth_outside_1 = fee_growth_outside_1;

        TICKS.save(storage, (pool_addr, tick_index), &tick)?;
        Ok(())
    }

    /// 在位图中寻找下一个已初始化的 tick
    pub fn find_next_initialized_tick(
        storage: &dyn Storage,
        pool_addr: &Addr,
        tick_index: i32,
        tick_spacing: i32,
        zero_for_one: bool,
    ) -> StdResult<Option<i32>> {
        let compressed = tick_index / tick_spacing;
        let word_pos = compressed >> 7;
        let bit_pos = compressed & 0x7F;

        if zero_for_one {
            // 向左搜索(更低的 tick)
            let mut current_word = TICK_BITMAP
                .may_load(storage, (&pool_addr, &word_pos))?
                .unwrap_or(Uint128::zero());

            if current_word != Uint128::zero() {
                let mask = (Uint128::from(1u128) << bit_pos) - Uint128::from(1u128);
                current_word &= mask;
                if current_word != Uint128::zero() {
                    let msb = 127 - current_word.leading_zeros();
                    return Ok(Some((word_pos * 128 + msb as i32) * tick_spacing));
                }
            }
            let mut pos = word_pos - 1;
            while pos >= -887272 / 128 {
                if let Some(word) = TICK_BITMAP
                    .may_load(storage, (&pool_addr, &pos))?
                {
                    if word != Uint128::zero() {
                        let msb = 127 - word.leading_zeros();
                        return Ok(Some((pos * 128 + msb as i32) * tick_spacing));
                    }
                }
                pos -= 1;
            }
            Ok(None)
        } else {
            // 向右搜索(更高的 tick)
            let mut current_word = TICK_BITMAP
                .may_load(storage, (&pool_addr, &word_pos))?
                .unwrap_or(Uint128::zero());

            if current_word != Uint128::zero() {
                let mask = (Uint128::from(1u128) << (128 - bit_pos - 1)) - Uint128::from(1u128);
                current_word = current_word.rotate_right(bit_pos as u32);
                current_word &= !mask;
                current_word = current_word.rotate_left(bit_pos as u32);
                if current_word != Uint128::zero() {
                    let lzb = current_word.leading_zeros();
                    let next_bit = 127 - lzb;
                    return Ok(Some((word_pos * 128 + next_bit as i32) * tick_spacing));
                }
            }
            let mut pos = word_pos + 1;
            let max_word = MAX_TICK / 128;
            while pos <= max_word {
                if let Some(word) = TICK_BITMAP
                    .may_load(storage, (&pool_addr, &pos))?
                {
                    if word != Uint128::zero() {
                        let lzb = word.leading_zeros();
                        let next_bit = 127 - lzb;
                        return Ok(Some((pos * 128 + next_bit as i32) * tick_spacing));
                    }
                }
                pos += 1;
            }
            Ok(None)
        }
    }
}

3.3 Position 数据结构

// contracts/pool_v3/src/position.rs
use cosmwasm_std::{Addr, Uint128};
use cw_storage_plus::{Item, Map};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

/// Position(仓位):LP 在一对上下界 tick 之间提供的流动性
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Position {
    /// 该仓位的流动性数量
    pub liquidity: Uint128,
    /// 该仓位已累积的 token0 fee growth inside
    pub fee_growth_inside_0: Uint128,
    /// 该仓位已累积的 token1 fee growth inside
    pub fee_growth_inside_1: Uint128,
    /// 该仓位已累积但尚未提取的 token0
    pub tokens_owed_0: Uint128,
    /// 该仓位已累积但尚未提取的 token1
    pub tokens_owed_1: Uint128,
}

/// Position 标识符(复合 key)
/// key: (pool_addr, owner_addr, lower_tick, upper_tick) -> Position
pub const POSITIONS: Map<(&Addr, &Addr, i32, i32), Position> = Map::new("positions");

/// 每个 position 的唯一 token_id 映射
/// token_id -> (pool_addr, owner, lower_tick, upper_tick)
pub const POSITION_TOKEN_ID: Map<Uint128, (&Addr, &Addr, i32, i32)> =
    Map::new("position_token_id");

/// 下一个可用的 token_id 计数器
pub const NEXT_TOKEN_ID: Item<Uint128> = Item::new("next_token_id");

3.4 Position 管理器

// contracts/pool_v3/src/position.rs (continued)

/// Position 管理操作
pub struct PositionManager;

impl PositionManager {
    /// 创建一个新仓位
    pub fn create_position(
        storage: &mut dyn Storage,
        pool_addr: &Addr,
        owner: &Addr,
        lower_tick: i32,
        upper_tick: i32,
        liquidity: Uint128,
        fee_growth_inside_0: Uint128,
        fee_growth_inside_1: Uint128,
    ) -> StdResult<Uint128> {
        let pos = Position {
            liquidity,
            fee_growth_inside_0,
            fee_growth_inside_1,
            tokens_owed_0: Uint128::zero(),
            tokens_owed_1: Uint128::zero(),
        };
        POSITIONS.save(storage, (&pool_addr, &owner, lower_tick, upper_tick), &pos)?;

        // 分配 token_id
        let mut next_id = NEXT_TOKEN_ID
            .may_load(storage)?
            .unwrap_or(Uint128::new(1));
        let token_id = next_id;
        next_id += Uint128::new(1);
        NEXT_TOKEN_ID.save(storage, &next_id)?;

        // 记录 token_id -> position 映射
        POSITION_TOKEN_ID.save(storage, token_id, &(pool_addr.clone(), owner.clone(), lower_tick, upper_tick))?;

        Ok(token_id)
    }

    /// 读取仓位信息
    pub fn get_position(
        storage: &dyn Storage,
        pool_addr: &Addr,
        owner: &Addr,
        lower_tick: i32,
        upper_tick: i32,
    ) -> StdResult<Option<Position>> {
        POSITIONS.may_load(storage, (&pool_addr, &owner, lower_tick, upper_tick))
    }

    /// 更新仓位流动性
    pub fn update_liquidity(
        storage: &mut dyn Storage,
        pool_addr: &Addr,
        owner: &Addr,
        lower_tick: i32,
        upper_tick: i32,
        liquidity_delta: Int128,
    ) -> StdResult<Position> {
        let mut pos = POSITIONS.load(storage, (&pool_addr, &owner, lower_tick, upper_tick))?;
        let delta_abs = if liquidity_delta.to_i128() >= 0 {
            Uint128::new(liquidity_delta.to_i128() as u128)
        } else {
            Uint128::new((-liquidity_delta.to_i128()) as u128)
        };

        if liquidity_delta.to_i128() >= 0 {
            pos.liquidity = pos.liquidity.checked_add(delta_abs)?;
        } else {
            pos.liquidity = pos.liquidity.checked_sub(delta_abs)?;
        }

        POSITIONS.save(storage, (&pool_addr, &owner, lower_tick, upper_tick), &pos)?;
        Ok(pos)
    }

    /// 更新仓位的已赚手续费
    pub fn update_fees_owed(
        storage: &mut dyn Storage,
        pool_addr: &Addr,
        owner: &Addr,
        lower_tick: i32,
        upper_tick: i32,
        fee_growth_inside_0: Uint128,
        fee_growth_inside_1: Uint128,
    ) -> StdResult<()> {
        let mut pos = POSITIONS.load(storage, (&pool_addr, &owner, lower_tick, upper_tick))?;

        let fee_delta_0 = pos.liquidity * (fee_growth_inside_0 - pos.fee_growth_inside_0);
        let fee_delta_1 = pos.liquidity * (fee_growth_inside_1 - pos.fee_growth_inside_1);

        pos.tokens_owed_0 += fee_delta_0;
        pos.tokens_owed_1 += fee_delta_1;
        pos.fee_growth_inside_0 = fee_growth_inside_0;
        pos.fee_growth_inside_1 = fee_growth_inside_1;

        POSITIONS.save(storage, (&pool_addr, &owner, lower_tick, upper_tick), &pos)?;
        Ok(())
    }

    /// 提取手续费
    pub fn collect_fees(
        storage: &mut dyn Storage,
        pool_addr: &Addr,
        owner: &Addr,
        lower_tick: i32,
        upper_tick: i32,
    ) -> StdResult<(Uint128, Uint128)> {
        let mut pos = POSITIONS.load(storage, (&pool_addr, &owner, lower_tick, upper_tick))?;
        let owed_0 = pos.tokens_owed_0;
        let owed_1 = pos.tokens_owed_1;
        pos.tokens_owed_0 = Uint128::zero();
        pos.tokens_owed_1 = Uint128::zero();
        POSITIONS.save(storage, (&pool_addr, &owner, lower_tick, upper_tick), &pos)?;
        Ok((owed_0, owed_1))
    }
}

3.5 NFT Descriptor 合约

// contracts/nft_descriptor/src/contract.rs
use cosmwasm_std::{Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult};
use cw721_base::{ContractError, ExecuteMsg, InstantiateMsg, MinterResponse, QueryMsg};

/// Position NFT:每个仓位对应一个 NFT
/// 使用 CW721 标准表示仓位所有权
///
/// metadata:
/// {
///   "name": "MSG CLAMM Position #1",
///   "description": "MSG Chain Concentrated Liquidity Position",
///   "image": "data:image/svg+xml;base64,...",
///   "attributes": [
///     {"trait_type": "Pool", "value": "msg1..."},
///     {"trait_type": "Lower Tick", "value": "-60000"},
///     {"trait_type": "Upper Tick", "value": "60000"},
///     {"trait_type": "Liquidity", "value": "1000000"},
///     {"trait_type": "Token0", "value": "umsg"},
///     {"trait_type": "Token1", "value": "usdc"},
///   ]
/// }
pub type PositionNftContract = cw721_base::Cw721Contract<Empty, Empty>;

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    let base = PositionNftContract::default();
    base.instantiate(deps, env, info, msg)
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg<Empty>,
) -> Result<Response, ContractError> {
    let base = PositionNftContract::default();
    base.execute(deps, env, info, msg)
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
    let base = PositionNftContract::default();
    base.query(deps, env, msg)
}

3.6 Pool 状态

// contracts/pool_v3/src/state.rs
use cosmwasm_std::{Addr, Uint128};
use cw_storage_plus::Item;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

/// Pool 全局状态
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PoolState {
    /// Token0 的 denom 或合约地址
    pub token0: String,
    /// Token1 的 denom 或合约地址
    pub token1: String,
    /// 当前 sqrtPrice (Q64.96)
    pub sqrt_price: Uint256,
    /// 当前 tick
    pub tick: i32,
    /// 当前流动性(当前 tick 的活跃流动性)
    pub liquidity: Uint128,
    /// 当前 tick 的费率层级(tick spacing 的倍数)
    pub tick_spacing: i32,
    /// 费率(百万分之一):500=0.05%, 3000=0.30%, 10000=1.00%
    pub fee_rate: Uint128,
    /// 协议费率比例(百万分之一),0-1000000
    pub protocol_fee_rate: Uint128,
    /// Token0 的全局 fee growth
    pub fee_growth_global_0: Uint128,
    /// Token1 的全局 fee growth
    pub fee_growth_global_1: Uint128,
    /// 协议已收取的 token0 手续费
    pub protocol_fees_0: Uint128,
    /// 协议已收取的 token1 手续费
    pub protocol_fees_1: Uint128,
    /// 未使用的 token0 余额(去除流动性的部分)
    pub uncollected_fees_0: Uint128,
    /// 未使用的 token1 余额
    pub uncollected_fees_1: Uint128,
}

impl PoolState {
    /// 初始化 Pool
    pub fn initialize(
        sqrt_price_x96: Uint256,
        tick: i32,
    ) -> Self {
        PoolState {
            token0: String::new(),
            token1: String::new(),
            sqrt_price: sqrt_price_x96,
            tick,
            liquidity: Uint128::zero(),
            tick_spacing: 60,
            fee_rate: Uint128::new(3000),
            protocol_fee_rate: Uint128::zero(),
            fee_growth_global_0: Uint128::zero(),
            fee_growth_global_1: Uint128::zero(),
            protocol_fees_0: Uint128::zero(),
            protocol_fees_1: Uint128::zero(),
            uncollected_fees_0: Uint128::zero(),
            uncollected_fees_1: Uint128::zero(),
        }
    }

    /// 获取当前价格
    pub fn current_price(&self) -> Uint256 {
        self.sqrt_price
    }

    /// 设置新价格
    pub fn set_sqrt_price(&mut self, sqrt_price: Uint256, tick: i32) {
        self.sqrt_price = sqrt_price;
        self.tick = tick;
    }
}

pub const POOL_STATE: Item<PoolState> = Item::new("pool_state");
pub const POOL_ADDRESS: Item<Addr> = Item::new("pool_address");
pub const FACTORY_ADDRESS: Item<Addr> = Item::new("factory_address");
pub const NFT_CONTRACT: Item<Addr> = Item::new("nft_contract");

3.7 测试:Tick 与 Position

#[cfg(test)]
mod position_tests {
    use super::*;
    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};

    #[test]
    fn test_create_and_get_position() {
        let mut deps = mock_dependencies();
        let pool_addr = Addr::unchecked("pool1");
        let owner = Addr::unchecked("user1");
        let lower_tick = -60000i32;
        let upper_tick = 60000i32;
        let liquidity = Uint128::new(1_000_000);

        let token_id = PositionManager::create_position(
            deps.as_mut().storage,
            &pool_addr,
            &owner,
            lower_tick,
            upper_tick,
            liquidity,
            Uint128::zero(),
            Uint128::zero(),
        ).unwrap();

        assert_eq!(token_id, Uint128::new(1));

        let pos = PositionManager::get_position(
            deps.as_ref().storage,
            &pool_addr,
            &owner,
            lower_tick,
            upper_tick,
        ).unwrap().unwrap();

        assert_eq!(pos.liquidity, liquidity);
        assert_eq!(pos.tokens_owed_0, Uint128::zero());
        assert_eq!(pos.tokens_owed_1, Uint128::zero());
    }

    #[test]
    fn test_update_liquidity() {
        let mut deps = mock_dependencies();
        let pool_addr = Addr::unchecked("pool1");
        let owner = Addr::unchecked("user1");

        PositionManager::create_position(
            deps.as_mut().storage,
            &pool_addr,
            &owner,
            -10000, 10000,
            Uint128::new(500_000),
            Uint128::zero(),
            Uint128::zero(),
        ).unwrap();

        PositionManager::update_liquidity(
            deps.as_mut().storage,
            &pool_addr,
            &owner,
            -10000, 10000,
            Int128::new(300_000),
        ).unwrap();

        let pos = PositionManager::get_position(
            deps.as_ref().storage,
            &pool_addr,
            &owner,
            -10000, 10000,
        ).unwrap().unwrap();
        assert_eq!(pos.liquidity, Uint128::new(800_000));

        PositionManager::update_liquidity(
            deps.as_mut().storage,
            &pool_addr,
            &owner,
            -10000, 10000,
            Int128::new(-200_000),
        ).unwrap();

        let pos = PositionManager::get_position(
            deps.as_ref().storage,
            &pool_addr,
            &owner,
            -10000, 10000,
        ).unwrap().unwrap();
        assert_eq!(pos.liquidity, Uint128::new(600_000));
    }

    #[test]
    fn test_fee_collection() {
        let mut deps = mock_dependencies();
        let pool_addr = Addr::unchecked("pool1");
        let owner = Addr::unchecked("user1");

        PositionManager::create_position(
            deps.as_mut().storage,
            &pool_addr,
            &owner,
            -10000, 10000,
            Uint128::new(1_000_000),
            Uint128::zero(),
            Uint128::zero(),
        ).unwrap();

        PositionManager::update_fees_owed(
            deps.as_mut().storage,
            &pool_addr,
            &owner,
            -10000, 10000,
            Uint128::new(500),
            Uint128::new(300),
        ).unwrap();

        let (owed_0, owed_1) = PositionManager::collect_fees(
            deps.as_mut().storage,
            &pool_addr,
            &owner,
            -10000, 10000,
        ).unwrap();

        assert_eq!(owed_0, Uint128::new(500_000_000));
        assert_eq!(owed_1, Uint128::new(300_000_000));

        let (owed_0_2, owed_1_2) = PositionManager::collect_fees(
            deps.as_mut().storage,
            &pool_addr,
            &owner,
            -10000, 10000,
        ).unwrap();
        assert_eq!(owed_0_2, Uint128::zero());
        assert_eq!(owed_1_2, Uint128::zero());
    }

    #[test]
    fn test_tick_initialization() {
        let mut deps = mock_dependencies();
        let pool_addr = Addr::unchecked("pool1");

        TickManager::initialize_tick(
            deps.as_mut().storage,
            &pool_addr,
            0,
        ).unwrap();

        let tick = TICKS.load(deps.as_ref().storage, (&pool_addr, 0)).unwrap();
        assert_eq!(tick.liquidity_gross, Uint128::zero());
        assert_eq!(tick.liquidity_net.to_i128(), 0);
    }

    #[test]
    fn test_tick_bitmap() {
        let mut deps = mock_dependencies();
        let pool_addr = Addr::unchecked("pool1");

        TickManager::initialize_tick(
            deps.as_mut().storage,
            &pool_addr,
            128,
        ).unwrap();

        let word_pos = 128 >> 7;
        let bitmap = TICK_BITMAP.load(deps.as_ref().storage, (&pool_addr, &1)).unwrap();
        assert_eq!(bitmap, Uint128::from(1u128));

        TickManager::clear_tick_bitmap_bit(
            deps.as_mut().storage,
            &pool_addr,
            128,
        ).unwrap();

        let bitmap = TICK_BITMAP
            .may_load(deps.as_ref().storage, (&pool_addr, &1))
            .unwrap()
            .unwrap_or(Uint128::zero());
        assert_eq!(bitmap, Uint128::zero());
    }
}

4. 核心交换逻辑

4.1 单步交换计算

// contracts/pool_v3/src/swap.rs
use cosmwasm_std::{Addr, Storage, Uint128};
use crate::math::*;
use crate::state::PoolState;
use crate::tick::{TickManager, TICKS, Tick};
use crate::error::ContractError;
use crate::oracle::OracleManager;
use crate::position::PositionManager;
use crate::fee::FeeCalculator;

/// 单步交换计算
/// 在一个 tick 区间内(无跨 tick)执行交换
///
/// Arguments
/// * `sqrt_ratio_current` - 当前 sqrtPrice (Q64.96)
/// * `sqrt_ratio_target` - 目标 sqrtPrice (Q64.96),不能超过当前 tick 的边界
/// * `liquidity` - 当前区间的流动性
/// * `amount_remaining` - 剩余输入数量(已扣手续费)
/// * `fee_pips` - 费率(百万分之一)
///
/// Returns
/// * `sqrt_ratio_next` - 实际达到的 sqrtPrice
/// * `amount_in` - 实际输入数量
/// * `amount_out` - 实际输出数量
/// * `fee_amount` - 手续费
pub fn compute_swap_step(
    sqrt_ratio_current: Uint256,
    sqrt_ratio_target: Uint256,
    liquidity: Uint128,
    amount_remaining: Uint128,
    fee_pips: Uint128,
) -> (Uint256, Uint128, Uint128, Uint128) {
    let zero_for_one = sqrt_ratio_current > sqrt_ratio_target;

    if zero_for_one {
        // token0 -> token1(价格下降)
        // 计算在给定流动性下,用 amount_remaining 能移动到的下一个价格
        // 公式:amount_in = liquidity * (currentSqrtPrice - targetSqrtPrice) * 2^96
        //        / (currentSqrtPrice * targetSqrtPrice)
        let numerator = liquidity.full_mul(sqrt_ratio_current - sqrt_ratio_target);
        let denominator = sqrt_ratio_current.checked_mul(sqrt_ratio_target).unwrap();
        let amount_in = if denominator > Uint256::zero() {
            let result = <<Uint512 as std::ops::Div<Uint512>>::Output as Into<Uint256>>::into(
                Uint512::from(numerator) / Uint512::from(denominator)
            );
            result
        } else {
            Uint256::zero()
        };

        // amount_in 是 Q64.96 格式,需要右移 96 位得到实际数量
        let amount_in_128 = amount_in.from_q64_96();

        // 考虑手续费的可用输入
        let max_amount_in = amount_remaining * FEE_RATE_DENOMINATOR
            / (FEE_RATE_DENOMINATOR - fee_pips);

        if amount_in_128 >= max_amount_in {
            // 使用所有剩余输入
            let amount_in_max = max_amount_in;
            let amount_out = liquidity * (sqrt_ratio_current - sqrt_ratio_target).from_q64_96()
                / Uint128::from(1u128 << 96);
            let fee_amount = amount_in_max - amount_remaining;
            (sqrt_ratio_target, amount_in_max, amount_out, fee_amount)
        } else {
            // 使用部分输入,计算实际达到的 sqrtPrice
            // sqrt_next = 1 / (1/sqrt_current + amount_in / liquidity)
            let numerator = amount_remaining.full_mul(sqrt_ratio_current).full_mul(sqrt_ratio_target);
            let denominator = liquidity.full_mul(sqrt_ratio_target)
                + amount_remaining.full_mul(sqrt_ratio_current);
            let sqrt_ratio_next = numerator / denominator;
            let amount_out = liquidity * (sqrt_ratio_current - sqrt_ratio_next).from_q64_96()
                / Uint128::from(1u128 << 96);
            (sqrt_ratio_next, amount_remaining, amount_out, Uint128::zero())
        }
    } else {
        // token1 -> token0(价格上涨)
        let amount_in = liquidity * (sqrt_ratio_target - sqrt_ratio_current).from_q64_96()
            / Uint128::from(1u128 << 96);

        let max_amount_in = amount_remaining * FEE_RATE_DENOMINATOR
            / (FEE_RATE_DENOMINATOR - fee_pips);

        if amount_in >= max_amount_in {
            let amount_in_max = max_amount_in;
            // sqrt_next = sqrt_current + amount_in_max * sqrt_target / liquidity
            let numerator = amount_in_max.full_mul(sqrt_ratio_current);
            let denominator = liquidity.full_mul(sqrt_ratio_target) >> 96;
            let sqrt_ratio_next = sqrt_ratio_current + numerator / denominator;
            let fee_amount = amount_in_max - amount_remaining;
            (sqrt_ratio_next, amount_in_max, amount_out, fee_amount)
        } else {
            let sqrt_ratio_next = sqrt_ratio_current
                + (amount_remaining * sqrt_ratio_current) / (liquidity * sqrt_ratio_target >> 96);
            let amount_out = amount_in;
            (sqrt_ratio_next, amount_remaining, amount_out, Uint128::zero())
        }
    }
}

/// 计算 swap 过程中 tick 跨越的 fee growth 更新
pub fn compute_cross_tick_fee_growth(
    tick: &Tick,
    fee_growth_global_0: Uint128,
    fee_growth_global_1: Uint128,
    zero_for_one: bool,
) -> (Uint128, Uint128) {
    if zero_for_one {
        (
            fee_growth_global_0 - tick.fee_growth_outside_0,
            fee_growth_global_1 - tick.fee_growth_outside_1,
        )
    } else {
        (
            fee_growth_global_0 - tick.fee_growth_outside_0,
            fee_growth_global_1 - tick.fee_growth_outside_1,
        )
    }
}

4.2 完整交换流程

// contracts/pool_v3/src/swap.rs (continued)
use cosmwasm_std::{BankMsg, Coin, DepsMut, Env, MessageInfo, Response};

/// 交换参数
pub struct SwapParams {
    /// 是否精确输入(精确输入 = true,精确输出 = false)
    pub exact_in: bool,
    /// 输入/输出数量
    pub amount: Uint128,
    /// 是否用 token0 兑换 token1
    pub zero_for_one: bool,
    /// 最大价格限制(sqrtPrice Q64.96)
    pub sqrt_price_limit: Uint256,
    /// 滑点保护:最小输出(exact_in)或最大输入(exact_out)
    pub amount_limit: Uint128,
    /// 截止时间(秒级时间戳)
    pub deadline: u64,
}

/// 交换结果
pub struct SwapResult {
    pub amount_in: Uint128,
    pub amount_out: Uint128,
    pub sqrt_price_after: Uint256,
    pub tick_after: i32,
    pub fee_amount: Uint128,
}

/// 执行交换
pub fn swap(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    params: SwapParams,
) -> Result<(SwapResult, Response), ContractError> {
    // 1. 检查截止时间
    if env.block.time.seconds() > params.deadline {
        return Err(ContractError::DeadlineExceeded {});
    }

    let state = POOL_STATE.load(deps.storage)?;

    // 2. 检查价格限制
    if params.zero_for_one {
        if params.sqrt_price_limit < MIN_SQRT_RATIO
            || params.sqrt_price_limit > state.sqrt_price
        {
            return Err(ContractError::InvalidPriceLimit {});
        }
    } else {
        if params.sqrt_price_limit > MAX_SQRT_RATIO
            || params.sqrt_price_limit < state.sqrt_price
        {
            return Err(ContractError::InvalidPriceLimit {});
        }
    }

    // 3. 初始化交换
    let mut sqrt_price = state.sqrt_price;
    let mut tick = state.tick;
    let mut liquidity = state.liquidity;
    let mut amount_remaining = params.amount;
    let mut amount_in_total = Uint128::zero();
    let mut amount_out_total = Uint128::zero();
    let mut fee_amount_total = Uint128::zero();

    let fee_pips = state.fee_rate;
    let pool_addr = POOL_ADDRESS.load(deps.storage)?;

    // Oracle 管理器
    let oracle_manager = OracleManager;

    // 4. 主循环:跨越多个 Tick
    while amount_remaining > Uint128::zero() && sqrt_price != params.sqrt_price_limit {
        // 4a. 查找下一个已初始化的 tick
        let next_tick = if params.zero_for_one {
            TickManager::find_next_initialized_tick(
                deps.storage,
                &pool_addr,
                tick,
                state.tick_spacing,
                true,
            )?
        } else {
            TickManager::find_next_initialized_tick(
                deps.storage,
                &pool_addr,
                tick,
                state.tick_spacing,
                false,
            )?
        };

        // 4b. 计算目标 sqrtPrice
        let sqrt_ratio_target = if let Some(next_tick_idx) = next_tick {
            let next_sqrt_price = tick_to_sqrt_price(next_tick_idx);
            if params.zero_for_one {
                if next_sqrt_price < params.sqrt_price_limit {
                    params.sqrt_price_limit
                } else {
                    next_sqrt_price
                }
            } else {
                if next_sqrt_price > params.sqrt_price_limit {
                    params.sqrt_price_limit
                } else {
                    next_sqrt_price
                }
            }
        } else {
            params.sqrt_price_limit
        };

        // 4c. 执行单步交换
        let (sqrt_ratio_next, step_amount_in, step_amount_out, step_fee) = compute_swap_step(
            sqrt_price,
            sqrt_ratio_target,
            liquidity,
            amount_remaining,
            fee_pips,
        );

        // 4d. 累积总数
        if params.exact_in {
            amount_in_total += step_amount_in;
            amount_out_total += step_amount_out;
            fee_amount_total += step_fee;
            amount_remaining = amount_remaining.saturating_sub(step_amount_in);
        } else {
            amount_in_total += step_amount_in;
            amount_out_total += step_amount_out;
            fee_amount_total += step_fee;
            amount_remaining = amount_remaining.saturating_sub(step_amount_out);
        }

        // 4e. 更新价格
        sqrt_price = sqrt_ratio_next;

        // 4f. 检查是否需要跨 tick
        if sqrt_price == sqrt_ratio_target && next_tick.is_some() {
            let next_tick_idx = next_tick.unwrap();

            let mut tick_data = TICKS.load(deps.storage, (&pool_addr, next_tick_idx))?;

            // 更新 fee_growth_outside
            if params.zero_for_one {
                tick_data.fee_growth_outside_0 =
                    state.fee_growth_global_0 - tick_data.fee_growth_outside_0;
                tick_data.fee_growth_outside_1 =
                    state.fee_growth_global_1 - tick_data.fee_growth_outside_1;
            } else {
                tick_data.fee_growth_outside_0 =
                    state.fee_growth_global_0 - tick_data.fee_growth_outside_0;
                tick_data.fee_growth_outside_1 =
                    state.fee_growth_global_1 - tick_data.fee_growth_outside_1;
            }

            // 更新流动性
            if params.zero_for_one {
                let net = tick_data.liquidity_net.to_i128();
                liquidity = Uint128::new(
                    (liquidity.u128() as i128 - net) as u128
                );
            } else {
                let net = tick_data.liquidity_net.to_i128();
                liquidity = Uint128::new(
                    (liquidity.u128() as i128 + net) as u128
                );
            }

            TICKS.save(deps.storage, (&pool_addr, next_tick_idx), &tick_data)?;

            if params.zero_for_one {
                tick = next_tick_idx - 1;
            } else {
                tick = next_tick_idx;
            }
        }
    }

    // 5. 检查限价
    if params.exact_in && amount_out_total < params.amount_limit {
        return Err(ContractError::SlippageExceeded {});
    }
    if !params.exact_in && amount_in_total > params.amount_limit {
        return Err(ContractError::SlippageExceeded {});
    }

    // 6. 更新状态
    let mut state = POOL_STATE.load(deps.storage)?;
    state.sqrt_price = sqrt_price;
    state.tick = tick;
    state.liquidity = liquidity;

    // 更新全局手续费
    if liquidity > Uint128::zero() {
        if params.zero_for_one {
            state.fee_growth_global_0 +=
                fee_amount_total * Uint128::new(1_000_000) / liquidity;
            state.uncollected_fees_0 += fee_amount_total;
        } else {
            state.fee_growth_global_1 +=
                fee_amount_total * Uint128::new(1_000_000) / liquidity;
            state.uncollected_fees_1 += fee_amount_total;
        }
    }

    POOL_STATE.save(deps.storage, &state)?;

    // 7. Oracle 更新
    oracle_manager.observe(deps.storage, &env, &state)?;

    // 8. 构建响应
    let (_, transfer_out) = if params.zero_for_one {
        (state.token0.clone(), state.token1.clone())
    } else {
        (state.token1.clone(), state.token0.clone())
    };

    let recipient = info.sender.clone();
    let response = Response::new()
        .add_message(BankMsg::Send {
            to_address: recipient.to_string(),
            amount: vec![Coin {
                denom: transfer_out,
                amount: amount_out_total,
            }],
        });

    let result = SwapResult {
        amount_in: amount_in_total,
        amount_out: amount_out_total,
        sqrt_price_after: sqrt_price,
        tick_after: tick,
        fee_amount: fee_amount_total,
    };

    Ok((result, response))
}

4.3 Swap 入口函数

// contracts/pool_v3/src/contract.rs
use cosmwasm_std::{
    entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult,
};
use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, SwapResponse};
use crate::state::*;
use crate::swap::*;
use crate::error::ContractError;

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    let state = PoolState::initialize(
        msg.initial_sqrt_price,
        msg.initial_tick,
    );
    POOL_STATE.save(deps.storage, &state)?;
    POOL_ADDRESS.save(deps.storage, &env.contract.address)?;
    FACTORY_ADDRESS.save(deps.storage, &info.sender)?;

    Ok(Response::new()
        .add_attribute("method", "instantiate")
        .add_attribute("pool", env.contract.address.to_string()))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::Swap(params) => {
            let (result, response) = swap(deps, env, info, params)?;
            Ok(response
                .add_attribute("method", "swap")
                .add_attribute("amount_in", result.amount_in.to_string())
                .add_attribute("amount_out", result.amount_out.to_string())
                .add_attribute("sqrt_price", result.sqrt_price_after.to_string())
                .add_attribute("tick", result.tick_after.to_string()))
        }
        ExecuteMsg::Mint(params) => {
            mint(deps, env, info, params)
        }
        ExecuteMsg::Burn(params) => {
            burn(deps, env, info, params)
        }
        ExecuteMsg::Collect(params) => {
            collect(deps, env, info, params)
        }
    }
}

4.4 错误类型

// contracts/pool_v3/src/error.rs
use cosmwasm_std::StdError;
use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
    #[error("{0}")]
    Std(#[from] StdError),

    #[error("Deadline exceeded")]
    DeadlineExceeded {},

    #[error("Invalid price limit")]
    InvalidPriceLimit {},

    #[error("Slippage exceeded")]
    SlippageExceeded {},

    #[error("Insufficient liquidity")]
    InsufficientLiquidity {},

    #[error("Invalid tick")]
    InvalidTick {},

    #[error("Ticks not initialized")]
    TicksNotInitialized {},

    #[error("Not enough token0")]
    NotEnoughToken0 {},

    #[error("Not enough token1")]
    NotEnoughToken1 {},

    #[error("Price not in range")]
    PriceNotInRange {},

    #[error("Unauthorized")]
    Unauthorized {},

    #[error("Not implemented")]
    NotImplemented {},
}

4.5 交换测试

#[cfg(test)]
mod swap_tests {
    use super::*;
    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
    use cosmwasm_std::{coins, Addr};

    fn setup_swap_pool(deps: &mut cosmwasm_std::OwnedDeps<impl cosmwasm_std::MemoryStorage>) {
        let pool_addr = Addr::unchecked("pool1");
        let init_sqrt_price = tick_to_sqrt_price(0);
        let mut state = PoolState::initialize(init_sqrt_price, 0);
        state.token0 = "umsg".to_string();
        state.token1 = "uusdc".to_string();
        state.tick_spacing = 60;
        state.fee_rate = Uint128::new(3000);
        state.liquidity = Uint128::new(1_000_000_000_000_000_000u128);
        POOL_STATE.save(deps.as_mut().storage, &state).unwrap();
        POOL_ADDRESS.save(deps.as_mut().storage, &pool_addr).unwrap();
    }

    #[test]
    fn test_compute_swap_step_zero_for_one() {
        let sqrt_price_current = tick_to_sqrt_price(0);
        let sqrt_price_target = tick_to_sqrt_price(-100);
        let liquidity = Uint128::new(1_000_000_000_000_000_000u128);
        let amount_remaining = Uint128::new(100_000_000_000_000_000u128);
        let fee_pips = Uint128::new(3000);

        let (sqrt_next, amount_in, amount_out, fee) = compute_swap_step(
            sqrt_price_current,
            sqrt_price_target,
            liquidity,
            amount_remaining,
            fee_pips,
        );

        assert!(amount_in > Uint128::zero());
        assert!(amount_out > Uint128::zero());
        assert!(sqrt_next <= sqrt_price_current);
        assert!(sqrt_next >= sqrt_price_target);
    }

    #[test]
    fn test_swap_full_range() {
        let mut deps = mock_dependencies();
        setup_swap_pool(&mut deps);

        let params = SwapParams {
            exact_in: true,
            amount: Uint128::new(100_000_000_000_000_000u128),
            zero_for_one: true,
            sqrt_price_limit: MIN_SQRT_RATIO,
            amount_limit: Uint128::zero(),
            deadline: 9999999999,
        };

        let info = mock_info("trader", &coins(100_000_000_000_000_000u128, "umsg"));
        let (result, _response) = swap(deps.as_mut(), mock_env(), info, params).unwrap();

        assert!(result.amount_in > Uint128::zero());
        assert!(result.amount_out > Uint128::zero());
        assert!(result.sqrt_price_after < tick_to_sqrt_price(0));
    }

    #[test]
    fn test_swap_with_tick_crossing() {
        let mut deps = mock_dependencies();
        let pool_addr = Addr::unchecked("pool1");
        let init_sqrt_price = tick_to_sqrt_price(0);
        let mut state = PoolState::initialize(init_sqrt_price, 0);
        state.token0 = "umsg".to_string();
        state.token1 = "uusdc".to_string();
        state.tick_spacing = 60;
        state.fee_rate = Uint128::new(3000);
        state.liquidity = Uint128::new(1_000_000_000_000_000_000u128);
        POOL_STATE.save(deps.as_mut().storage, &state).unwrap();
        POOL_ADDRESS.save(deps.as_mut().storage, &pool_addr).unwrap();

        TickManager::initialize_tick(deps.as_mut().storage, &pool_addr, -60).unwrap();
        TickManager::initialize_tick(deps.as_mut().storage, &pool_addr, -120).unwrap();

        let mut tick_minus_60 = TICKS
            .load(deps.as_mut().storage, (&pool_addr, -60))
            .unwrap();
        tick_minus_60.liquidity_gross = Uint128::new(500_000_000_000_000_000u128);
        tick_minus_60.liquidity_net = Int128::new(500_000_000_000_000_000i128);
        TICKS.save(deps.as_mut().storage, (&pool_addr, -60), &tick_minus_60).unwrap();

        let params = SwapParams {
            exact_in: true,
            amount: Uint128::new(1_000_000_000_000_000_000u128),
            zero_for_one: true,
            sqrt_price_limit: MIN_SQRT_RATIO,
            amount_limit: Uint128::zero(),
            deadline: 9999999999,
        };

        let info = mock_info("trader", &coins(1_000_000_000_000_000_000u128, "umsg"));
        let (result, _response) = swap(deps.as_mut(), mock_env(), info, params).unwrap();
        assert!(result.sqrt_price_after < init_sqrt_price);
        assert!(result.tick_after < 0);
    }

    #[test]
    fn test_swap_slippage_protection() {
        let mut deps = mock_dependencies();
        setup_swap_pool(&mut deps);

        let params = SwapParams {
            exact_in: true,
            amount: Uint128::new(100_000_000_000_000_000u128),
            zero_for_one: true,
            sqrt_price_limit: MIN_SQRT_RATIO,
            amount_limit: Uint128::new(1_000_000_000_000_000_000_000u128),
            deadline: 9999999999,
        };

        let info = mock_info("trader", &coins(100_000_000_000_000_000u128, "umsg"));
        let result = swap(deps.as_mut(), mock_env(), info, params);
        assert!(result.is_err());
        assert_eq!(result.unwrap_err(), ContractError::SlippageExceeded {});
    }

    #[test]
    fn test_swap_deadline() {
        let mut deps = mock_dependencies();
        let pool_addr = Addr::unchecked("pool1");
        let init_sqrt_price = tick_to_sqrt_price(0);
        let state = PoolState::initialize(init_sqrt_price, 0);
        POOL_STATE.save(deps.as_mut().storage, &state).unwrap();
        POOL_ADDRESS.save(deps.as_mut().storage, &pool_addr).unwrap();

        let params = SwapParams {
            exact_in: true,
            amount: Uint128::new(100_000_000u128),
            zero_for_one: true,
            sqrt_price_limit: MIN_SQRT_RATIO,
            amount_limit: Uint128::zero(),
            deadline: 0,
        };

        let info = mock_info("trader", &[]);
        let result = swap(deps.as_mut(), mock_env(), info, params);
        assert!(result.is_err());
        assert_eq!(result.unwrap_err(), ContractError::DeadlineExceeded {});
    }
}

5. 流动性管理

5.1 Mint(添加流动性)

Mint 操作创建一个新的 Position 或向已有 Position 添加流动性。

// contracts/pool_v3/src/liquidity.rs
use cosmwasm_std::{BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Uint128, Addr};
use crate::error::ContractError;
use crate::math::*;
use crate::position::*;
use crate::state::*;
use crate::tick::*;

/// Mint 参数
pub struct MintParams {
    pub lower_tick: i32,
    pub upper_tick: i32,
    pub amount0_desired: Uint128,
    pub amount1_desired: Uint128,
    pub amount0_min: Uint128,
    pub amount1_min: Uint128,
    pub recipient: Addr,
    pub deadline: u64,
}

/// 添加流动性(Mint)
pub fn mint(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    params: MintParams,
) -> Result<Response, ContractError> {
    // 1. 检查截止时间
    if env.block.time.seconds() > params.deadline {
        return Err(ContractError::DeadlineExceeded {});
    }

    let pool_addr = POOL_ADDRESS.load(deps.storage)?;
    let mut state = POOL_STATE.load(deps.storage)?;

    // 2. 检查上下界 tick
    if params.lower_tick >= params.upper_tick {
        return Err(ContractError::InvalidTick {});
    }
    if params.lower_tick < MIN_TICK || params.upper_tick > MAX_TICK {
        return Err(ContractError::InvalidTick {});
    }
    if params.lower_tick % state.tick_spacing != 0
        || params.upper_tick % state.tick_spacing != 0
    {
        return Err(ContractError::InvalidTick {});
    }

    let sqrt_price = state.sqrt_price;
    let tick = state.tick;

    // 3. 计算价格上下界的 sqrtPrice
    let sqrt_price_lower = tick_to_sqrt_price(params.lower_tick);
    let sqrt_price_upper = tick_to_sqrt_price(params.upper_tick);

    // 4. 计算流动性 L
    let amount0 = params.amount0_desired;
    let amount1 = params.amount1_desired;

    let (liquidity, amount0_actual, amount1_actual) = if sqrt_price <= sqrt_price_lower {
        // 价格在区间下方:全部为 token0
        // L = amount0 * sqrtPriceUpper * sqrtPriceLower / (sqrtPriceUpper - sqrtPriceLower)
        let numerator = amount0.full_mul(sqrt_price_upper).full_mul(sqrt_price_lower);
        let denominator = sqrt_price_upper - sqrt_price_lower;
        let l_in_256 = numerator >> 192;
        let l = l_in_256.to_uint128();
        (l, amount0, Uint128::zero())
    } else if sqrt_price >= sqrt_price_upper {
        // 价格在区间上方:全部为 token1
        let l = amount1 * Uint128::from(1u128 << 96)
            / (sqrt_price_upper - sqrt_price_lower).from_q64_96();
        (l, Uint128::zero(), amount1)
    } else {
        // 价格在区间内:需要两种 token
        let l0_numerator = amount0.full_mul(sqrt_price).full_mul(sqrt_price_upper);
        let l0_denominator = sqrt_price_upper - sqrt_price;
        let l0 = (l0_numerator >> 192).to_uint128();

        let l1 = amount1 * Uint128::from(1u128 << 96)
            / (sqrt_price - sqrt_price_lower).from_q64_96();

        let l = l0.min(l1);

        // 实际需要的数量
        let a0 = l * (sqrt_price_upper - sqrt_price).from_q64_96()
            / (sqrt_price * sqrt_price_upper).from_q64_96();
        let a1 = l * (sqrt_price - sqrt_price_lower).from_q64_96()
            / Uint128::from(1u128 << 96);

        (l, a0, a1)
    };

    // 5. 检查最小数量
    if amount0_actual < params.amount0_min || amount1_actual < params.amount1_min {
        return Err(ContractError::SlippageExceeded {});
    }

    // 6. 初始化 tick(如果尚未初始化)
    let lower_init = TICKS.may_load(deps.storage, (&pool_addr, params.lower_tick))?;
    if lower_init.is_none() {
        TickManager::initialize_tick(deps.storage, &pool_addr, params.lower_tick)?;
    }
    let upper_init = TICKS.may_load(deps.storage, (&pool_addr, params.upper_tick))?;
    if upper_init.is_none() {
        TickManager::initialize_tick(deps.storage, &pool_addr, params.upper_tick)?;
    }

    // 7. 更新 tick 流动性
    let liquidity_delta = Int128::new(liquidity.u128() as i128);

    TickManager::update_tick(
        deps.storage,
        &pool_addr,
        params.lower_tick,
        liquidity_delta,
        state.fee_growth_global_0,
        state.fee_growth_global_1,
        false,
    )?;

    TickManager::update_tick(
        deps.storage,
        &pool_addr,
        params.upper_tick,
        liquidity_delta,
        state.fee_growth_global_0,
        state.fee_growth_global_1,
        true,
    )?;

    // 8. 如果价格在当前区间内,更新全局流动性
    if sqrt_price >= sqrt_price_lower && sqrt_price <= sqrt_price_upper {
        state.liquidity = state.liquidity.checked_add(liquidity)?;
    }

    // 9. 计算 fee growth inside
    let (fee_growth_inside_0, fee_growth_inside_1) = if tick >= params.upper_tick {
        let upper_tick_data = TICKS.load(deps.storage, (&pool_addr, params.upper_tick))?;
        (
            state.fee_growth_global_0 - upper_tick_data.fee_growth_outside_0,
            state.fee_growth_global_1 - upper_tick_data.fee_growth_outside_1,
        )
    } else if tick < params.lower_tick {
        let lower_tick_data = TICKS.load(deps.storage, (&pool_addr, params.lower_tick))?;
        (
            lower_tick_data.fee_growth_outside_0,
            lower_tick_data.fee_growth_outside_1,
        )
    } else {
        let lower_tick_data = TICKS.load(deps.storage, (&pool_addr, params.lower_tick))?;
        let upper_tick_data = TICKS.load(deps.storage, (&pool_addr, params.upper_tick))?;
        (
            state.fee_growth_global_0
                - lower_tick_data.fee_growth_outside_0
                - upper_tick_data.fee_growth_outside_0,
            state.fee_growth_global_1
                - lower_tick_data.fee_growth_outside_1
                - upper_tick_data.fee_growth_outside_1,
        )
    };

    // 10. 创建或更新 Position
    let existing_pos = PositionManager::get_position(
        deps.storage,
        &pool_addr,
        &params.recipient,
        params.lower_tick,
        params.upper_tick,
    )?;

    let token_id = if let Some(mut pos) = existing_pos {
        let fee_delta_0 = pos.liquidity * (fee_growth_inside_0 - pos.fee_growth_inside_0);
        let fee_delta_1 = pos.liquidity * (fee_growth_inside_1 - pos.fee_growth_inside_1);
        pos.tokens_owed_0 += fee_delta_0;
        pos.tokens_owed_1 += fee_delta_1;
        pos.liquidity += liquidity;
        pos.fee_growth_inside_0 = fee_growth_inside_0;
        pos.fee_growth_inside_1 = fee_growth_inside_1;
        POSITIONS.save(
            deps.storage,
            (&pool_addr, &params.recipient, params.lower_tick, params.upper_tick),
            &pos,
        )?;
        Uint128::zero()
    } else {
        PositionManager::create_position(
            deps.storage,
            &pool_addr,
            &params.recipient,
            params.lower_tick,
            params.upper_tick,
            liquidity,
            fee_growth_inside_0,
            fee_growth_inside_1,
        )?
    };

    POOL_STATE.save(deps.storage, &state)?;

    // 11. 构建响应
    let mut response = Response::new()
        .add_attribute("method", "mint")
        .add_attribute("token_id", token_id.to_string())
        .add_attribute("liquidity", liquidity.to_string())
        .add_attribute("amount0", amount0_actual.to_string())
        .add_attribute("amount1", amount1_actual.to_string());

    if amount0_actual > Uint128::zero() {
        response = response.add_message(BankMsg::Send {
            to_address: pool_addr.to_string(),
            amount: vec![Coin {
                denom: state.token0.clone(),
                amount: amount0_actual,
            }],
        });
    }
    if amount1_actual > Uint128::zero() {
        response = response.add_message(BankMsg::Send {
            to_address: pool_addr.to_string(),
            amount: vec![Coin {
                denom: state.token1.clone(),
                amount: amount1_actual,
            }],
        });
    }

    Ok(response)
}

5.2 Burn(移除流动性)

// contracts/pool_v3/src/liquidity.rs (continued)

/// Burn 参数
pub struct BurnParams {
    pub lower_tick: i32,
    pub upper_tick: i32,
    pub liquidity: Uint128,
    pub amount0_min: Uint128,
    pub amount1_min: Uint128,
    pub deadline: u64,
}

/// 移除流动性(Burn)
pub fn burn(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    params: BurnParams,
) -> Result<Response, ContractError> {
    if env.block.time.seconds() > params.deadline {
        return Err(ContractError::DeadlineExceeded {});
    }

    let pool_addr = POOL_ADDRESS.load(deps.storage)?;
    let sender = info.sender;

    let mut pos = POSITIONS.load(
        deps.storage,
        (&pool_addr, &sender, params.lower_tick, params.upper_tick),
    )?;

    if params.liquidity > pos.liquidity {
        return Err(ContractError::InsufficientLiquidity {});
    }

    let mut state = POOL_STATE.load(deps.storage)?;
    let sqrt_price = state.sqrt_price;
    let sqrt_price_lower = tick_to_sqrt_price(params.lower_tick);
    let sqrt_price_upper = tick_to_sqrt_price(params.upper_tick);

    // 计算应退还的 token 数量
    let (amount0, amount1) = if sqrt_price <= sqrt_price_lower {
        let numerator = params.liquidity.full_mul(sqrt_price_upper - sqrt_price_lower);
        let denominator = sqrt_price_upper.full_mul(sqrt_price_lower);
        let a0 = (numerator / denominator).to_uint128();
        (a0, Uint128::zero())
    } else if sqrt_price >= sqrt_price_upper {
        let a1 = params.liquidity * (sqrt_price_upper - sqrt_price_lower).from_q64_96()
            / Uint128::from(1u128 << 96);
        (Uint128::zero(), a1)
    } else {
        let a0 = params.liquidity * (sqrt_price_upper - sqrt_price).from_q64_96()
            / (sqrt_price * sqrt_price_upper).from_q64_96();
        let a1 = params.liquidity * (sqrt_price - sqrt_price_lower).from_q64_96()
            / Uint128::from(1u128 << 96);
        (a0, a1)
    };

    if amount0 < params.amount0_min || amount1 < params.amount1_min {
        return Err(ContractError::SlippageExceeded {});
    }

    // 更新 tick 流动性
    let liquidity_delta = Int128::new(-(params.liquidity.u128() as i128));

    TickManager::update_tick(
        deps.storage,
        &pool_addr,
        params.lower_tick,
        liquidity_delta,
        state.fee_growth_global_0,
        state.fee_growth_global_1,
        false,
    )?;
    TickManager::update_tick(
        deps.storage,
        &pool_addr,
        params.upper_tick,
        liquidity_delta,
        state.fee_growth_global_0,
        state.fee_growth_global_1,
        true,
    )?;

    if sqrt_price >= sqrt_price_lower && sqrt_price <= sqrt_price_upper {
        state.liquidity = state.liquidity.checked_sub(params.liquidity)?;
    }

    // 更新 position
    pos.liquidity = pos.liquidity.checked_sub(params.liquidity)?;
    POSITIONS.save(
        deps.storage,
        (&pool_addr, &sender, params.lower_tick, params.upper_tick),
        &pos,
    )?;

    POOL_STATE.save(deps.storage, &state)?;

    let mut response = Response::new()
        .add_attribute("method", "burn")
        .add_attribute("liquidity", params.liquidity.to_string())
        .add_attribute("amount0", amount0.to_string())
        .add_attribute("amount1", amount1.to_string());

    if amount0 > Uint128::zero() {
        response = response.add_message(BankMsg::Send {
            to_address: sender.to_string(),
            amount: vec![Coin {
                denom: state.token0.clone(),
                amount: amount0,
            }],
        });
    }
    if amount1 > Uint128::zero() {
        response = response.add_message(BankMsg::Send {
            to_address: sender.to_string(),
            amount: vec![Coin {
                denom: state.token1.clone(),
                amount: amount1,
            }],
        });
    }

    Ok(response)
}

5.3 Collect(提取手续费)

// contracts/pool_v3/src/liquidity.rs (continued)

/// Collect 参数
pub struct CollectParams {
    pub lower_tick: i32,
    pub upper_tick: i32,
    pub recipient: Option<Addr>,
}

/// 提取累积的手续费
pub fn collect(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    params: CollectParams,
) -> Result<Response, ContractError> {
    let pool_addr = POOL_ADDRESS.load(deps.storage)?;
    let sender = info.sender;
    let state = POOL_STATE.load(deps.storage)?;
    let tick = state.tick;

    let lower_tick_data = TICKS.load(deps.storage, (&pool_addr, params.lower_tick))?;
    let upper_tick_data = TICKS.load(deps.storage, (&pool_addr, params.upper_tick))?;

    let (fee_growth_inside_0, fee_growth_inside_1) = if tick >= params.upper_tick {
        (
            state.fee_growth_global_0 - upper_tick_data.fee_growth_outside_0,
            state.fee_growth_global_1 - upper_tick_data.fee_growth_outside_1,
        )
    } else if tick < params.lower_tick {
        (
            lower_tick_data.fee_growth_outside_0,
            lower_tick_data.fee_growth_outside_1,
        )
    } else {
        (
            state.fee_growth_global_0
                - lower_tick_data.fee_growth_outside_0
                - upper_tick_data.fee_growth_outside_0,
            state.fee_growth_global_1
                - lower_tick_data.fee_growth_outside_1
                - upper_tick_data.fee_growth_outside_1,
        )
    };

    PositionManager::update_fees_owed(
        deps.storage,
        &pool_addr,
        &sender,
        params.lower_tick,
        params.upper_tick,
        fee_growth_inside_0,
        fee_growth_inside_1,
    )?;

    let (owed_0, owed_1) = PositionManager::collect_fees(
        deps.storage,
        &pool_addr,
        &sender,
        params.lower_tick,
        params.upper_tick,
    )?;

    let recipient = params.recipient.unwrap_or(sender);

    let mut response = Response::new()
        .add_attribute("method", "collect")
        .add_attribute("amount0", owed_0.to_string())
        .add_attribute("amount1", owed_1.to_string());

    if owed_0 > Uint128::zero() {
        let actual_0 = owed_0.min(state.uncollected_fees_0);
        response = response.add_message(BankMsg::Send {
            to_address: recipient.to_string(),
            amount: vec![Coin {
                denom: state.token0.clone(),
                amount: actual_0,
            }],
        });
    }
    if owed_1 > Uint128::zero() {
        let actual_1 = owed_1.min(state.uncollected_fees_1);
        response = response.add_message(BankMsg::Send {
            to_address: recipient.to_string(),
            amount: vec![Coin {
                denom: state.token1.clone(),
                amount: actual_1,
            }],
        });
    }

    Ok(response)
}

5.4 流动性管理测试

#[cfg(test)]
mod liquidity_tests {
    use super::*;
    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
    use cosmwasm_std::{coins, Addr, Uint128};

    fn setup_pool(deps: &mut cosmwasm_std::OwnedDeps<impl cosmwasm_std::MemoryStorage>) {
        let pool_addr = Addr::unchecked("pool1");
        let init_sqrt_price = tick_to_sqrt_price(0);
        let mut state = PoolState::initialize(init_sqrt_price, 0);
        state.token0 = "umsg".to_string();
        state.token1 = "uusdc".to_string();
        state.tick_spacing = 60;
        state.fee_rate = Uint128::new(3000);
        POOL_STATE.save(deps.as_mut().storage, &state).unwrap();
        POOL_ADDRESS.save(deps.as_mut().storage, &pool_addr).unwrap();
        NEXT_TOKEN_ID.save(deps.as_mut().storage, &Uint128::new(1)).unwrap();
    }

    #[test]
    fn test_mint_new_position() {
        let mut deps = mock_dependencies();
        setup_pool(&mut deps);
        let pool_addr = Addr::unchecked("pool1");
        let user = Addr::unchecked("user1");
        let lower_tick = -600;
        let upper_tick = 600;

        TickManager::initialize_tick(deps.as_mut().storage, &pool_addr, lower_tick).unwrap();
        TickManager::initialize_tick(deps.as_mut().storage, &pool_addr, upper_tick).unwrap();

        let params = MintParams {
            lower_tick,
            upper_tick,
            amount0_desired: Uint128::new(100_000_000_000_000_000u128),
            amount1_desired: Uint128::new(100_000_000_000_000_000u128),
            amount0_min: Uint128::zero(),
            amount1_min: Uint128::zero(),
            recipient: user.clone(),
            deadline: 9999999999,
        };

        let info = mock_info("user1", &coins(200_000_000_000_000_000u128, "umsg"));
        let result = mint(deps.as_mut(), mock_env(), info, params);
        assert!(result.is_ok());
    }

    #[test]
    fn test_mint_and_burn() {
        let mut deps = mock_dependencies();
        setup_pool(&mut deps);
        let pool_addr = Addr::unchecked("pool1");
        let user = Addr::unchecked("user1");
        let lower_tick = -600;
        let upper_tick = 600;

        TickManager::initialize_tick(deps.as_mut().storage, &pool_addr, lower_tick).unwrap();
        TickManager::initialize_tick(deps.as_mut().storage, &pool_addr, upper_tick).unwrap();

        let mint_params = MintParams {
            lower_tick,
            upper_tick,
            amount0_desired: Uint128::new(100_000_000_000_000_000u128),
            amount1_desired: Uint128::new(100_000_000_000_000_000u128),
            amount0_min: Uint128::zero(),
            amount1_min: Uint128::zero(),
            recipient: user.clone(),
            deadline: 9999999999,
        };
        let info = mock_info("user1", &coins(200_000_000_000_000_000u128, "umsg"));
        mint(deps.as_mut(), mock_env(), info, mint_params).unwrap();

        let pos = POSITIONS
            .load(deps.as_ref().storage, (&pool_addr, &user, lower_tick, upper_tick))
            .unwrap();
        assert!(pos.liquidity > Uint128::zero());

        let burn_params = BurnParams {
            lower_tick,
            upper_tick,
            liquidity: pos.liquidity,
            amount0_min: Uint128::zero(),
            amount1_min: Uint128::zero(),
            deadline: 9999999999,
        };
        let info = mock_info("user1", &[]);
        let result = burn(deps.as_mut(), mock_env(), info, burn_params);
        assert!(result.is_ok());

        let pos = POSITIONS
            .load(deps.as_ref().storage, (&pool_addr, &user, lower_tick, upper_tick))
            .unwrap();
        assert_eq!(pos.liquidity, Uint128::zero());
    }

    #[test]
    fn test_invalid_ticks() {
        let mut deps = mock_dependencies();
        setup_pool(&mut deps);
        let user = Addr::unchecked("user1");

        let params = MintParams {
            lower_tick: 600,
            upper_tick: -600,
            amount0_desired: Uint128::new(100_000_000u128),
            amount1_desired: Uint128::new(100_000_000u128),
            amount0_min: Uint128::zero(),
            amount1_min: Uint128::zero(),
            recipient: user.clone(),
            deadline: 9999999999,
        };
        let info = mock_info("user1", &[]);
        let result = mint(deps.as_mut(), mock_env(), info, params);
        assert!(result.is_err());
    }

    #[test]
    fn test_collect_fees() {
        let mut deps = mock_dependencies();
        setup_pool(&mut deps);
        let pool_addr = Addr::unchecked("pool1");
        let user = Addr::unchecked("user1");
        let lower_tick = -600;
        let upper_tick = 600;

        TickManager::initialize_tick(deps.as_mut().storage, &pool_addr, lower_tick).unwrap();
        TickManager::initialize_tick(deps.as_mut().storage, &pool_addr, upper_tick).unwrap();

        let mint_params = MintParams {
            lower_tick,
            upper_tick,
            amount0_desired: Uint128::new(1_000_000_000_000_000_000u128),
            amount1_desired: Uint128::new(1_000_000_000_000_000_000u128),
            amount0_min: Uint128::zero(),
            amount1_min: Uint128::zero(),
            recipient: user.clone(),
            deadline: 9999999999,
        };
        let info = mock_info("user1", &coins(2_000_000_000_000_000_000u128, "umsg"));
        mint(deps.as_mut(), mock_env(), info, mint_params).unwrap();

        let mut state = POOL_STATE.load(deps.as_mut().storage).unwrap();
        state.fee_growth_global_0 = Uint128::new(1_000_000_000);
        state.fee_growth_global_1 = Uint128::new(500_000_000);
        POOL_STATE.save(deps.as_mut().storage, &state).unwrap();

        let collect_params = CollectParams {
            lower_tick,
            upper_tick,
            recipient: None,
        };
        let info = mock_info("user1", &[]);
        let result = collect(deps.as_mut(), mock_env(), info, collect_params);
        assert!(result.is_ok());
    }
}

6. 多费率层级

6.1 费率设计

Uniswap V3 支持三个费率层级,对应不同的交易对类型:

费率层级 Fee Rate Tick Spacing 适用场景
0.05% 500 10 稳定币对(USDC/USDT)
0.30% 3000 60 主流币对(ETH/USDC)
1.00% 10000 200 波动性币对/长尾资产

每个池子在创建时指定费率层级,之后不可更改。

// contracts/pool_v3/src/fee.rs
use cosmwasm_std::{Addr, Storage, Uint128};
use cw_storage_plus::Map;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

/// 支持的费率层级
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum FeeTier {
    /// 0.05% - 稳定币对
    Stable,
    /// 0.30% - 主流币对
    Default,
    /// 1.00% - 波动性币对
    Volatile,
}

impl FeeTier {
    pub fn fee_rate(&self) -> Uint128 {
        match self {
            FeeTier::Stable => Uint128::new(500),
            FeeTier::Default => Uint128::new(3000),
            FeeTier::Volatile => Uint128::new(10000),
        }
    }

    pub fn tick_spacing(&self) -> i32 {
        match self {
            FeeTier::Stable => 10,
            FeeTier::Default => 60,
            FeeTier::Volatile => 200,
        }
    }

    pub fn from_fee_rate(fee_rate: Uint128) -> Option<FeeTier> {
        match fee_rate.u128() {
            500 => Some(FeeTier::Stable),
            3000 => Some(FeeTier::Default),
            10000 => Some(FeeTier::Volatile),
            _ => None,
        }
    }
}

/// Fee 计算器
pub struct FeeCalculator;

impl FeeCalculator {
    /// 从交换输入中扣除手续费
    pub fn calculate_fee(amount: Uint128, fee_rate: Uint128) -> (Uint128, Uint128) {
        let fee = amount * fee_rate / FEE_RATE_DENOMINATOR;
        let amount_after = amount - fee;
        (amount_after, fee)
    }

    /// 计算协议应得的手续费
    pub fn calculate_protocol_fee(
        fee_amount: Uint128,
        protocol_fee_rate: Uint128,
    ) -> (Uint128, Uint128) {
        let protocol_fee = fee_amount * protocol_fee_rate / FEE_RATE_DENOMINATOR;
        let lp_fee = fee_amount - protocol_fee;
        (lp_fee, protocol_fee)
    }

    /// 根据价格变动计算 fee growth
    pub fn calculate_fee_growth(fee_amount: Uint128, liquidity: Uint128) -> Uint128 {
        if liquidity == Uint128::zero() {
            return Uint128::zero();
        }
        fee_amount * Uint128::new(1_000_000_000_000u128) / liquidity
    }
}

6.2 Factory 合约(支持多费率)

// contracts/factory_v3/src/state.rs
use cosmwasm_std::Addr;
use cw_storage_plus::Item;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use clamm_types::FeeTier;

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PoolConfig {
    pub token0: String,
    pub token1: String,
    pub fee_tier: FeeTier,
    pub pool_addr: Addr,
}

pub const OWNER: Item<Addr> = Item::new("owner");
pub const POOL_COUNT: Item<Uint128> = Item::new("pool_count");
pub const FEE_TIER_INFO: Item<Vec<FeeTier>> = Item::new("fee_tier_info");
pub const POOL_CONFIGS: Map<(String, String, Uint128), PoolConfig> =
    Map::new("pool_configs");

// contracts/factory_v3/src/contract.rs
use cosmwasm_std::{
    entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo,
    Reply, Response, StdResult, SubMsg, WasmMsg,
};
use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
use crate::state::{FEE_TIER_INFO, OWNER, POOL_COUNT, POOL_CONFIGS, PoolConfig};

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    OWNER.save(deps.storage, &info.sender)?;
    POOL_COUNT.save(deps.storage, &Uint128::zero())?;

    let fee_tiers = vec![
        FeeTier::Stable,
        FeeTier::Default,
        FeeTier::Volatile,
    ];
    FEE_TIER_INFO.save(deps.storage, &fee_tiers)?;

    Ok(Response::new()
        .add_attribute("method", "instantiate")
        .add_attribute("owner", info.sender))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::CreatePool {
            token0,
            token1,
            fee_tier,
            sqrt_price_x96,
        } => create_pool(deps, env, info, token0, token1, fee_tier, sqrt_price_x96),
    }
}

pub fn create_pool(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    token0: String,
    token1: String,
    fee_tier: FeeTier,
    sqrt_price_x96: Uint256,
) -> Result<Response, ContractError> {
    let owner = OWNER.load(deps.storage)?;
    let (token0, token1) = if token0 < token1 {
        (token0, token1)
    } else {
        (token1, token0)
    };

    let fee_rate = fee_tier.fee_rate();
    let key = (token0.clone(), token1.clone(), fee_rate);
    if POOL_CONFIGS.may_load(deps.storage, key.clone())?.is_some() {
        return Err(ContractError::PoolAlreadyExists {});
    }

    let tick_spacing = fee_tier.tick_spacing();
    let initial_tick = sqrt_price_to_tick(sqrt_price_x96);
    let initial_tick = tick_floor(initial_tick, tick_spacing);
    let sqrt_price = tick_to_sqrt_price(initial_tick);

    let pool_code_id = msg.pool_code_id;
    let init_msg = InstantiateMsg {
        token0: token0.clone(),
        token1: token1.clone(),
        tick_spacing,
        fee_rate,
        initial_sqrt_price: sqrt_price,
        initial_tick,
    };

    let label = format!("CLAMM-{}-{}-{}", token0, token1, fee_rate);
    let sub_msg = SubMsg::reply_on_success(
        WasmMsg::Instantiate {
            admin: Some(owner.to_string()),
            code_id: pool_code_id,
            msg: to_binary(&init_msg)?,
            funds: vec![],
            label,
        },
        1,
    );

    let mut count = POOL_COUNT.load(deps.storage)?;
    count += Uint128::new(1);
    POOL_COUNT.save(deps.storage, &count)?;

    Ok(Response::new()
        .add_submessage(sub_msg)
        .add_attribute("method", "create_pool")
        .add_attribute("token0", token0)
        .add_attribute("token1", token1)
        .add_attribute("fee_rate", fee_rate.to_string())
        .add_attribute("tick_spacing", tick_spacing.to_string()))
}

6.3 协议手续费

// contracts/pool_v3/src/fee.rs (continued)

pub struct ProtocolFee;

impl ProtocolFee {
    pub fn set_protocol_fee_rate(
        storage: &mut dyn Storage,
        new_rate: Uint128,
    ) -> StdResult<()> {
        if new_rate > Uint128::new(250_000) {
            return Err(StdError::generic_err("Protocol fee too high"));
        }
        let mut state = POOL_STATE.load(storage)?;
        state.protocol_fee_rate = new_rate;
        POOL_STATE.save(storage, &state)
    }

    pub fn collect_protocol_fees(
        storage: &mut dyn Storage,
        recipient: &Addr,
    ) -> StdResult<Response> {
        let state = POOL_STATE.load(storage)?;
        let fees_0 = state.protocol_fees_0;
        let fees_1 = state.protocol_fees_1;

        let mut state = state;
        state.protocol_fees_0 = Uint128::zero();
        state.protocol_fees_1 = Uint128::zero();
        POOL_STATE.save(storage, &state)?;

        let mut response = Response::new()
            .add_attribute("method", "collect_protocol_fees");
        if fees_0 > Uint128::zero() {
            response = response.add_message(BankMsg::Send {
                to_address: recipient.to_string(),
                amount: vec![Coin { denom: state.token0.clone(), amount: fees_0 }],
            });
        }
        if fees_1 > Uint128::zero() {
            response = response.add_message(BankMsg::Send {
                to_address: recipient.to_string(),
                amount: vec![Coin { denom: state.token1.clone(), amount: fees_1 }],
            });
        }
        Ok(response)
    }
}

6.4 费率测试

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

    #[test]
    fn test_fee_tier_values() {
        assert_eq!(FeeTier::Stable.fee_rate(), Uint128::new(500));
        assert_eq!(FeeTier::Default.fee_rate(), Uint128::new(3000));
        assert_eq!(FeeTier::Volatile.fee_rate(), Uint128::new(10000));
        assert_eq!(FeeTier::Stable.tick_spacing(), 10);
        assert_eq!(FeeTier::Default.tick_spacing(), 60);
        assert_eq!(FeeTier::Volatile.tick_spacing(), 200);
    }

    #[test]
    fn test_calculate_fee() {
        let amount = Uint128::new(1_000_000_000_000_000_000u128);
        let (after, fee) = FeeCalculator::calculate_fee(amount, Uint128::new(3000));
        assert_eq!(fee, Uint128::new(3_000_000_000_000_000u128));
        assert_eq!(after + fee, amount);
    }

    #[test]
    fn test_protocol_fee() {
        let fee_amount = Uint128::new(1_000_000u128);
        let (lp_fee, protocol_fee) =
            FeeCalculator::calculate_protocol_fee(fee_amount, Uint128::new(100_000));
        assert_eq!(protocol_fee, Uint128::new(100_000u128));
        assert_eq!(lp_fee, Uint128::new(900_000u128));
    }
}

7. Oracle 集成

7.1 TWAP Oracle 原理

Uniswap V3 的 Oracle 通过累积 tick 和 secondsPerLiquidity 来实现时间加权平均价格(TWAP)。

核心公式:

tickCumulative = Σ(tick * Δt)
secondsPerLiquidityCumulative = Σ(Δt / liquidity)

TWAP 计算:

twapTick = (tickCumulative[t1] - tickCumulative[t0]) / (t1 - t0)
twapPrice = 1.0001^twapTick
// contracts/pool_v3/src/oracle.rs
use cosmwasm_std::{Env, StdError, StdResult, Storage, Uint128};
use cw_storage_plus::Item;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::state::PoolState;

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Observation {
    pub tick_cumulative: i128,
    pub seconds_per_liquidity_cumulative: Uint128,
    pub block_timestamp: u64,
    pub tick: i32,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct OracleState {
    pub observations: Vec<Observation>,
    pub index: u32,
    pub cardinality: u32,
    pub cardinality_max: u32,
}

pub const ORACLE_STATE: Item<OracleState> = Item::new("oracle_state");

pub struct OracleManager;

impl OracleManager {
    pub fn initialize(
        storage: &mut dyn Storage,
        env: &Env,
        state: &PoolState,
    ) -> StdResult<()> {
        let observation = Observation {
            tick_cumulative: 0i128,
            seconds_per_liquidity_cumulative: Uint128::zero(),
            block_timestamp: env.block.time.seconds(),
            tick: state.tick,
        };
        let oracle = OracleState {
            observations: vec![observation; 1],
            index: 0,
            cardinality: 1,
            cardinality_max: 100,
        };
        ORACLE_STATE.save(storage, &oracle)
    }

    pub fn observe(
        storage: &mut dyn Storage,
        env: &Env,
        state: &PoolState,
    ) -> StdResult<()> {
        let mut oracle = ORACLE_STATE.load(storage)?;
        let current_time = env.block.time.seconds();
        let last_observation = &oracle.observations[oracle.index as usize];

        if current_time == last_observation.block_timestamp {
            let mut obs = oracle.observations.get_mut(oracle.index as usize).unwrap();
            obs.tick = state.tick;
        } else {
            let time_delta = current_time - last_observation.block_timestamp;
            let liq = state.liquidity;
            let seconds_per_liq = if liq > Uint128::zero() {
                Uint128::from(time_delta) * Uint256::from(1u128 << 96) / Uint256::from(liq.u128())
            } else {
                Uint128::zero()
            };

            let tick_cumulative = last_observation.tick_cumulative
                + (last_observation.tick as i128) * (time_delta as i128);
            let sec_liq_cumulative = last_observation.seconds_per_liquidity_cumulative
                + seconds_per_liq;

            let new_index = (oracle.index + 1) % oracle.cardinality_max;

            let observation = Observation {
                tick_cumulative,
                seconds_per_liquidity_cumulative: sec_liq_cumulative,
                block_timestamp: current_time,
                tick: state.tick,
            };

            if oracle.cardinality < oracle.cardinality_max {
                oracle.observations.push(observation);
                oracle.cardinality += 1;
            } else {
                let idx = new_index as usize;
                if idx < oracle.observations.len() {
                    oracle.observations[idx] = observation;
                } else {
                    oracle.observations.push(observation);
                }
            }
            oracle.index = new_index;
        }

        ORACLE_STATE.save(storage, &oracle)
    }

    pub fn consult_twap(
        storage: &dyn Storage,
        env: &Env,
        seconds_ago: u32,
    ) -> StdResult<i32> {
        let oracle = ORACLE_STATE.load(storage)?;
        if oracle.cardinality < 2 {
            return Ok(oracle.observations[oracle.index as usize].tick);
        }

        let latest = oracle.observations[oracle.index as usize].clone();
        let oldest_idx = if oracle.cardinality < oracle.cardinality_max {
            0usize
        } else {
            ((oracle.index + 1) % oracle.cardinality_max) as usize
        };
        let oldest = oracle.observations[oldest_idx].clone();

        let time_elapsed = latest.block_timestamp - oldest.block_timestamp;
        if time_elapsed == 0 {
            return Ok(latest.tick);
        }

        let tick_cumulative_delta = latest.tick_cumulative - oldest.tick_cumulative;
        let twap_tick = (tick_cumulative_delta / time_elapsed as i128) as i32;
        Ok(twap_tick)
    }
}

7.2 Oracle 查询接口

// contracts/pool_v3/src/msg.rs
use cosmwasm_std::Uint128;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum OracleQuery {
    Observations {},
    Twap { seconds_ago: u32 },
    Cumulative {},
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct TwapResponse {
    pub tick: i32,
    pub sqrt_price: Uint256,
    pub price: Uint128,
}

// contracts/pool_v3/src/contract.rs (query)
use crate::oracle::{ORACLE_STATE, OracleManager};
use crate::msg::OracleQuery;

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::Oracle { query } => match query {
            OracleQuery::Observations {} => {
                let oracle = ORACLE_STATE.load(deps.storage)?;
                to_binary(&oracle)
            }
            OracleQuery::Twap { seconds_ago } => {
                let tick = OracleManager::consult_twap(deps.storage, &env, seconds_ago)?;
                let sqrt_price = tick_to_sqrt_price(tick);
                let price = sqrt_price.from_q64_96();
                to_binary(&TwapResponse { tick, sqrt_price, price })
            }
            OracleQuery::Cumulative {} => {
                let oracle = ORACLE_STATE.load(deps.storage)?;
                let latest = oracle.observations[oracle.index as usize].clone();
                to_binary(&latest)
            }
        },
        _ => Err(StdError::generic_err("Unknown query")),
    }
}

7.3 Oracle 测试

#[cfg(test)]
mod oracle_tests {
    use super::*;
    use cosmwasm_std::testing::{mock_dependencies, mock_env};
    use cosmwasm_std::{BlockInfo, Timestamp};

    #[test]
    fn test_oracle_initialize() {
        let mut deps = mock_dependencies();
        let mut env = mock_env();
        env.block = BlockInfo {
            height: 1,
            time: Timestamp::from_seconds(1000),
            chain_id: "msg-chain-1".to_string(),
        };

        let state = PoolState::initialize(tick_to_sqrt_price(0), 0);
        OracleManager::initialize(deps.as_mut().storage, &env, &state).unwrap();

        let oracle = ORACLE_STATE.load(deps.as_ref().storage).unwrap();
        assert_eq!(oracle.cardinality, 1);
        assert_eq!(oracle.observations[0].block_timestamp, 1000);
    }

    #[test]
    fn test_oracle_observe() {
        let mut deps = mock_dependencies();
        let mut env = mock_env();
        env.block = BlockInfo {
            height: 1,
            time: Timestamp::from_seconds(1000),
            chain_id: "msg-chain-1".to_string(),
        };

        let mut state = PoolState::initialize(tick_to_sqrt_price(0), 0);
        state.liquidity = Uint128::new(1_000_000_000_000_000_000u128);
        OracleManager::initialize(deps.as_mut().storage, &env, &state).unwrap();

        env.block = BlockInfo {
            height: 2,
            time: Timestamp::from_seconds(1100),
            chain_id: "msg-chain-1".to_string(),
        };
        state.tick = -100;
        OracleManager::observe(deps.as_mut().storage, &env, &state).unwrap();

        let oracle = ORACLE_STATE.load(deps.as_ref().storage).unwrap();
        assert_eq!(oracle.cardinality, 2);
        assert_eq!(oracle.observations[1].tick, -100);
        assert!(oracle.observations[1].tick_cumulative != 0);
    }

    #[test]
    fn test_twap_consult() {
        let mut deps = mock_dependencies();
        let mut env = mock_env();
        env.block = BlockInfo {
            height: 1,
            time: Timestamp::from_seconds(1000),
            chain_id: "msg-chain-1".to_string(),
        };

        let mut state = PoolState::initialize(tick_to_sqrt_price(0), 0);
        state.liquidity = Uint128::new(1_000_000_000_000_000_000u128);
        OracleManager::initialize(deps.as_mut().storage, &env, &state).unwrap();

        env.block = BlockInfo {
            height: 2,
            time: Timestamp::from_seconds(2000),
            chain_id: "msg-chain-1".to_string(),
        };
        state.tick = 500;
        OracleManager::observe(deps.as_mut().storage, &env, &state).unwrap();

        let twap = OracleManager::consult_twap(deps.as_ref().storage, &env, 1000).unwrap();
        assert!(twap >= 0);
    }
}

8. 前端集成

8.1 React Hooks

// frontend/src/hooks/useClamm.ts
import { useCallback, useEffect, useState } from 'react';
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate';
import { Coin, StdFee } from '@cosmjs/stargate';

export interface PoolInfo {
  token0: string;
  token1: string;
  sqrtPrice: string;
  tick: number;
  liquidity: string;
  tickSpacing: number;
  feeRate: number;
}

export interface PositionInfo {
  liquidity: string;
  feeGrowthInside0: string;
  feeGrowthInside1: string;
  tokensOwed0: string;
  tokensOwed1: string;
}

export interface TickInfo {
  liquidityGross: string;
  liquidityNet: string;
  feeGrowthOutside0: string;
  feeGrowthOutside1: string;
}

const MSG_DECIMALS = 18;

export function useClamm(client: SigningCosmWasmClient | null) {
  const [pools, setPools] = useState<PoolInfo[]>([]);
  const [loading, setLoading] = useState(false);

  const fetchPoolState = useCallback(
    async (poolAddr: string): Promise<PoolInfo> => {
      if (!client) throw new Error('Client not connected');
      const state: PoolInfo = await client.queryContractSmart(poolAddr, {
        pool_state: { query: { state: {} } },
      });
      return state;
    },
    [client]
  );

  const fetchPosition = useCallback(
    async (
      poolAddr: string,
      owner: string,
      lowerTick: number,
      upperTick: number
    ): Promise<PositionInfo> => {
      if (!client) throw new Error('Client not connected');
      const pos: PositionInfo = await client.queryContractSmart(poolAddr, {
        position: {
          query: {
            position: {
              owner,
              lower_tick: lowerTick,
              upper_tick: upperTick,
            },
          },
        },
      });
      return pos;
    },
    [client]
  );

  const fetchTwap = useCallback(
    async (poolAddr: string, secondsAgo: number) => {
      if (!client) throw new Error('Client not connected');
      return await client.queryContractSmart(poolAddr, {
        oracle: {
          query: { twap: { seconds_ago: secondsAgo } },
        },
      });
    },
    [client]
  );

  const estimateSwap = useCallback(
    async (
      poolAddr: string,
      amountIn: string,
      zeroForOne: boolean,
      sqrtPriceLimit?: string
    ) => {
      if (!client) throw new Error('Client not connected');
      const pool = await fetchPoolState(poolAddr);
      const priceLimit =
        sqrtPriceLimit ||
        (zeroForOne ? '4295128739' : '1461446703485210103287273052203988822378723970342');

      const msg = {
        swap: {
          exact_in: true,
          amount: amountIn,
          zero_for_one: zeroForOne,
          sqrt_price_limit: priceLimit,
          amount_limit: '0',
          deadline: Math.floor(Date.now() / 1000) + 600,
        },
      };

      // Simulate by querying
      const result = await client.queryContractSmart(poolAddr, {
        simulate: msg,
      });
      return result;
    },
    [client, fetchPoolState]
  );

  const addLiquidity = useCallback(
    async (
      poolAddr: string,
      lowerTick: number,
      upperTick: number,
      amount0: string,
      amount1: string,
      amount0Min: string,
      amount1Min: string,
      funds: Coin[]
    ) => {
      if (!client) throw new Error('Client not connected');
      const sender = (await client.getAccount()).address;

      const msg = {
        mint: {
          lower_tick: lowerTick,
          upper_tick: upperTick,
          amount0_desired: amount0,
          amount1_desired: amount1,
          amount0_min: amount0Min,
          amount1_min: amount1Min,
          recipient: sender,
          deadline: Math.floor(Date.now() / 1000) + 600,
        },
      };

      const fee: StdFee = {
        amount: [{ denom: 'umsg', amount: '1000000' }],
        gas: '3000000',
      };

      const result = await client.execute(sender, poolAddr, msg, fee, undefined, funds);
      return result;
    },
    [client]
  );

  const removeLiquidity = useCallback(
    async (
      poolAddr: string,
      lowerTick: number,
      upperTick: number,
      liquidity: string,
      amount0Min: string,
      amount1Min: string
    ) => {
      if (!client) throw new Error('Client not connected');
      const sender = (await client.getAccount()).address;

      const msg = {
        burn: {
          lower_tick: lowerTick,
          upper_tick: upperTick,
          liquidity,
          amount0_min: amount0Min,
          amount1_min: amount1Min,
          deadline: Math.floor(Date.now() / 1000) + 600,
        },
      };

      const fee: StdFee = {
        amount: [{ denom: 'umsg', amount: '500000' }],
        gas: '2000000',
      };

      const result = await client.execute(sender, poolAddr, msg, fee);
      return result;
    },
    [client]
  );

  const collectFees = useCallback(
    async (poolAddr: string, lowerTick: number, upperTick: number) => {
      if (!client) throw new Error('Client not connected');
      const sender = (await client.getAccount()).address;

      const msg = {
        collect: {
          lower_tick: lowerTick,
          upper_tick: upperTick,
          recipient: null,
        },
      };

      const fee: StdFee = {
        amount: [{ denom: 'umsg', amount: '300000' }],
        gas: '1500000',
      };

      const result = await client.execute(sender, poolAddr, msg, fee);
      return result;
    },
    [client]
  );

  const swap = useCallback(
    async (
      poolAddr: string,
      amount: string,
      zeroForOne: boolean,
      amountLimit: string,
      funds: Coin[]
    ) => {
      if (!client) throw new Error('Client not connected');
      const sender = (await client.getAccount()).address;

      const sqrtPriceLimit = zeroForOne
        ? '4295128739'
        : '1461446703485210103287273052203988822378723970342';

      const msg = {
        swap: {
          exact_in: true,
          amount,
          zero_for_one: zeroForOne,
          sqrt_price_limit: sqrtPriceLimit,
          amount_limit: amountLimit,
          deadline: Math.floor(Date.now() / 1000) + 600,
        },
      };

      const fee: StdFee = {
        amount: [{ denom: 'umsg', amount: '1000000' }],
        gas: '3000000',
      };

      const result = await client.execute(sender, poolAddr, msg, fee, undefined, funds);
      return result;
    },
    [client]
  );

  return {
    pools,
    loading,
    fetchPoolState,
    fetchPosition,
    fetchTwap,
    estimateSwap,
    addLiquidity,
    removeLiquidity,
    collectFees,
    swap,
  };
}

8.2 添加流动性组件

// frontend/src/components/AddLiquidity.tsx
import React, { useState, useEffect, useCallback } from 'react';
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate';
import { Coin } from '@cosmjs/stargate';
import { useClamm } from '../hooks/useClamm';

interface Props {
  client: SigningCosmWasmClient;
  poolAddr: string;
}

export const AddLiquidity: React.FC<Props> = ({ client, poolAddr }) => {
  const { fetchPoolState, addLiquidity } = useClamm(client);
  const [amount0, setAmount0] = useState('');
  const [amount1, setAmount1] = useState('');
  const [lowerPrice, setLowerPrice] = useState('');
  const [upperPrice, setUpperPrice] = useState('');
  const [currentPrice, setCurrentPrice] = useState(0);
  const [slippage, setSlippage] = useState(5);
  const [loading, setLoading] = useState(false);
  const [txHash, setTxHash] = useState('');

  useEffect(() => {
    fetchPoolState(poolAddr).then((pool) => {
      const price = Number(pool.sqrtPrice) / 2 ** 96;
      setCurrentPrice(price);
    });
  }, [poolAddr, fetchPoolState]);

  const priceToTick = (price: number): number => {
    return Math.round(Math.log(price) / Math.log(1.0001));
  };

  const handleAddLiquidity = useCallback(async () => {
    if (!amount0 || !amount1 || !lowerPrice || !upperPrice) return;
    setLoading(true);

    try {
      const lowerTick = priceToTick(parseFloat(lowerPrice));
      const upperTick = priceToTick(parseFloat(upperPrice));

      const amount0Min = Math.floor(
        parseFloat(amount0) * (1 - slippage / 100) * 10 ** 18
      ).toString();
      const amount1Min = Math.floor(
        parseFloat(amount1) * (1 - slippage / 100) * 10 ** 18
      ).toString();

      const funds: Coin[] = [
        { denom: 'umsg', amount: Math.floor(parseFloat(amount0) * 10 ** 18).toString() },
      ];

      const result = await addLiquidity(
        poolAddr,
        lowerTick,
        upperTick,
        Math.floor(parseFloat(amount0) * 10 ** 18).toString(),
        Math.floor(parseFloat(amount1) * 10 ** 18).toString(),
        amount0Min,
        amount1Min,
        funds
      );
      setTxHash(result.transactionHash);
    } catch (err) {
      console.error('Add liquidity failed:', err);
    }
    setLoading(false);
  }, [amount0, amount1, lowerPrice, upperPrice, slippage, poolAddr, addLiquidity]);

  const formatPrice = (num: number) => num.toFixed(6);

  return (
    <div className="bg-gray-900 rounded-xl p-6 max-w-md mx-auto text-white">
      <h2 className="text-xl font-bold mb-4">Add Liquidity</h2>

      <div className="mb-4">
        <label className="block text-sm text-gray-400 mb-1">Current Price</label>
        <div className="text-lg font-mono">{formatPrice(currentPrice)}</div>
      </div>

      <div className="grid grid-cols-2 gap-4 mb-4">
        <div>
          <label className="block text-sm text-gray-400 mb-1">Min Price</label>
          <input
            type="number"
            value={lowerPrice}
            onChange={(e) => setLowerPrice(e.target.value)}
            className="w-full bg-gray-800 rounded px-3 py-2 text-white"
            placeholder="0.0"
          />
        </div>
        <div>
          <label className="block text-sm text-gray-400 mb-1">Max Price</label>
          <input
            type="number"
            value={upperPrice}
            onChange={(e) => setUpperPrice(e.target.value)}
            className="w-full bg-gray-800 rounded px-3 py-2 text-white"
            placeholder="0.0"
          />
        </div>
      </div>

      <div className="mb-4">
        <label className="block text-sm text-gray-400 mb-1">Amount 0 (umsg)</label>
        <input
          type="number"
          value={amount0}
          onChange={(e) => setAmount0(e.target.value)}
          className="w-full bg-gray-800 rounded px-3 py-2 text-white"
          placeholder="0.0"
        />
      </div>

      <div className="mb-4">
        <label className="block text-sm text-gray-400 mb-1">Amount 1</label>
        <input
          type="number"
          value={amount1}
          onChange={(e) => setAmount1(e.target.value)}
          className="w-full bg-gray-800 rounded px-3 py-2 text-white"
          placeholder="0.0"
        />
      </div>

      <div className="mb-6">
        <label className="block text-sm text-gray-400 mb-1">Slippage {slippage}%</label>
        <input
          type="range"
          min="0.1"
          max="20"
          step="0.1"
          value={slippage}
          onChange={(e) => setSlippage(parseFloat(e.target.value))}
          className="w-full"
        />
      </div>

      <button
        onClick={handleAddLiquidity}
        disabled={loading}
        className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 rounded-lg py-3 font-bold"
      >
        {loading ? 'Processing...' : 'Add Liquidity'}
      </button>

      {txHash && (
        <div className="mt-4 text-sm text-green-400 break-all">
          Tx: {txHash}
        </div>
      )}
    </div>
  );
};

8.3 Swap Panel 组件

// frontend/src/components/SwapPanel.tsx
import React, { useState, useCallback, useEffect } from 'react';
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate';
import { Coin } from '@cosmjs/stargate';
import { useClamm } from '../hooks/useClamm';

interface Props {
  client: SigningCosmWasmClient;
  poolAddr: string;
}

export const SwapPanel: React.FC<Props> = ({ client, poolAddr }) => {
  const { fetchPoolState, estimateSwap, swap } = useClamm(client);
  const [amountIn, setAmountIn] = useState('');
  const [amountOut, setAmountOut] = useState('');
  const [zeroForOne, setZeroForOne] = useState(true);
  const [slippage, setSlippage] = useState(5);
  const [priceImpact, setPriceImpact] = useState(0);
  const [loading, setLoading] = useState(false);
  const [txHash, setTxHash] = useState('');
  const [pool, setPool] = useState<any>(null);

  useEffect(() => {
    fetchPoolState(poolAddr).then(setPool);
  }, [poolAddr, fetchPoolState]);

  useEffect(() => {
    if (!amountIn) return;
    const timer = setTimeout(async () => {
      try {
        const result = await estimateSwap(
          poolAddr,
          Math.floor(parseFloat(amountIn) * 10 ** 18).toString(),
          zeroForOne
        );
        const out = parseFloat(result.amount_out) / 10 ** 18;
        setAmountOut(out.toFixed(6));

        if (pool) {
          const priceBefore = parseInt(pool.sqrtPrice) / 2 ** 96;
          const priceAfter = parseInt(result.sqrt_price_after) / 2 ** 96;
          const impact = Math.abs((priceAfter - priceBefore) / priceBefore) * 100;
          setPriceImpact(impact);
        }
      } catch (e) {
        console.error('Estimation failed', e);
      }
    }, 500);
    return () => clearTimeout(timer);
  }, [amountIn, zeroForOne, poolAddr, estimateSwap, pool]);

  const handleSwap = useCallback(async () => {
    if (!amountIn || !amountOut) return;
    setLoading(true);

    try {
      const amountOutMin = Math.floor(
        parseFloat(amountOut) * (1 - slippage / 100) * 10 ** 18
      ).toString();

      const funds: Coin[] = zeroForOne
        ? [{ denom: 'umsg', amount: Math.floor(parseFloat(amountIn) * 10 ** 18).toString() }]
        : [];

      const result = await swap(
        poolAddr,
        Math.floor(parseFloat(amountIn) * 10 ** 18).toString(),
        zeroForOne,
        amountOutMin,
        funds
      );
      setTxHash(result.transactionHash);
    } catch (err) {
      console.error('Swap failed:', err);
    }
    setLoading(false);
  }, [amountIn, amountOut, zeroForOne, slippage, poolAddr, swap]);

  const toggleDirection = () => {
    setZeroForOne(!zeroForOne);
    setAmountIn(amountOut);
    setAmountOut(amountIn);
  };

  return (
    <div className="bg-gray-900 rounded-xl p-6 max-w-md mx-auto text-white">
      <h2 className="text-xl font-bold mb-4">Swap</h2>

      <div className="mb-4">
        <label className="block text-sm text-gray-400 mb-1">
          {zeroForOne ? 'umsg' : 'Token1'}
        </label>
        <input
          type="number"
          value={amountIn}
          onChange={(e) => setAmountIn(e.target.value)}
          className="w-full bg-gray-800 rounded px-3 py-2 text-white"
          placeholder="0.0"
        />
      </div>

      <button
        onClick={toggleDirection}
        className="w-full flex justify-center my-2 text-gray-400 hover:text-white"
      >
        &#8593;&#8595;
      </button>

      <div className="mb-4">
        <label className="block text-sm text-gray-400 mb-1">
          {zeroForOne ? 'Token1' : 'umsg'}
        </label>
        <input
          type="number"
          value={amountOut}
          readOnly
          className="w-full bg-gray-800 rounded px-3 py-2 text-white opacity-70"
          placeholder="0.0"
        />
      </div>

      {priceImpact > 0 && (
        <div className={`text-sm mb-4 ${priceImpact > 5 ? 'text-red-400' : 'text-yellow-400'}`}>
          Price Impact: {priceImpact.toFixed(2)}%
        </div>
      )}

      <div className="mb-6">
        <label className="block text-sm text-gray-400 mb-1">
          Slippage {slippage}%
        </label>
        <input
          type="range"
          min="0.1"
          max="20"
          step="0.1"
          value={slippage}
          onChange={(e) => setSlippage(parseFloat(e.target.value))}
          className="w-full"
        />
      </div>

      <button
        onClick={handleSwap}
        disabled={loading || !amountIn}
        className="w-full bg-green-600 hover:bg-green-700 disabled:bg-gray-600 rounded-lg py-3 font-bold"
      >
        {loading ? 'Swapping...' : 'Swap'}
      </button>

      {txHash && (
        <div className="mt-4 text-sm text-green-400 break-all">Tx: {txHash}</div>
      )}
    </div>
  );
};

8.4 Pool Creator 组件

// frontend/src/components/PoolCreator.tsx
import React, { useState } from 'react';
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate';
import { StdFee } from '@cosmjs/stargate';

interface Props {
  client: SigningCosmWasmClient;
  factoryAddr: string;
}

const FEE_TIERS = [
  { label: '0.05% - Stable', value: 500, spacing: 10 },
  { label: '0.30% - Default', value: 3000, spacing: 60 },
  { label: '1.00% - Volatile', value: 10000, spacing: 200 },
];

export const PoolCreator: React.FC<Props> = ({ client, factoryAddr }) => {
  const [token0, setToken0] = useState('umsg');
  const [token1, setToken1] = useState('');
  const [feeTier, setFeeTier] = useState(3000);
  const [initialPrice, setInitialPrice] = useState('1.0');
  const [loading, setLoading] = useState(false);
  const [txHash, setTxHash] = useState('');

  const handleCreate = async () => {
    setLoading(true);
    try {
      const sender = (await client.getAccount()).address;
      const sqrtPrice = Math.floor(Math.sqrt(parseFloat(initialPrice)) * 2 ** 96).toString();

      const feeTierStr = FEE_TIERS.find((f) => f.value === feeTier)?.label.split(' - ')[0] || '';

      const msg = {
        create_pool: {
          token0,
          token1,
          fee_tier: feeTierStr,
          sqrt_price_x96: sqrtPrice,
        },
      };

      const fee: StdFee = {
        amount: [{ denom: 'umsg', amount: '2000000' }],
        gas: '5000000',
      };

      const result = await client.execute(sender, factoryAddr, msg, fee);
      setTxHash(result.transactionHash);
    } catch (err) {
      console.error('Create pool failed:', err);
    }
    setLoading(false);
  };

  return (
    <div className="bg-gray-900 rounded-xl p-6 max-w-md mx-auto text-white">
      <h2 className="text-xl font-bold mb-4">Create Pool</h2>

      <div className="mb-4">
        <label className="block text-sm text-gray-400 mb-1">Token 0</label>
        <input
          type="text"
          value={token0}
          onChange={(e) => setToken0(e.target.value)}
          className="w-full bg-gray-800 rounded px-3 py-2 text-white"
        />
      </div>

      <div className="mb-4">
        <label className="block text-sm text-gray-400 mb-1">Token 1</label>
        <input
          type="text"
          value={token1}
          onChange={(e) => setToken1(e.target.value)}
          className="w-full bg-gray-800 rounded px-3 py-2 text-white"
        />
      </div>

      <div className="mb-4">
        <label className="block text-sm text-gray-400 mb-1">Fee Tier</label>
        <select
          value={feeTier}
          onChange={(e) => setFeeTier(parseInt(e.target.value))}
          className="w-full bg-gray-800 rounded px-3 py-2 text-white"
        >
          {FEE_TIERS.map((ft) => (
            <option key={ft.value} value={ft.value}>
              {ft.label}
            </option>
          ))}
        </select>
      </div>

      <div className="mb-6">
        <label className="block text-sm text-gray-400 mb-1">Initial Price (Token1 per Token0)</label>
        <input
          type="number"
          value={initialPrice}
          onChange={(e) => setInitialPrice(e.target.value)}
          step="0.0001"
          className="w-full bg-gray-800 rounded px-3 py-2 text-white"
        />
      </div>

      <button
        onClick={handleCreate}
        disabled={loading || !token1}
        className="w-full bg-purple-600 hover:bg-purple-700 disabled:bg-gray-600 rounded-lg py-3 font-bold"
      >
        {loading ? 'Creating...' : 'Create Pool'}
      </button>

      {txHash && <div className="mt-4 text-sm text-green-400 break-all">Tx: {txHash}</div>}
    </div>
  );
};

9. 完整示例

9.1 TypeScript SDK

// ts-sdk/src/math.ts
import { Decimal } from 'decimal.js';

export const Q96 = new Decimal(2).pow(96);
export const MIN_TICK = -887272;
export const MAX_TICK = 887272;

export function tickToSqrtPrice(tick: number): Decimal {
  return new Decimal(1.0001).pow(tick / 2).mul(Q96);
}

export function sqrtPriceToTick(sqrtPrice: Decimal): number {
  const price = sqrtPrice.div(Q96);
  const tick = Decimal.log(price, 1.0001).mul(2);
  return Math.round(tick.toNumber());
}

export function priceToTick(price: number): number {
  return Math.round(Math.log(price) / Math.log(1.0001));
}

export function tickToPrice(tick: number): number {
  return Math.pow(1.0001, tick);
}

export function getAmount0Delta(
  sqrtRatioAX96: Decimal,
  sqrtRatioBX96: Decimal,
  liquidity: Decimal
): Decimal {
  if (sqrtRatioAX96.gt(sqrtRatioBX96)) {
    [sqrtRatioAX96, sqrtRatioBX96] = [sqrtRatioBX96, sqrtRatioAX96];
  }
  const numerator = liquidity.mul(sqrtRatioBX96.sub(sqrtRatioAX96));
  const denominator = sqrtRatioBX96.mul(sqrtRatioAX96);
  return numerator.div(denominator).mul(Q96);
}

export function getAmount1Delta(
  sqrtRatioAX96: Decimal,
  sqrtRatioBX96: Decimal,
  liquidity: Decimal
): Decimal {
  if (sqrtRatioAX96.gt(sqrtRatioBX96)) {
    [sqrtRatioAX96, sqrtRatioBX96] = [sqrtRatioBX96, sqrtRatioAX96];
  }
  return liquidity.mul(sqrtRatioBX96.sub(sqrtRatioAX96)).div(Q96);
}

export function getLiquidityForAmount0(
  sqrtRatioAX96: Decimal,
  sqrtRatioBX96: Decimal,
  amount0: Decimal
): Decimal {
  if (sqrtRatioAX96.gt(sqrtRatioBX96)) {
    [sqrtRatioAX96, sqrtRatioBX96] = [sqrtRatioBX96, sqrtRatioAX96];
  }
  const numerator = amount0.mul(sqrtRatioAX96).mul(sqrtRatioBX96);
  const denominator = sqrtRatioBX96.sub(sqrtRatioAX96);
  return numerator.div(denominator).div(Q96);
}

export function getLiquidityForAmount1(
  sqrtRatioAX96: Decimal,
  sqrtRatioBX96: Decimal,
  amount1: Decimal
): Decimal {
  if (sqrtRatioAX96.gt(sqrtRatioBX96)) {
    [sqrtRatioAX96, sqrtRatioBX96] = [sqrtRatioBX96, sqrtRatioAX96];
  }
  return amount1.mul(Q96).div(sqrtRatioBX96.sub(sqrtRatioAX96));
}

export function estimateSwapIn(
  sqrtPriceCurrent: Decimal,
  sqrtPriceTarget: Decimal,
  liquidity: Decimal,
  amountIn: Decimal
): { sqrtPriceAfter: Decimal; amountOut: Decimal } {
  const zeroForOne = sqrtPriceCurrent.gt(sqrtPriceTarget);
  if (zeroForOne) {
    const numerator = amountIn.mul(Q96).mul(sqrtPriceCurrent).mul(sqrtPriceTarget);
    const denominator = liquidity.mul(sqrtPriceTarget).add(amountIn.mul(sqrtPriceCurrent));
    const sqrtNext = numerator.div(denominator);
    const amountOut = liquidity.mul(sqrtPriceCurrent.sub(sqrtNext)).div(Q96);
    return { sqrtPriceAfter: sqrtNext, amountOut };
  } else {
    const sqrtNext = sqrtPriceCurrent.add(
      amountIn.mul(Q96).div(liquidity)
    );
    const amountOut = liquidity.mul(sqrtNext.sub(sqrtPriceCurrent)).div(Q96);
    return { sqrtPriceAfter: sqrtNext, amountOut };
  }
}

9.2 Pool Class

// ts-sdk/src/pool.ts
import { Decimal } from 'decimal.js';
import { Q96, tickToSqrtPrice } from './math';

export interface PoolConfig {
  token0: string;
  token1: string;
  feeRate: number;
  tickSpacing: number;
}

export class Pool {
  public sqrtPrice: Decimal;
  public tick: number;
  public liquidity: Decimal;
  public readonly config: PoolConfig;

  constructor(
    sqrtPrice: Decimal,
    tick: number,
    liquidity: Decimal,
    config: PoolConfig
  ) {
    this.sqrtPrice = sqrtPrice;
    this.tick = tick;
    this.liquidity = liquidity;
    this.config = config;
  }

  static fromContractState(state: any, config: PoolConfig): Pool {
    return new Pool(
      new Decimal(state.sqrt_price),
      state.tick,
      new Decimal(state.liquidity),
      config
    );
  }

  get price(): number {
    return this.sqrtPrice.div(Q96).pow(2).toNumber();
  }

  public getAmount0Delta(
    lowerTick: number,
    upperTick: number,
    liquidity: Decimal
  ): Decimal {
    const sqrtA = tickToSqrtPrice(lowerTick);
    const sqrtB = tickToSqrtPrice(upperTick);
    if (sqrtA.gt(sqrtB)) {
      return getAmount0Delta(sqrtB, sqrtA, liquidity);
    }
    return getAmount0Delta(sqrtA, sqrtB, liquidity);
  }

  public getAmount1Delta(
    lowerTick: number,
    upperTick: number,
    liquidity: Decimal
  ): Decimal {
    const sqrtA = tickToSqrtPrice(lowerTick);
    const sqrtB = tickToSqrtPrice(upperTick);
    if (sqrtA.gt(sqrtB)) {
      return getAmount1Delta(sqrtB, sqrtA, liquidity);
    }
    return getAmount1Delta(sqrtA, sqrtB, liquidity);
  }
}

9.3 Router 计算

// ts-sdk/src/router.ts
import { Pool } from './pool';
import { estimateSwapIn, tickToSqrtPrice, sqrtPriceToTick } from './math';
import { Decimal } from 'decimal.js';
import { Q96 } from './math';

export interface SwapRoute {
  pool: Pool;
  amountIn: Decimal;
  amountOut: Decimal;
  sqrtPriceAfter: Decimal;
}

export class Router {
  public findBestRoute(
    pools: Pool[],
    amountIn: Decimal,
    zeroForOne: boolean
  ): SwapRoute | null {
    let bestRoute: SwapRoute | null = null;

    for (const pool of pools) {
      const priceLimit = zeroForOne
        ? new Decimal('4295128739')
        : new Decimal('1461446703485210103287273052203988822378723970342');

      const currentSqrtPrice = pool.sqrtPrice;

      // Simulate swap
      const result = estimateSwapIn(
        currentSqrtPrice,
        priceLimit,
        pool.liquidity,
        amountIn
      );

      if (!bestRoute || result.amountOut.gt(bestRoute.amountOut)) {
        bestRoute = {
          pool,
          amountIn,
          amountOut: result.amountOut,
          sqrtPriceAfter: result.sqrtPriceAfter,
        };
      }
    }

    return bestRoute;
  }

  public calculatePositionValue(
    pool: Pool,
    lowerTick: number,
    upperTick: number,
    liquidity: Decimal
  ): { amount0: Decimal; amount1: Decimal } {
    const sqrtA = tickToSqrtPrice(lowerTick);
    const sqrtB = tickToSqrtPrice(upperTick);
    const currentSqrt = pool.sqrtPrice;

    if (currentSqrt.lte(sqrtA)) {
      // Price below range: all token0
      const amount0 = getAmount0Delta(sqrtA, sqrtB, liquidity);
      return { amount0, amount1: new Decimal(0) };
    } else if (currentSqrt.gte(sqrtB)) {
      // Price above range: all token1
      const amount1 = getAmount1Delta(sqrtA, sqrtB, liquidity);
      return { amount0: new Decimal(0), amount1 };
    } else {
      // Price in range: both tokens
      const amount0 = getAmount0Delta(currentSqrt, sqrtB, liquidity);
      const amount1 = getAmount1Delta(sqrtA, currentSqrt, liquidity);
      return { amount0, amount1 };
    }
  }
}

9.4 部署脚本

// scripts/deploy-v3.ts
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate';
import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing';
import { GasPrice, calculateFee } from '@cosmjs/stargate';
import Decimal from 'decimal.js';

const MSG_CHAIN_RPC = 'https://rpc.msg-chain-1.msgchain.org';
const MSG_CHAIN_PREFIX = 'msg';
const GAS_PRICE = GasPrice.fromString('1000000000attoMSG');

async function main() {
  // 1. Setup wallet
  const mnemonic = process.env.MNEMONIC || 'your mnemonic here';
  const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, {
    prefix: MSG_CHAIN_PREFIX,
  });
  const [account] = await wallet.getAccounts();

  console.log(`Deploying from: ${account.address}`);

  // 2. Connect client
  const client = await SigningCosmWasmClient.connectWithSigner(
    MSG_CHAIN_RPC,
    wallet,
    { gasPrice: GAS_PRICE }
  );

  // 3. Upload contracts
  const factoryCodeId = await uploadContract(client, account.address, './artifacts/factory_v3.wasm');
  console.log(`Factory code ID: ${factoryCodeId}`);

  const poolCodeId = await uploadContract(client, account.address, './artifacts/pool_v3.wasm');
  console.log(`Pool code ID: ${poolCodeId}`);

  const nftCodeId = await uploadContract(client, account.address, './artifacts/nft_descriptor.wasm');
  console.log(`NFT descriptor code ID: ${nftCodeId}`);

  const routerCodeId = await uploadContract(client, account.address, './artifacts/router_v3.wasm');
  console.log(`Router code ID: ${routerCodeId}`);

  // 4. Instantiate Factory
  const factoryAddr = await instantiateContract(
    client,
    account.address,
    factoryCodeId,
    'MSG CLAMM Factory v1',
    {}
  );
  console.log(`Factory: ${factoryAddr}`);

  // 5. Instantiate NFT Descriptor
  const nftAddr = await instantiateContract(
    client,
    account.address,
    nftCodeId,
    'MSG CLAMM Position NFT v1',
    {
      name: 'MSG CLAMM Position',
      symbol: 'CLAMM',
      minter: factoryAddr,
    }
  );
  console.log(`NFT Descriptor: ${nftAddr}`);

  // 6. Create a pool (MSG-USDC, 0.30% fee)
  const token0 = 'umsg';
  const token1 = 'uusdc';
  const initialPrice = new Decimal(1.0); // 1 MSG = 1 USDC
  const sqrtPrice = initialPrice.sqrt().mul(new Decimal(2).pow(96));

  const createMsg = {
    create_pool: {
      token0,
      token1,
      fee_rate: '3000',
      sqrt_price_x96: sqrtPrice.toFixed(0),
      pool_code_id: poolCodeId,
    },
  };

  const result = await client.execute(
    account.address,
    factoryAddr,
    createMsg,
    calculateFee(5000000, GAS_PRICE)
  );

  console.log(`Pool created! Tx: ${result.transactionHash}`);

  // 7. Find pool address from events
  const poolAddr = result.events
    .flatMap((e) => e.attributes)
    .find((a) => a.key === 'contract_address')?.value;
  console.log(`Pool: ${poolAddr}`);

  // 8. Add initial liquidity
  const lowerTick = -60000; // wide range for initial LP
  const upperTick = 60000;
  const amount0 = '1000000000000000000'; // 1 MSG
  const amount1 = '1000000000000000000'; // 1 USDC

  const mintMsg = {
    mint: {
      lower_tick: lowerTick,
      upper_tick: upperTick,
      amount0_desired: amount0,
      amount1_desired: amount1,
      amount0_min: '0',
      amount1_min: '0',
      recipient: account.address,
      deadline: Math.floor(Date.now() / 1000) + 600,
    },
  };

  const mintResult = await client.execute(
    account.address,
    poolAddr,
    mintMsg,
    calculateFee(3000000, GAS_PRICE),
    undefined,
    [
      { denom: 'umsg', amount: amount0 },
    ]
  );

  console.log(`Liquidity added! Tx: ${mintResult.transactionHash}`);

  // 9. Query pool state
  const state = await client.queryContractSmart(poolAddr, {
    pool_state: { query: { state: {} } },
  });
  console.log('Pool State:', JSON.stringify(state, null, 2));

  console.log('Deployment complete!');
}

async function uploadContract(
  client: SigningCosmWasmClient,
  sender: string,
  filePath: string
): Promise<number> {
  const wasm = require('fs').readFileSync(filePath);
  const result = await client.upload(sender, wasm, calculateFee(5000000, GAS_PRICE));
  return result.codeId;
}

async function instantiateContract(
  client: SigningCosmWasmClient,
  sender: string,
  codeId: number,
  label: string,
  msg: any
): Promise<string> {
  const result = await client.instantiate(
    sender,
    codeId,
    msg,
    label,
    calculateFee(2000000, GAS_PRICE)
  );
  return result.contractAddress;
}

main().catch(console.error);

9.5 Python 集成示例

#!/usr/bin/env python3
# scripts/py/clamm_client.py
"""
MSG Chain CLAMM V3 Python SDK

使用 cosmwasm-py 与 MSG Chain 的 V3 AMM 交互
"""

from decimal import Decimal
import hashlib
import json
from typing import Optional, Tuple
from cosmpy.aerial.client import LedgerClient
from cosmpy.aerial.wallet import LocalWallet
from cosmpy.aerial.contract import CosmWasmClient

Q96 = Decimal(2) ** 96
MIN_TICK = -887272
MAX_TICK = 887272
FEE_DENOMINATOR = 1_000_000


class ClammClient:
    """MSG Chain CLAMM V3 Client"""

    def __init__(
        self,
        rpc_endpoint: str,
        wallet: LocalWallet,
        factory_addr: str = "",
    ):
        self.client = LedgerClient(rpc_endpoint)
        self.wallet = wallet
        self.factory_addr = factory_addr

    @staticmethod
    def price_to_tick(price: Decimal) -> int:
        return int(round(Decimal.log10(price) / Decimal.log10(Decimal("1.0001"))))

    @staticmethod
    def tick_to_sqrt_price(tick: int) -> Decimal:
        return Decimal(1.0001) ** (Decimal(tick) / 2) * Q96

    @staticmethod
    def sqrt_price_to_tick(sqrt_price: Decimal) -> int:
        price = sqrt_price / Q96
        tick = price.log10() / Decimal("1.0001").log10() * 2
        return int(round(tick))

    def create_pool(
        self,
        token0: str,
        token1: str,
        fee_rate: int,
        initial_price: Decimal,
    ) -> str:
        """Create a new V3 pool"""
        token0, token1 = sorted([token0, token1])
        sqrt_price = int((initial_price.sqrt() * Q96).to_integral_value())

        msg = {
            "create_pool": {
                "token0": token0,
                "token1": token1,
                "fee_rate": str(fee_rate),
                "sqrt_price_x96": str(sqrt_price),
            }
        }

        # Execute contract call
        result = self.client.execute(
            self.wallet,
            self.factory_addr,
            json.dumps(msg).encode(),
            funds=[],
        )
        return result.tx_hash

    def add_liquidity(
        self,
        pool_addr: str,
        lower_tick: int,
        upper_tick: int,
        amount0: int,
        amount1: int,
        amount0_min: int = 0,
        amount1_min: int = 0,
        deadline: Optional[int] = None,
    ) -> str:
        """Add liquidity to a position"""
        if deadline is None:
            deadline = self._get_deadline()

        msg = {
            "mint": {
                "lower_tick": lower_tick,
                "upper_tick": upper_tick,
                "amount0_desired": str(amount0),
                "amount1_desired": str(amount1),
                "amount0_min": str(amount0_min),
                "amount1_min": str(amount1_min),
                "recipient": str(self.wallet.address()),
                "deadline": deadline,
            }
        }

        funds = []
        if amount0 > 0:
            funds.append({"denom": "umsg", "amount": str(amount0)})

        result = self.client.execute(
            self.wallet,
            pool_addr,
            json.dumps(msg).encode(),
            funds=funds,
        )
        return result.tx_hash

    def remove_liquidity(
        self,
        pool_addr: str,
        lower_tick: int,
        upper_tick: int,
        liquidity: int,
        amount0_min: int = 0,
        amount1_min: int = 0,
    ) -> str:
        """Remove liquidity from a position"""
        msg = {
            "burn": {
                "lower_tick": lower_tick,
                "upper_tick": upper_tick,
                "liquidity": str(liquidity),
                "amount0_min": str(amount0_min),
                "amount1_min": str(amount1_min),
                "deadline": self._get_deadline(),
            }
        }

        result = self.client.execute(
            self.wallet,
            pool_addr,
            json.dumps(msg).encode(),
        )
        return result.tx_hash

    def swap(
        self,
        pool_addr: str,
        amount: int,
        zero_for_one: bool,
        amount_limit: int = 0,
    ) -> str:
        """Execute a swap"""
        sqrt_price_limit = "4295128739" if zero_for_one else \
            "1461446703485210103287273052203988822378723970342"

        msg = {
            "swap": {
                "exact_in": True,
                "amount": str(amount),
                "zero_for_one": zero_for_one,
                "sqrt_price_limit": sqrt_price_limit,
                "amount_limit": str(amount_limit),
                "deadline": self._get_deadline(),
            }
        }

        funds = []
        if zero_for_one:
            funds.append({"denom": "umsg", "amount": str(amount)})

        result = self.client.execute(
            self.wallet,
            pool_addr,
            json.dumps(msg).encode(),
            funds=funds,
        )
        return result.tx_hash

    def collect_fees(
        self,
        pool_addr: str,
        lower_tick: int,
        upper_tick: int,
    ) -> str:
        """Collect accumulated fees"""
        msg = {
            "collect": {
                "lower_tick": lower_tick,
                "upper_tick": upper_tick,
                "recipient": None,
            }
        }

        result = self.client.execute(
            self.wallet,
            pool_addr,
            json.dumps(msg).encode(),
        )
        return result.tx_hash

    def get_pool_state(self, pool_addr: str) -> dict:
        """Query pool state"""
        return self.client.query_contract_state(
            pool_addr,
            {"pool_state": {"query": {"state": {}}}},
        )

    def get_position(
        self,
        pool_addr: str,
        owner: str,
        lower_tick: int,
        upper_tick: int,
    ) -> dict:
        """Query position info"""
        return self.client.query_contract_state(
            pool_addr,
            {
                "position": {
                    "query": {
                        "position": {
                            "owner": owner,
                            "lower_tick": lower_tick,
                            "upper_tick": upper_tick,
                        }
                    }
                }
            },
        )

    def get_twap(self, pool_addr: str, seconds_ago: int = 3600) -> dict:
        """Query TWAP"""
        return self.client.query_contract_state(
            pool_addr,
            {"oracle": {"query": {"twap": {"seconds_ago": seconds_ago}}}},
        )

    def _get_deadline(self) -> int:
        """Get deadline timestamp (10 minutes from now)"""
        import time
        return int(time.time()) + 600


# Example usage
if __name__ == "__main__":
    import asyncio

    RPC = "https://rpc.msg-chain-1.msgchain.org"
    FACTORY = "msg1factoryaddress..."

    wallet = LocalWallet.from_mnemonic("your mnemonic here")
    clamm = ClammClient(RPC, wallet, FACTORY)

    # Create pool
    tx = clamm.create_pool("umsg", "uusdc", 3000, Decimal("1.0"))
    print(f"Create pool tx: {tx}")

    # Add liquidity
    tx = clamm.add_liquidity(
        "msg1pooladdress...",
        -60000,
        60000,
        int(Decimal("1000") * Decimal(10) ** 18),
        int(Decimal("1000") * Decimal(10) ** 18),
    )
    print(f"Add liquidity tx: {tx}")

    # Swap
    tx = clamm.swap(
        "msg1pooladdress...",
        int(Decimal("100") * Decimal(10) ** 18),
        zero_for_one=True,
    )
    print(f"Swap tx: {tx}")

    # Query TWAP
    twap = clamm.get_twap("msg1pooladdress...", 3600)
    print(f"TWAP (1h): {twap}")

9.6 完整集成测试

// scripts/integration.test.ts
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate';
import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing';
import { GasPrice } from '@cosmjs/stargate';
import Decimal from 'decimal.js';

describe('CLAMM V3 Integration', () => {
  let client: SigningCosmWasmClient;
  let wallet: DirectSecp256k1HdWallet;
  let userAddr: string;
  let factoryAddr: string;
  let poolAddr: string;

  const GAS_PRICE = GasPrice.fromString('1000000000attoMSG');

  beforeAll(async () => {
    wallet = await DirectSecp256k1HdWallet.fromMnemonic(
      'test test test test test test test test test test test test',
      { prefix: 'msg' }
    );
    [userAddr] = await wallet.getAccounts();
    client = await SigningCosmWasmClient.connectWithSigner(
      'https://rpc.msg-chain-1.msgchain.org',
      wallet,
      { gasPrice: GAS_PRICE }
    );
  });

  test('should deploy factory contract', async () => {
    const wasm = require('fs').readFileSync('./artifacts/factory_v3.wasm');
    const result = await client.upload(userAddr, wasm, 'auto');
    expect(result.codeId).toBeGreaterThan(0);
  });

  test('should create pool with initial price', async () => {
    const initialPrice = new Decimal(1.0);
    const sqrtPrice = initialPrice.sqrt().mul(new Decimal(2).pow(96));

    const msg = {
      create_pool: {
        token0: 'umsg',
        token1: 'uusdc',
        fee_tier: 'Default',
        sqrt_price_x96: sqrtPrice.toFixed(0),
        pool_code_id: 1,
      },
    };

    const result = await client.execute(userAddr, factoryAddr, msg, 'auto');
    expect(result.transactionHash).toBeDefined();

    // Extract pool address
    poolAddr = result.events
      .flatMap((e) => e.attributes)
      .find((a) => a.key === 'contract_address')?.value || '';
    expect(poolAddr).toBeTruthy();
  });

  test('should query pool state', async () => {
    const state = await client.queryContractSmart(poolAddr, {
      pool_state: { query: { state: {} } },
    });
    expect(state.token0).toBe('umsg');
    expect(state.token1).toBe('uusdc');
    expect(Number(state.fee_rate)).toBe(3000);
  });

  test('should add liquidity', async () => {
    const msg = {
      mint: {
        lower_tick: -60000,
        upper_tick: 60000,
        amount0_desired: '1000000000000000000',
        amount1_desired: '1000000000000000000',
        amount0_min: '0',
        amount1_min: '0',
        recipient: userAddr,
        deadline: Math.floor(Date.now() / 1000) + 600,
      },
    };

    const result = await client.execute(
      userAddr, poolAddr, msg, 'auto',
      undefined,
      [{ denom: 'umsg', amount: '1000000000000000000' }]
    );
    expect(result.transactionHash).toBeDefined();
  });

  test('should query position', async () => {
    const pos = await client.queryContractSmart(poolAddr, {
      position: {
        query: {
          position: {
            owner: userAddr,
            lower_tick: -60000,
            upper_tick: 60000,
          },
        },
      },
    });
    expect(Number(pos.liquidity)).toBeGreaterThan(0);
  });

  test('should execute swap', async () => {
    const msg = {
      swap: {
        exact_in: true,
        amount: '100000000000000000',
        zero_for_one: true,
        sqrt_price_limit: '4295128739',
        amount_limit: '0',
        deadline: Math.floor(Date.now() / 1000) + 600,
      },
    };

    const result = await client.execute(
      userAddr, poolAddr, msg, 'auto',
      undefined,
      [{ denom: 'umsg', amount: '100000000000000000' }]
    );
    expect(result.transactionHash).toBeDefined();
  });

  test('should query TWAP after swap', async () => {
    const twap = await client.queryContractSmart(poolAddr, {
      oracle: { query: { twap: { seconds_ago: 3600 } } },
    });
    expect(twap.tick).toBeDefined();
    expect(twap.price).toBeDefined();
  });

  test('should remove liquidity', async () => {
    const pos = await client.queryContractSmart(poolAddr, {
      position: {
        query: {
          position: {
            owner: userAddr,
            lower_tick: -60000,
            upper_tick: 60000,
          },
        },
      },
    });

    const msg = {
      burn: {
        lower_tick: -60000,
        upper_tick: 60000,
        liquidity: pos.liquidity,
        amount0_min: '0',
        amount1_min: '0',
        deadline: Math.floor(Date.now() / 1000) + 600,
      },
    };

    const result = await client.execute(userAddr, poolAddr, msg, 'auto');
    expect(result.transactionHash).toBeDefined();
  });
});

10. 附录

10.1 合约大小

合约 WASM 大小 优化后大小
factory_v3 ~180 KB ~90 KB
pool_v3 ~280 KB ~140 KB
nft_descriptor ~160 KB ~80 KB
router_v3 ~200 KB ~100 KB

10.2 Gas 成本估算

操作 Gas 估算 MSG 成本(低/中/高)
创建 Pool 2,000,000 - 3,000,000 20,000 / 50,000 / 80,000 umsg
添加流动性 800,000 - 2,000,000 8,000 / 20,000 / 32,000 umsg
移除流动性 600,000 - 1,500,000 6,000 / 15,000 / 24,000 umsg
交换(无跨 tick) 300,000 - 500,000 3,000 / 7,500 / 12,000 umsg
交换(跨 1 tick) 500,000 - 700,000 5,000 / 12,500 / 20,000 umsg
交换(跨 5 tick) 1,000,000 - 1,500,000 10,000 / 25,000 / 40,000 umsg
提取手续费 300,000 - 500,000 3,000 / 7,500 / 12,000 umsg
Oracle 查询 100,000 - 200,000 1,000 / 2,500 / 4,000 umsg

10.3 部署指南

#!/bin/bash
# scripts/deploy-v3.sh
# MSG Chain CLAMM V3 部署脚本

set -e

CHAIN_ID="msg-chain-1"
RPC_URL="https://rpc.msg-chain-1.msgchain.org"
WALLET="deployer"
ARTIFACTS="./artifacts"

echo "=== MSG Chain CLAMM V3 Deployment ==="
echo "Chain: $CHAIN_ID"
echo "RPC: $RPC_URL"

# 1. Upload contracts
echo "Uploading factory_v3..."
FACTORY_CODE=$(msgd tx wasm store "$ARTIFACTS/factory_v3.wasm" \
  --from $WALLET --chain-id $CHAIN_ID --node $RPC_URL \
  --gas-prices 1000000000attoMSG --gas auto --gas-adjustment 1.3 -y --output json | \
  jq -r '.logs[0].events[] | select(.type == "store_code") | .attributes[] | select(.key == "code_id") | .value')
echo "Factory code ID: $FACTORY_CODE"

echo "Uploading pool_v3..."
POOL_CODE=$(msgd tx wasm store "$ARTIFACTS/pool_v3.wasm" \
  --from $WALLET --chain-id $CHAIN_ID --node $RPC_URL \
  --gas-prices 1000000000attoMSG --gas auto --gas-adjustment 1.3 -y --output json | \
  jq -r '.logs[0].events[] | select(.type == "store_code") | .attributes[] | select(.key == "code_id") | .value')
echo "Pool code ID: $POOL_CODE"

echo "Uploading nft_descriptor..."
NFT_CODE=$(msgd tx wasm store "$ARTIFACTS/nft_descriptor.wasm" \
  --from $WALLET --chain-id $CHAIN_ID --node $RPC_URL \
  --gas-prices 1000000000attoMSG --gas auto --gas-adjustment 1.3 -y --output json | \
  jq -r '.logs[0].events[] | select(.type == "store_code") | .attributes[] | select(.key == "code_id") | .value')
echo "NFT code ID: $NFT_CODE"

echo "Uploading router_v3..."
ROUTER_CODE=$(msgd tx wasm store "$ARTIFACTS/router_v3.wasm" \
  --from $WALLET --chain-id $CHAIN_ID --node $RPC_URL \
  --gas-prices 1000000000attoMSG --gas auto --gas-adjustment 1.3 -y --output json | \
  jq -r '.logs[0].events[] | select(.type == "store_code") | .attributes[] | select(.key == "code_id") | .value')
echo "Router code ID: $ROUTER_CODE"

# 2. Instantiate Factory
echo "Instantiating Factory..."
FACTORY_ADDR=$(msgd tx wasm instantiate $FACTORY_CODE '{}' \
  --from $WALLET --chain-id $CHAIN_ID --node $RPC_URL \
  --label "msg-clamm-factory-v1" --gas-prices 1000000000attoMSG --gas auto --gas-adjustment 1.3 \
  --admin $(msgd keys show $WALLET -a) -y --output json | \
  jq -r '.logs[0].events[] | select(.type == "instantiate") | .attributes[] | select(.key == "_contract_address") | .value')
echo "Factory: $FACTORY_ADDR"

# 3. Create a pool
echo "Creating Pool (umsg/uusdc, 0.30%)..."
INIT_PRICE="79228162514264337593543950336"  # sqrt(1.0) * 2^96

CREATE_MSG=$(cat <<EOF
{
  "create_pool": {
    "token0": "umsg",
    "token1": "uusdc",
    "fee_tier": "Default",
    "sqrt_price_x96": "$INIT_PRICE",
    "pool_code_id": $POOL_CODE
  }
}
EOF
)

TX_RESULT=$(msgd tx wasm execute $FACTORY_ADDR "$CREATE_MSG" \
  --from $WALLET --chain-id $CHAIN_ID --node $RPC_URL \
  --gas-prices 1000000000attoMSG --gas auto --gas-adjustment 1.3 -y --output json)

# Extract pool address from reply
POOL_ADDR=$(echo $TX_RESULT | jq -r '.logs[0].events[] | select(.type == "wasm") | .attributes[] | select(.key == "contract_address") | .value')
echo "Pool: $POOL_ADDR"

echo "=== Deployment Complete ==="
echo "Factory: $FACTORY_ADDR"
echo "Pool ($POOL_CODE): $POOL_ADDR"
echo "NFT Descriptor: $NFT_CONTRACT"
echo "Router: $ROUTER_ADDR"

# Save addresses
cat > deployed-v3.json <<EOF
{
  "chain_id": "$CHAIN_ID",
  "factory_addr": "$FACTORY_ADDR",
  "pool_code_id": $POOL_CODE,
  "nft_code_id": $NFT_CODE,
  "router_code_id": $ROUTER_CODE,
  "pools": [
    {
      "token0": "umsg",
      "token1": "uusdc",
      "fee_rate": 3000,
      "address": "$POOL_ADDR"
    }
  ]
}
EOF

10.4 快速启动(Docker)

# docker-compose.yml
version: '3.8'

services:
  msg-chain:
    image: msgchain/msgd:latest
    ports:
      - "26657:26657"  # RPC
      - "1317:1317"    # REST/LCD
    environment:
      - MONIKER=clamm-local
      - CHAIN_ID=msg-chain-1
    volumes:
      - msg-data:/root/.msgd

  clamm-deployer:
    image: node:20-alpine
    depends_on:
      - msg-chain
    working_dir: /app
    volumes:
      - ./artifacts:/app/artifacts
      - ./scripts:/app/scripts
    command: >
      sh -c "npm install @cosmjs/cosmwasm-stargate @cosmjs/proto-signing &&
             npx ts-node scripts/deploy-v3.ts"

  clamm-frontend:
    image: node:20-alpine
    depends_on:
      - msg-chain
    working_dir: /app
    ports:
      - "5173:5173"
    volumes:
      - ./frontend:/app
    command: >
      sh -c "npm install && npm run dev -- --host 0.0.0.0"

volumes:
  msg-data:

10.5 常见问题

Q: 为什么需要虚拟准备金?

A: 虚拟准备金是数学构造,使得恒定乘积公式 x * y = k 在价格区间 [pa, pb] 内成立。通过加减 L/√pb 和 L√pa,我们可以在任意价格区间内模拟恒定乘积行为。

Q: 如何选择合适的费率层级?

A: 稳定币对(如 USDC/USDT)适合 0.05% 费率(tick spacing 10),主流波动对(如 MSG/USDC)适合 0.30% 费率(tick spacing 60),高波动或长尾资产适合 1.00% 费率(tick spacing 200)。

Q: 跨 tick 时会发生什么?

A: 当价格跨越一个已初始化的 tick 时,该 tick 的流动性会从当前流动性池中增加或移除。具体地,liquidity += liquidity_net(从下往上)或 liquidity -= liquidity_net(从上往下)。

Q: 如何计算仓位的实际价值?

A: 使用 getAmount0Delta 和 getAmount1Delta 函数,根据当前价格相对于区间的位置计算:价格在区间下方时全部为 token0,上方时全部为 token1,区间内时两种代币都有。

Q: TWAP 如何工作?

A: Oracle 在每个区块中记录累积的 tick * time。TWAP 通过在时间窗口 [t0, t1] 上计算 (tickCumulative[t1] - tickCumulative[t0]) / (t1 - t0) 得到时间加权平均 tick。

10.6 参考资源


文档版本: v1.0.0
本文档基于 MSG Chain 代码库核实的技术事实。
白皮书系统: https://msgchain.org/whitepaper/
适用链: MSG Chain (msg-chain-1)
作者: MSG Chain 开发团队
许可证: MIT