SC Day 64
Move/Sui - DeFi模式 - 共享对象 + 流动性池 + SimpleSwap模块
### 1. 共享对象 (Shared Objects) 深入理解
2026-06-25
第三阶段:安全审计DeFiSharedObject流动性池AMMSui
日期: 2026-06-25 方向: Move/Sui 阶段: 第三阶段:安全审计 标签: #DeFi #SharedObject #流动性池 #AMM #Sui
今日目标
- 深入理解 Sui 共享对象(Shared Objects)的并发访问模型
- 设计 Pool<A, B> 结构,实现恒定乘积 Swap 逻辑
- 实现完整的 SimpleSwap 模块(创建池/添加流动性/移除流动性/交换)
- 对比 DeFi on Sui 与 DeFi on EVM 的架构差异
核心概念
1. 共享对象 (Shared Objects) 深入理解
在 Sui 的对象模型中,共享对象是 DeFi 的基石。理解共享对象的特性对设计 DeFi 协议至关重要:
Sui 对象类型与 DeFi 场景:
Owned Objects (单所有者对象):
├── 特点: 只有 owner 可以使用,无需共识
├── 优点: 并行执行,低延迟
├── 缺点: 其他人无法直接交互
├── DeFi 用例: 用户的 LP Token, 个人的 Coin
└── 类比: 你钱包里的钱
Shared Objects (共享对象):
├── 特点: 任何人都可以读写,需要共识排序
├── 优点: 多用户可并发交互
├── 缺点: 需要排序,吞吐量较低
├── DeFi 用例: 流动性池, 订单簿, 全局配置
└── 类比: 银行的ATM机(所有人可用,需要排队)
Immutable Objects (不可变对象):
├── 特点: 创建后不可修改,任何人可读
├── 优点: 完全并行,无共识开销
├── DeFi 用例: 合约Package, 协议元数据
└── 类比: 公告板上的公告
2. 共享对象的并发处理
EVM 模型 (全局状态):
┌─────────────────────────────────┐
│ 全局状态树 │
│ ┌─────────────────────────┐ │
│ │ Pool.reserve0 = 1000 │ │ 所有交易顺序执行
│ │ Pool.reserve1 = 2000 │ │ 每笔交易都需要
│ │ User1.balance = 100 │ │ 访问全局状态
│ │ User2.balance = 200 │ │
│ └─────────────────────────┘ │
└─────────────────────────────────┘
Sui 模型 (对象级并发):
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Pool │ │ User1的 │ │ User2的 │
│ (Shared) │ │ Coin │ │ Coin │
│ │ │ (Owned) │ │ (Owned) │
└──────────┘ └──────────┘ └──────────┘
↑ ↑ ↑
│ │ │
需要共识 并行执行 并行执行
优势: 不涉及 Pool 的交易可以完全并行
只有访问同一个 Shared Object 的交易需要排序
3. 恒定乘积公式 (Constant Product)
AMM 核心公式: x * y = k
初始状态: Pool 有 1000 ETH 和 2,000,000 USDC
k = 1000 * 2,000,000 = 2,000,000,000
用户想用 10 ETH 买 USDC:
新的 ETH 储备: 1000 + 10 = 1010
新的 USDC 储备: k / 1010 = 2,000,000,000 / 1010 ≈ 1,980,198
用户获得: 2,000,000 - 1,980,198 = 19,802 USDC
实际价格: 19,802 / 10 = 1,980.2 USDC/ETH
(低于初始价格 2,000 USDC/ETH,这就是滑点)
手续费处理:
在 Swap 前从输入中扣除手续费(如 0.3%)
实际用于计算的输入量: 10 * (1 - 0.003) = 9.97 ETH
手续费累积在池中,增加了 LP 的收益
4. LP Token 机制
LP Token 代表流动性份额:
首次添加流动性:
LP_tokens = sqrt(amount_a * amount_b) - MINIMUM_LIQUIDITY
MINIMUM_LIQUIDITY (如 1000) 永久锁定,防止操纵
后续添加:
LP_tokens = min(
amount_a * total_supply / reserve_a,
amount_b * total_supply / reserve_b
)
移除流动性:
amount_a = LP_tokens * reserve_a / total_supply
amount_b = LP_tokens * reserve_b / total_supply
代码实战
实战 1: SimpleSwap 完整模块
module defi::simple_swap {
use sui::coin::{Self, Coin};
use sui::balance::{Self, Balance, Supply};
use sui::math;
use sui::event;
// === 错误码 ===
const EZeroAmount: u64 = 0;
const EInsufficientLiquidity: u64 = 1;
const ESlippageExceeded: u64 = 2;
const EInvalidFeeRate: u64 = 3;
const EPoolNotEmpty: u64 = 4;
const EZeroOutput: u64 = 5;
const EInsufficientInputAmount: u64 = 6;
// === 常量 ===
const MINIMUM_LIQUIDITY: u64 = 1000;
const FEE_DENOMINATOR: u64 = 10000;
const DEFAULT_FEE_BPS: u64 = 30; // 0.3%
// === LP Token 类型 ===
/// LP Token 的幻影类型(Phantom Type)
/// 每个池有唯一的 LP Token 类型
public struct LP<phantom A, phantom B> has drop {}
// === 流动性池 ===
/// Pool 是共享对象,任何人可以交互
public struct Pool<phantom A, phantom B> has key {
id: UID,
/// A 代币储备
reserve_a: Balance<A>,
/// B 代币储备
reserve_b: Balance<B>,
/// LP Token 供应量管理
lp_supply: Supply<LP<A, B>>,
/// 手续费率(基点,30 = 0.3%)
fee_bps: u64,
/// 累积的协议手续费
fee_a: Balance<A>,
fee_b: Balance<B>,
}
// === 事件 ===
public struct PoolCreated<phantom A, phantom B> has copy, drop {
pool_id: ID,
initial_a: u64,
initial_b: u64,
lp_minted: u64,
}
public struct LiquidityAdded<phantom A, phantom B> has copy, drop {
pool_id: ID,
amount_a: u64,
amount_b: u64,
lp_minted: u64,
}
public struct LiquidityRemoved<phantom A, phantom B> has copy, drop {
pool_id: ID,
amount_a: u64,
amount_b: u64,
lp_burned: u64,
}
public struct Swapped<phantom A, phantom B> has copy, drop {
pool_id: ID,
a_to_b: bool,
amount_in: u64,
amount_out: u64,
}
// === 创建池 ===
/// 创建新的流动性池
/// 调用者提供初始流动性,获得 LP Token
public fun create_pool<A, B>(
coin_a: Coin<A>,
coin_b: Coin<B>,
ctx: &mut TxContext,
): Coin<LP<A, B>> {
let amount_a = coin::value(&coin_a);
let amount_b = coin::value(&coin_b);
assert!(amount_a > 0 && amount_b > 0, EZeroAmount);
// 计算初始 LP Token 数量
// LP = sqrt(amount_a * amount_b) - MINIMUM_LIQUIDITY
let lp_amount = math::sqrt(
(amount_a as u128) * (amount_b as u128)
) as u64;
assert!(lp_amount > MINIMUM_LIQUIDITY, EInsufficientLiquidity);
// 创建 LP Token Supply
let mut lp_supply = balance::create_supply(LP<A, B> {});
// 铸造 MINIMUM_LIQUIDITY 并永久锁定(发送到 0x0)
let locked_lp = balance::increase_supply(&mut lp_supply, MINIMUM_LIQUIDITY);
// 在实际实现中应该转给一个不可访问的地址
balance::destroy_for_testing(locked_lp); // 测试用
// 铸造用户的 LP Token
let user_lp_amount = lp_amount - MINIMUM_LIQUIDITY;
let user_lp = balance::increase_supply(&mut lp_supply, user_lp_amount);
let pool_id_val = object::new(ctx);
let pool_id = object::uid_to_inner(&pool_id_val);
// 创建 Pool(共享对象)
let pool = Pool<A, B> {
id: pool_id_val,
reserve_a: coin::into_balance(coin_a),
reserve_b: coin::into_balance(coin_b),
lp_supply,
fee_bps: DEFAULT_FEE_BPS,
fee_a: balance::zero(),
fee_b: balance::zero(),
};
// 共享 Pool 对象,使所有人都能交互
transfer::share_object(pool);
event::emit(PoolCreated<A, B> {
pool_id,
initial_a: amount_a,
initial_b: amount_b,
lp_minted: user_lp_amount,
});
coin::from_balance(user_lp, ctx)
}
// === 添加流动性 ===
/// 添加流动性到已有池
/// 按照当前比例添加,多余的部分返还
public fun add_liquidity<A, B>(
pool: &mut Pool<A, B>,
mut coin_a: Coin<A>,
mut coin_b: Coin<B>,
ctx: &mut TxContext,
): (Coin<LP<A, B>>, Coin<A>, Coin<B>) {
let amount_a = coin::value(&coin_a);
let amount_b = coin::value(&coin_b);
assert!(amount_a > 0 && amount_b > 0, EZeroAmount);
let reserve_a = balance::value(&pool.reserve_a);
let reserve_b = balance::value(&pool.reserve_b);
let lp_total = balance::supply_value(&pool.lp_supply);
// 计算最优添加量(按当前比例)
let optimal_b = (amount_a as u128) * (reserve_b as u128) / (reserve_a as u128);
let (actual_a, actual_b) = if ((optimal_b as u64) <= amount_b) {
(amount_a, optimal_b as u64)
} else {
let optimal_a = (amount_b as u128) * (reserve_a as u128) / (reserve_b as u128);
(optimal_a as u64, amount_b)
};
// 计算 LP Token 数量
let lp_amount = math::min(
(actual_a as u128) * (lp_total as u128) / (reserve_a as u128),
(actual_b as u128) * (lp_total as u128) / (reserve_b as u128),
) as u64;
assert!(lp_amount > 0, EZeroOutput);
// 从 coin 中取出实际需要的量
let balance_a = coin::into_balance(coin::split(&mut coin_a, actual_a, ctx));
let balance_b = coin::into_balance(coin::split(&mut coin_b, actual_b, ctx));
// 添加到储备
balance::join(&mut pool.reserve_a, balance_a);
balance::join(&mut pool.reserve_b, balance_b);
// 铸造 LP Token
let lp_balance = balance::increase_supply(&mut pool.lp_supply, lp_amount);
event::emit(LiquidityAdded<A, B> {
pool_id: object::uid_to_inner(&pool.id),
amount_a: actual_a,
amount_b: actual_b,
lp_minted: lp_amount,
});
// 返回 LP Token 和多余的 coin
(
coin::from_balance(lp_balance, ctx),
coin_a, // 多余的 A 返还
coin_b, // 多余的 B 返还
)
}
// === 移除流动性 ===
/// 按比例移除流动性
public fun remove_liquidity<A, B>(
pool: &mut Pool<A, B>,
lp_coin: Coin<LP<A, B>>,
ctx: &mut TxContext,
): (Coin<A>, Coin<B>) {
let lp_amount = coin::value(&lp_coin);
assert!(lp_amount > 0, EZeroAmount);
let reserve_a = balance::value(&pool.reserve_a);
let reserve_b = balance::value(&pool.reserve_b);
let lp_total = balance::supply_value(&pool.lp_supply);
// 按比例计算可取回的代币量
let amount_a = (lp_amount as u128) * (reserve_a as u128) / (lp_total as u128);
let amount_b = (lp_amount as u128) * (reserve_b as u128) / (lp_total as u128);
let amount_a = amount_a as u64;
let amount_b = amount_b as u64;
assert!(amount_a > 0 && amount_b > 0, EInsufficientLiquidity);
// 销毁 LP Token
balance::decrease_supply(
&mut pool.lp_supply,
coin::into_balance(lp_coin),
);
// 从储备中取出代币
let coin_a = coin::from_balance(
balance::split(&mut pool.reserve_a, amount_a),
ctx,
);
let coin_b = coin::from_balance(
balance::split(&mut pool.reserve_b, amount_b),
ctx,
);
event::emit(LiquidityRemoved<A, B> {
pool_id: object::uid_to_inner(&pool.id),
amount_a,
amount_b,
lp_burned: lp_amount,
});
(coin_a, coin_b)
}
// === Swap 操作 ===
/// A → B 交换
public fun swap_a_to_b<A, B>(
pool: &mut Pool<A, B>,
coin_in: Coin<A>,
min_out: u64, // 最小输出量(滑点保护)
ctx: &mut TxContext,
): Coin<B> {
let amount_in = coin::value(&coin_in);
assert!(amount_in > 0, EZeroAmount);
let reserve_a = balance::value(&pool.reserve_a);
let reserve_b = balance::value(&pool.reserve_b);
// 计算输出量(含手续费)
let amount_out = calculate_output(
amount_in,
reserve_a,
reserve_b,
pool.fee_bps,
);
assert!(amount_out >= min_out, ESlippageExceeded);
assert!(amount_out > 0, EZeroOutput);
// 添加输入到储备
balance::join(&mut pool.reserve_a, coin::into_balance(coin_in));
// 从储备取出输出
let coin_out = coin::from_balance(
balance::split(&mut pool.reserve_b, amount_out),
ctx,
);
event::emit(Swapped<A, B> {
pool_id: object::uid_to_inner(&pool.id),
a_to_b: true,
amount_in,
amount_out,
});
coin_out
}
/// B → A 交换
public fun swap_b_to_a<A, B>(
pool: &mut Pool<A, B>,
coin_in: Coin<B>,
min_out: u64,
ctx: &mut TxContext,
): Coin<A> {
let amount_in = coin::value(&coin_in);
assert!(amount_in > 0, EZeroAmount);
let reserve_a = balance::value(&pool.reserve_a);
let reserve_b = balance::value(&pool.reserve_b);
let amount_out = calculate_output(
amount_in,
reserve_b,
reserve_a,
pool.fee_bps,
);
assert!(amount_out >= min_out, ESlippageExceeded);
assert!(amount_out > 0, EZeroOutput);
balance::join(&mut pool.reserve_b, coin::into_balance(coin_in));
let coin_out = coin::from_balance(
balance::split(&mut pool.reserve_a, amount_out),
ctx,
);
event::emit(Swapped<A, B> {
pool_id: object::uid_to_inner(&pool.id),
a_to_b: false,
amount_in,
amount_out,
});
coin_out
}
// === 内部函数 ===
/// 恒定乘积公式计算输出量
/// output = (input * fee_factor * reserve_out) / (reserve_in * FEE_DENOMINATOR + input * fee_factor)
fun calculate_output(
amount_in: u64,
reserve_in: u64,
reserve_out: u64,
fee_bps: u64,
): u64 {
let fee_factor = FEE_DENOMINATOR - fee_bps; // 9970 for 0.3%
let amount_in_with_fee = (amount_in as u128) * (fee_factor as u128);
let numerator = amount_in_with_fee * (reserve_out as u128);
let denominator = (reserve_in as u128) * (FEE_DENOMINATOR as u128) + amount_in_with_fee;
(numerator / denominator) as u64
}
// === 查询函数 ===
/// 获取池储备
public fun get_reserves<A, B>(pool: &Pool<A, B>): (u64, u64) {
(
balance::value(&pool.reserve_a),
balance::value(&pool.reserve_b),
)
}
/// 获取 LP 总供应量
public fun get_lp_supply<A, B>(pool: &Pool<A, B>): u64 {
balance::supply_value(&pool.lp_supply)
}
/// 预览 Swap 输出量(不执行交易)
public fun quote_swap_a_to_b<A, B>(
pool: &Pool<A, B>,
amount_in: u64,
): u64 {
calculate_output(
amount_in,
balance::value(&pool.reserve_a),
balance::value(&pool.reserve_b),
pool.fee_bps,
)
}
public fun quote_swap_b_to_a<A, B>(
pool: &Pool<A, B>,
amount_in: u64,
): u64 {
calculate_output(
amount_in,
balance::value(&pool.reserve_b),
balance::value(&pool.reserve_a),
pool.fee_bps,
)
}
}
实战 2: 单元测试
#[test_only]
module defi::simple_swap_tests {
use sui::test_scenario;
use sui::coin;
use sui::sui::SUI;
use defi::simple_swap::{Self, Pool, LP};
// 测试用代币类型
public struct USDC has drop {}
#[test]
fun test_create_pool() {
let mut scenario = test_scenario::begin(@alice);
// Alice 创建池
test_scenario::next_tx(&mut scenario, @alice);
{
let ctx = test_scenario::ctx(&mut scenario);
let coin_a = coin::mint_for_testing<SUI>(1_000_000, ctx);
let coin_b = coin::mint_for_testing<USDC>(2_000_000, ctx);
let lp_coin = simple_swap::create_pool(coin_a, coin_b, ctx);
// LP Token 应该大于 0
assert!(coin::value(&lp_coin) > 0, 0);
// 清理
coin::burn_for_testing(lp_coin);
};
// 验证 Pool 被创建为共享对象
test_scenario::next_tx(&mut scenario, @alice);
{
assert!(test_scenario::has_most_recent_shared<Pool<SUI, USDC>>(), 1);
};
test_scenario::end(scenario);
}
#[test]
fun test_swap() {
let mut scenario = test_scenario::begin(@alice);
// 创建池: 1000 SUI + 2000 USDC
test_scenario::next_tx(&mut scenario, @alice);
{
let ctx = test_scenario::ctx(&mut scenario);
let coin_a = coin::mint_for_testing<SUI>(1_000_000_000, ctx);
let coin_b = coin::mint_for_testing<USDC>(2_000_000_000, ctx);
let lp = simple_swap::create_pool(coin_a, coin_b, ctx);
coin::burn_for_testing(lp);
};
// Bob 用 10 SUI 换 USDC
test_scenario::next_tx(&mut scenario, @bob);
{
let mut pool = test_scenario::take_shared<Pool<SUI, USDC>>(&scenario);
let ctx = test_scenario::ctx(&mut scenario);
let swap_coin = coin::mint_for_testing<SUI>(10_000_000, ctx);
// 预览输出
let expected_out = simple_swap::quote_swap_a_to_b(&pool, 10_000_000);
let usdc_out = simple_swap::swap_a_to_b(
&mut pool,
swap_coin,
0, // 测试中不设最小输出
ctx,
);
// 验证输出大于 0
assert!(coin::value(&usdc_out) > 0, 2);
assert!(coin::value(&usdc_out) == expected_out, 3);
coin::burn_for_testing(usdc_out);
test_scenario::return_shared(pool);
};
test_scenario::end(scenario);
}
#[test]
#[expected_failure(abort_code = simple_swap::ESlippageExceeded)]
fun test_slippage_protection() {
let mut scenario = test_scenario::begin(@alice);
test_scenario::next_tx(&mut scenario, @alice);
{
let ctx = test_scenario::ctx(&mut scenario);
let coin_a = coin::mint_for_testing<SUI>(1_000_000, ctx);
let coin_b = coin::mint_for_testing<USDC>(2_000_000, ctx);
let lp = simple_swap::create_pool(coin_a, coin_b, ctx);
coin::burn_for_testing(lp);
};
test_scenario::next_tx(&mut scenario, @bob);
{
let mut pool = test_scenario::take_shared<Pool<SUI, USDC>>(&scenario);
let ctx = test_scenario::ctx(&mut scenario);
let swap_coin = coin::mint_for_testing<SUI>(10_000, ctx);
// 设置不可能达到的最小输出 → 应该失败
let usdc_out = simple_swap::swap_a_to_b(
&mut pool,
swap_coin,
999_999_999, // 不可能的最小输出
ctx,
);
coin::burn_for_testing(usdc_out);
test_scenario::return_shared(pool);
};
test_scenario::end(scenario);
}
}
关键要点总结
DeFi on Sui vs DeFi on EVM 架构差异
| 维度 | EVM (Uniswap V2) | Sui (SimpleSwap) |
|---|---|---|
| 池的表示 | 每个池一个合约 | 每个池一个共享对象 |
| 代币储备 | ERC20 余额 (balanceOf) | Balance<T> 内嵌在 Pool 中 |
| LP Token | 独立 ERC20 合约 | Coin<LP<A,B>> 类型 |
| Factory | Factory 合约部署新 Pool 合约 | 调用函数创建新 Pool 对象 |
| 并发模型 | 顺序执行所有交易 | 不同池的交易可并行 |
| 重入风险 | 需要 ReentrancyGuard | 不存在,Move 无回调 |
| 代币安全 | 需要 SafeERC20 | Balance 类型自动保证 |
| 价格操纵 | 闪电贷可在同一交易操纵 | 同样可能(共享对象) |
| Gas 模型 | 按计算复杂度收费 | 按对象访问+计算收费 |
| 升级 | Proxy 模式,复杂 | Package 升级,更结构化 |
Sui DeFi 的独特优势
1. 对象级并行
- 不同池的交易完全并行
- 同一池的交易需要排序,但比 EVM 全局排序效率高
2. 资源安全
- Balance<T> 不能凭空创建或销毁
- LP Token 的铸造/销毁由 Supply 对象严格控制
- 不存在 ERC20 的 approve/transferFrom 攻击面
3. 可组合性
- Coin 对象可以在不同 Pool 间自由流动
- 无需 approve 步骤,直接传入 Coin 即可
4. 类型安全
- Pool<A, B> 和 Pool<B, A> 是不同类型
- LP<A, B> 不能用于 Pool<C, D>
- 编译时就能发现类型错误
Sui DeFi 的注意事项
1. 共享对象瓶颈
- 热门池的共享对象可能成为瓶颈
- 需要优化交易排序策略
2. 闪电贷
- Sui 上闪电贷通过 Hot Potato 模式实现
- 仍然可以在同一交易中操纵价格
3. MEV
- Sui 的交易排序由验证者控制
- MEV 问题仍然存在,只是形式不同
4. 精度问题
- Move 只有整数,需要仔细处理精度
- 使用 u128 进行中间计算避免溢出
常见误区
误区 1: "Sui 上不需要担心闪电贷攻击"
纠正: Sui 上通过 Hot Potato 模式可以实现闪电贷。攻击者同样可以在单个 Programmable Transaction Block(PTB)中组合多个操作来操纵价格。DeFi 协议仍需使用 TWAP 等机制来防止价格操纵。
误区 2: "共享对象不能并行,所以 Sui DeFi 性能不行"
纠正: 只有访问同一个共享对象的交易需要排序。不同的流动性池是不同的共享对象,它们之间的交易完全可以并行。此外,Sui 还在开发更细粒度的并发控制(如 Mysticeti 共识)。
误区 3: "Move 没有 mapping,所以不能做复杂 DeFi"
纠正: Move 使用 Table 和 Bag 等动态集合替代 mapping。此外,Sui 的对象模型本身就是一种"分布式映射"——每个用户的资产是独立对象,不需要在合约中维护映射。
面试关联
Q1: 在 Sui 上设计 AMM,与 Uniswap V2 有哪些关键架构差异?
回答要点:
最大的差异是状态模型。Uniswap V2 每个池是一个独立合约,代币储备通过 ERC20 balanceOf 查询。Sui 上池是一个共享对象,Balance<T> 直接嵌入在 Pool 结构体中。
第二个差异是安全模型。EVM 上需要 approve → transferFrom 两步操作,存在授权攻击面。Sui 上用户直接传入 Coin 对象,没有 approve 概念。
第三个差异是并发模型。EVM 所有交易顺序执行,Sui 不同池的交易可以并行处理,这对 DEX 聚合器特别有利。
Q2: Sui 的 DeFi 面临哪些特有的安全挑战?
回答思路:
- 共享对象的竞态条件需要仔细设计
- Hot Potato 闪电贷同样能被滥用
- 对象所有权转移中的逻辑错误
- 整数精度问题(Move 没有浮点数)