返回 SC 笔记
SC Day 64

Move/Sui - DeFi模式 - 共享对象 + 流动性池 + SimpleSwap模块

### 1. 共享对象 (Shared Objects) 深入理解

2026-06-25
第三阶段:安全审计
DeFiSharedObject流动性池AMMSui

日期: 2026-06-25 方向: Move/Sui 阶段: 第三阶段:安全审计 标签: #DeFi #SharedObject #流动性池 #AMM #Sui


今日目标

  1. 深入理解 Sui 共享对象(Shared Objects)的并发访问模型
  2. 设计 Pool<A, B> 结构,实现恒定乘积 Swap 逻辑
  3. 实现完整的 SimpleSwap 模块(创建池/添加流动性/移除流动性/交换)
  4. 对比 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>> 类型
FactoryFactory 合约部署新 Pool 合约调用函数创建新 Pool 对象
并发模型顺序执行所有交易不同池的交易可并行
重入风险需要 ReentrancyGuard不存在,Move 无回调
代币安全需要 SafeERC20Balance 类型自动保证
价格操纵闪电贷可在同一交易操纵同样可能(共享对象)
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 使用 TableBag 等动态集合替代 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 没有浮点数)

参考资源

  1. Sui DeFi 开发指南
  2. DeepBook - Sui 原生 CLOB
  3. Cetus AMM on Sui
  4. Turbos Finance
  5. Uniswap V2 白皮书(对比参考)
  6. Sui 共享对象文档
  7. Move Book - Generics