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. 概述与架构设计
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 设计决策
- Tick 间距:支持 1、10、60、200 等 tick spacing,对应不同费率层级
- Token 精度:所有内部计算使用 Uint256(通过
uintcrate),适应 18 位小数 - Gas 优化:Tick 信息存储在
cw-storage-plus的 Map 中,使用复合 key(pool_id, tick_index) - Position NFT:使用 CW721 标准表示仓位所有权
- 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²
其中:
x= token0 的实际余额y= token1 的实际余额L= 流动性(liquidity)pa= 价格区间下限pb= 价格区间上限
当当前价格 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 的数学性质:
p(i) * p(-i) = 1(对称性)p(i + 1) / p(i) = 1.0001(等比数列)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)
实际计算步骤:
- 根据
dx计算新的√p_next - 计算实际输出
dy = L * (√p_next - √p_current) - 如果
√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:
- 在 tick
i以下(价格更低,即 token0 更贵):feeGrowthBelow = feeGrowthOutside - 在 tick
i及以上(价格更高):feeGrowthAbove = feeGrowthGlobal - feeGrowthOutside
仓位内部 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,
¶ms.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, ¶ms.recipient, params.lower_tick, params.upper_tick),
&pos,
)?;
Uint128::zero()
} else {
PositionManager::create_position(
deps.storage,
&pool_addr,
¶ms.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"
>
↑↓
</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 参考资源
- Uniswap V3 Whitepaper: https://uniswap.org/whitepaper-v3.pdf
- Uniswap V3 Core GitHub: https://github.com/Uniswap/v3-core
- Uniswap V3 Periphery GitHub: https://github.com/Uniswap/v3-periphery
- CosmWasm Documentation: https://docs.cosmwasm.com
- MSG Chain Developer Portal: https://docs.msgchain.org
uintcrate: https://crates.io/crates/uint
文档版本: v1.0.0
本文档基于 MSG Chain 代码库核实的技术事实。
白皮书系统: https://msgchain.org/whitepaper/
适用链: MSG Chain (msg-chain-1)
作者: MSG Chain 开发团队
许可证: MIT
