diff --git a/mover/JasonRUAN/code/task5/js_swap/Move.lock b/mover/JasonRUAN/code/task5/js_swap/Move.lock new file mode 100644 index 000000000..5bff162e8 --- /dev/null +++ b/mover/JasonRUAN/code/task5/js_swap/Move.lock @@ -0,0 +1,44 @@ +# @generated by Move, please check-in and do not edit manually. + +[move] +version = 3 +manifest_digest = "D40BE8607C30E07AC5B66E8961834715A1F79B63116A1993C7C3C57185C4F610" +deps_digest = "060AD7E57DFB13104F21BE5F5C3759D03F0553FC3229247D9A7A6B45F50D03A3" +dependencies = [ + { id = "Sui", name = "Sui" }, + { id = "js_coin", name = "js_coin" }, + { id = "js_faucet_coin", name = "js_faucet_coin" }, +] + +[[move.package]] +id = "MoveStdlib" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/testnet", subdir = "crates/sui-framework/packages/move-stdlib" } + +[[move.package]] +id = "Sui" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/testnet", subdir = "crates/sui-framework/packages/sui-framework" } + +dependencies = [ + { id = "MoveStdlib", name = "MoveStdlib" }, +] + +[[move.package]] +id = "js_coin" +source = { local = "../../task2/js_coin" } + +dependencies = [ + { id = "Sui", name = "Sui" }, +] + +[[move.package]] +id = "js_faucet_coin" +source = { local = "../../task2/js_faucet_coin" } + +dependencies = [ + { id = "Sui", name = "Sui" }, +] + +[move.toolchain-version] +compiler-version = "1.39.0" +edition = "2024.beta" +flavor = "sui" diff --git a/mover/JasonRUAN/code/task5/js_swap/Move.toml b/mover/JasonRUAN/code/task5/js_swap/Move.toml new file mode 100644 index 000000000..f1f8d5c60 --- /dev/null +++ b/mover/JasonRUAN/code/task5/js_swap/Move.toml @@ -0,0 +1,39 @@ +[package] +name = "js_swap" +edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move +# license = "" # e.g., "MIT", "GPL", "Apache 2.0" +# authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] + +[dependencies] +Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" } +js_coin = { local = "../../task2/js_coin" } +js_faucet_coin = { local = "../../task2/js_faucet_coin" } + +# For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. +# Revision can be a branch, a tag, and a commit hash. +# MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } + +# For local dependencies use `local = path`. Path is relative to the package root +# Local = { local = "../path/to" } + +# To resolve a version conflict and force a specific version for dependency +# override use `override = true` +# Override = { local = "../conflicting/version", override = true } + +[addresses] +js_swap = "0x0" + +# Named addresses will be accessible in Move as `@name`. They're also exported: +# for example, `std = "0x1"` is exported by the Standard Library. +# alice = "0xA11CE" + +[dev-dependencies] +# The dev-dependencies section allows overriding dependencies for `--test` and +# `--dev` modes. You can introduce test-only dependencies here. +# Local = { local = "../path/to/dev-build" } + +[dev-addresses] +# The dev-addresses section allows overwriting named addresses for the `--test` +# and `--dev` modes. +# alice = "0xB0B" + diff --git a/mover/JasonRUAN/code/task5/js_swap/sources/js_pool.move b/mover/JasonRUAN/code/task5/js_swap/sources/js_pool.move new file mode 100644 index 000000000..6334030c9 --- /dev/null +++ b/mover/JasonRUAN/code/task5/js_swap/sources/js_pool.move @@ -0,0 +1,20 @@ +module js_swap::js_pool { + use sui::coin::Coin; + use js_coin::js_coin::JS_COIN; + use js_coin::js_faucet_coin::JS_FAUCET_COIN; + + public struct JS has drop {} + + entry fun create_pool( + coin_a: Coin, + coin_b: Coin, + fee_percent: u64, + ctx: &mut TxContext + ) { + + transfer::public_transfer( + js_swap::js_swap::create_pool(JS{}, coin_a, coin_b, fee_percent, ctx), + ctx.sender() + ) + } +} \ No newline at end of file diff --git a/mover/JasonRUAN/code/task5/js_swap/sources/js_swap.move b/mover/JasonRUAN/code/task5/js_swap/sources/js_swap.move new file mode 100644 index 000000000..49cae486d --- /dev/null +++ b/mover/JasonRUAN/code/task5/js_swap/sources/js_swap.move @@ -0,0 +1,311 @@ +module js_swap::js_swap { + use sui::coin::{Self, Coin}; + use sui::balance::{Self, Supply, Balance}; + + /// 提供数量不可为0 + const EZeroAmount: u64 = 0; + + /// 池子费率值设置错误。允许范围为:[0-10000),表示费率为:0-100% + const EWrongFee: u64 = 1; + + /// 池子已空,无法交换 + const EReservesEmpty: u64 = 2; + + /// 池子已满,单边达到MAX_POOL_VALUE + const EPoolFull: u64 = 4; + + /// 费用计算基准值,表示100% + const FEE_SCALING: u128 = 10000; + + /// 池子单边最大余额 + /// U64 MAX / FEE_SCALING + const MAX_POOL_VALUE: u64 = { + 18446744073709551615 / 10000 + }; + + /// The Pool token that will be used to mark the pool share + /// of a liquidity provider. The first type parameter stands + /// for the witness type of a pool. The seconds is for the + /// coin held in the pool. + public struct LSP has drop {} + + /// The pool with exchange. + /// + /// - `fee_percent` should be in the range: [0-10000), meaning + /// that 10000 is 100% and 1 is 0.1% + public struct Pool has key { + id: UID, + coinA: Balance, + coinB: Balance, + lsp_supply: Supply>, + /// Fee Percent is denominated in basis points. + fee_percent: u64 + } + + #[allow(unused_function)] + /// Module initializer is empty - to publish a new Pool one has + /// to create a type which will mark LSPs. + fun init(_: &mut TxContext) {} + + /// Create new `Pool` for token `TokenA` and `TokenB`. + /// Each Pool holds a `Coin` and a `Coin`. + /// Swaps are available in both directions. + /// + /// Share is calculated based on Uniswap's constant product formula: + /// liquidity = sqrt( X * Y ) + public fun create_pool( + _: P, + coinA: Coin, + coinB: Coin, + fee_percent: u64, + ctx: &mut TxContext + ): Coin> { + let token_a_amt = coinA.value(); + let token_b_amt = coinB.value(); + + assert!(token_a_amt > 0 && token_b_amt > 0, EZeroAmount); + assert!(token_a_amt < MAX_POOL_VALUE && token_b_amt < MAX_POOL_VALUE, EPoolFull); + assert!(fee_percent >= 0 && fee_percent < 10000, EWrongFee); + + // Initial share of LSP is the sqrt(a) * sqrt(b) + let share = std::u64::sqrt(token_a_amt) * std::u64::sqrt(token_b_amt); + let mut lsp_supply = balance::create_supply(LSP {}); + let lsp = balance::increase_supply(&mut lsp_supply, share); + + // 创建共享交易池对象,包含了组成池子的两种代币,其比例决定了初始价格 + // 例如:100 JSC + 200 JSFC,则说明:1 JSC = 2 JSFC + transfer::share_object(Pool { + id: object::new(ctx), + coinA: coin::into_balance(coinA), + coinB: coin::into_balance(coinB), + lsp_supply, + fee_percent + }); + + coin::from_balance(lsp, ctx) + } + + // 使用精确数量的TokenA兑换TokenB + entry fun swap_exact_a_for_b( + pool: &mut Pool, coin_a_in: Coin, ctx: &mut TxContext + ) { + transfer::public_transfer( + swap_exact_a_for_b_(pool, coin_a_in, ctx), + ctx.sender() + ) + } + + public fun swap_exact_a_for_b_( + pool: &mut Pool, coin_a_in: Coin, ctx: &mut TxContext + ): Coin { + assert!(coin_a_in.value() > 0, EZeroAmount); + + let coin_a_balance = coin::into_balance(coin_a_in); + + let (coin_a_reserve, coin_b_reserve, _) = get_amounts(pool); + + assert!(coin_a_reserve > 0 && coin_b_reserve > 0, EReservesEmpty); + + let output_amount = get_input_price( + balance::value(&coin_a_balance), + coin_a_reserve, + coin_b_reserve, + pool.fee_percent + ); + + balance::join(&mut pool.coinA, coin_a_balance); + coin::take(&mut pool.coinB, output_amount, ctx) + } + + // 使用精确数量的TokenB兑换TokenA + entry fun swap_exact_b_for_a( + pool: &mut Pool, coin_b_out: Coin, ctx: &mut TxContext + ) { + transfer::public_transfer( + swap_exact_b_for_a_(pool, coin_b_out, ctx), + tx_context::sender(ctx) + ) + } + + public fun swap_exact_b_for_a_( + pool: &mut Pool, coin_b_out: Coin, ctx: &mut TxContext + ): Coin { + assert!(coin_b_out.value() > 0, EZeroAmount); + + let coin_b_balance = coin::into_balance(coin_b_out); + let (coin_a_reserve, coin_b_reserve, _) = get_amounts(pool); + + assert!(coin_a_reserve > 0 && coin_b_reserve > 0, EReservesEmpty); + + let output_amount = get_input_price( + balance::value(&coin_b_balance), + coin_b_reserve, + coin_a_reserve, + pool.fee_percent + ); + + balance::join(&mut pool.coinB, coin_b_balance); + coin::take(&mut pool.coinA, output_amount, ctx) + } + + /// Entrypoint for the `add_liquidity` method. Sends `Coin` to + /// the transaction sender. + entry fun add_liquidity( + pool: &mut Pool, coin_a: Coin, coin_b: Coin, ctx: &mut TxContext + ) { + transfer::public_transfer( + add_liquidity_(pool, coin_a, coin_b, ctx), + tx_context::sender(ctx) + ); + } + + /// Add liquidity to the `Pool`. Sender needs to provide both + /// `Coin` and `Coin`, and in exchange he gets `Coin` - + /// liquidity provider tokens. + public fun add_liquidity_( + pool: &mut Pool, coin_a: Coin, coin_b: Coin, ctx: &mut TxContext + ): Coin> { + assert!(coin_a.value() > 0 && coin_b.value() > 0, EZeroAmount); + + let coin_a_balance = coin::into_balance(coin_a); + let coin_b_balance = coin::into_balance(coin_b); + + let (coin_a_amount, coin_b_amount, lsp_supply) = get_amounts(pool); + + let coin_a_added = balance::value(&coin_a_balance); + let coin_b_added = balance::value(&coin_b_balance); + + // 取最小值是为了维持价格稳定性和防止价格操纵 + // 用户获得的LP数量与其贡献的资产价值成正比 + // 原有LP持有者的份额没有被稀释 + // 任何时候都可以按比例赎回对应的资产 + // 新LP数量 = 用户添加的代币数量 * 当前LP总供应量 / 池子中该代币储备量 + // 新LP数量 = 用户添加的代币数量 * 每个代币代表多少LP + let amount_lp_value = std::u64::min( + (coin_a_added * lsp_supply) / coin_a_amount, + (coin_b_added * lsp_supply) / coin_b_amount + ); + + // 省略多余资产退回操作 + + let token_a_amt = balance::join(&mut pool.coinA, coin_a_balance); + let token_b_amt = balance::join(&mut pool.coinB, coin_b_balance); + + assert!(token_a_amt < MAX_POOL_VALUE, EPoolFull); + assert!(token_b_amt < MAX_POOL_VALUE, EPoolFull); + + let balance = balance::increase_supply(&mut pool.lsp_supply, amount_lp_value); + coin::from_balance(balance, ctx) + } + + /// Entrypoint for the `remove_liquidity` method. Transfers + /// withdrawn assets to the sender. + entry fun remove_liquidity( + pool: &mut Pool, + lsp: Coin>, + ctx: &mut TxContext + ) { + let (coin_a, coin_b) = remove_liquidity_(pool, lsp, ctx); + let sender = ctx.sender(); + + transfer::public_transfer(coin_a, sender); + transfer::public_transfer(coin_b, sender); + } + + /// Remove liquidity from the `Pool` by burning `Coin`. + public fun remove_liquidity_( + pool: &mut Pool, + lsp: Coin>, + ctx: &mut TxContext + ): (Coin, Coin) { + let lsp_amount = coin::value(&lsp); + + // If there's a non-empty LSP, we can + assert!(lsp_amount > 0, EZeroAmount); + + let (token_a_amt, token_b_amt, lsp_supply) = get_amounts(pool); + + // 按是有LP的比例赎回对应的资产 + let token_a_removed = (token_a_amt * lsp_amount) / lsp_supply; + let token_b_removed = (token_b_amt * lsp_amount) / lsp_supply; + + balance::decrease_supply(&mut pool.lsp_supply, coin::into_balance(lsp)); + + ( + coin::take(&mut pool.coinA, token_a_removed, ctx), + coin::take(&mut pool.coinB, token_b_removed, ctx) + ) + } + + public fun coin_a_price(pool: &Pool, to_sell: u64): u64 { + let (token_a_amt, token_b_amt, _) = get_amounts(pool); + get_input_price(to_sell, token_b_amt, token_a_amt, pool.fee_percent) + } + + public fun coin_b_price(pool: &Pool, to_sell: u64): u64 { + let (token_a_amt, token_b_amt, _) = get_amounts(pool); + get_input_price(to_sell, token_a_amt, token_b_amt, pool.fee_percent) + } + + public fun get_amounts(pool: &Pool): (u64, u64, u64) { + ( + balance::value(&pool.coinA), + balance::value(&pool.coinB), + balance::supply_value(&pool.lsp_supply) + ) + } + + /// Calculate the output amount minus the fee - 0.3% + // 举例:`100 JSC`可兑换`181 JSFC` + // + // 1. 输入参数: + // - input_amount = 100 (JSC) + // - input_reserve = 1000 (JSC) + // - output_reserve = 2000 (JSFC) + // - fee_percent = 30 (0.3%) + // - FEE_SCALING = 10000 + + // 2. 计算步骤: + // a) input_amount_with_fee = 100 * (10000 - 30) + // = 100 * 9970 + // = 997000 + + // b) numerator = 997000 * 2000 + // = 1,994,000,000 + + // c) denominator = (1000 * 10000) + 997000 + // = 10,000,000 + 997,000 + // = 10,997,000 + + // d) 最终结果 = numerator / denominator + // = 1,994,000,000 / 10,997,000 + // ≈ 181.32... + // ≈ 181 (向下取整) + public fun get_input_price( + input_amount: u64, input_reserve: u64, output_reserve: u64, fee_percent: u64 + ): u64 { + // up casts + let ( + input_amount, + input_reserve, + output_reserve, + fee_percent + ) = ( + (input_amount as u128), + (input_reserve as u128), + (output_reserve as u128), + (fee_percent as u128) + ); + + let input_amount_with_fee = input_amount * (FEE_SCALING - fee_percent); + let numerator = input_amount_with_fee * output_reserve; + let denominator = (input_reserve * FEE_SCALING) + input_amount_with_fee; + + (numerator / denominator as u64) + } + + #[test_only] + public fun init_for_testing(ctx: &mut TxContext) { + init(ctx) + } +} diff --git a/mover/JasonRUAN/code/task5/js_swap/tests/js_swap_tests.move b/mover/JasonRUAN/code/task5/js_swap/tests/js_swap_tests.move new file mode 100644 index 000000000..3be02cccc --- /dev/null +++ b/mover/JasonRUAN/code/task5/js_swap/tests/js_swap_tests.move @@ -0,0 +1,18 @@ +/* +#[test_only] +module js_swap::js_swap_tests; +// uncomment this line to import the module +// use js_swap::js_swap; + +const ENotImplemented: u64 = 0; + +#[test] +fun test_js_swap() { + // pass +} + +#[test, expected_failure(abort_code = ::js_swap::js_swap_tests::ENotImplemented)] +fun test_js_swap_fail() { + abort ENotImplemented +} +*/