diff --git a/apps/rooch_dex/Move.toml b/apps/rooch_dex/Move.toml new file mode 100644 index 0000000000..3a32d4fde7 --- /dev/null +++ b/apps/rooch_dex/Move.toml @@ -0,0 +1,16 @@ +[package] +name = "rooch_dex" +version = "0.0.1" + +[dependencies] +MoveStdlib = { local = "../../frameworks/move-stdlib" } +MoveosStdlib = { local = "../../frameworks/moveos-stdlib" } +RoochFramework = { local = "../../frameworks/rooch-framework" } +app_admin = { local = "../app_admin" } + +[addresses] +rooch_dex = "_" +app_admin = "_" +std = "0x1" +moveos_std = "0x2" +rooch_framework = "0x3" diff --git a/apps/rooch_dex/sources/route.move b/apps/rooch_dex/sources/route.move new file mode 100644 index 0000000000..08a086b3cc --- /dev/null +++ b/apps/rooch_dex/sources/route.move @@ -0,0 +1,556 @@ +module rooch_dex::router { + use rooch_dex::swap; + use std::signer; + use std::signer::address_of; + use rooch_framework::account_coin_store; + use moveos_std::object::ObjectID; + use rooch_framework::coin; + use rooch_dex::swap_utils; + + + + const ErrorInsufficientOutputAmount: u64 = 1; + const ErrorInsufficientInputAmount: u64 = 2; + const ErrorInsufficientXAmount: u64 = 3; + const ErrorInsufficientYAmount: u64 = 4; + const ErrorTokenPairNotExist: u64 = 5; + + + public entry fun create_token_pair( + sender: &signer, + ) { + if (swap_utils::sort_token_type()) { + swap::create_pair(sender); + } else { + swap::create_pair(sender); + } + } + + + public entry fun add_liquidity( + sender: &signer, + amount_x_desired: u64, + amount_y_desired: u64, + amount_x_min: u64, + amount_y_min: u64, + coin_info: ObjectID, + ) { + if (!(swap::is_pair_created() || swap::is_pair_created())) { + create_token_pair(sender); + }; + + let amount_x; + let amount_y; + let _lp_amount; + if (swap_utils::sort_token_type()) { + (amount_x, amount_y, _lp_amount) = swap::add_liquidity(sender, amount_x_desired, amount_y_desired, coin_info); + assert!(amount_x >= amount_x_min, ErrorInsufficientXAmount); + assert!(amount_y >= amount_y_min, ErrorInsufficientYAmount); + } else { + (amount_y, amount_x, _lp_amount) = swap::add_liquidity(sender, amount_y_desired, amount_x_desired, coin_info); + assert!(amount_x >= amount_x_min, ErrorInsufficientXAmount); + assert!(amount_y >= amount_y_min, ErrorInsufficientYAmount); + }; + } + + fun assert_token_pair_created(){ + assert!(swap::is_pair_created() || swap::is_pair_created(), ErrorTokenPairNotExist); + } + + /// Remove Liquidity + public entry fun remove_liquidity( + sender: &signer, + liquidity: u64, + amount_x_min: u64, + amount_y_min: u64, + coin_info: ObjectID + ) { + assert_token_pair_created(); + let amount_x; + let amount_y; + if (swap_utils::sort_token_type()) { + (amount_x, amount_y) = swap::remove_liquidity(sender, liquidity, coin_info); + assert!(amount_x >= amount_x_min, ErrorInsufficientXAmount); + assert!(amount_y >= amount_y_min, ErrorInsufficientYAmount); + } else { + (amount_y, amount_x) = swap::remove_liquidity(sender, liquidity, coin_info); + assert!(amount_x >= amount_x_min, ErrorInsufficientXAmount); + assert!(amount_y >= amount_y_min, ErrorInsufficientYAmount); + } + } + + fun swap_event( + sender_addr: address, + amount_x_in: u64, + amount_y_in: u64, + amount_x_out: u64, + amount_y_out: u64 + ) { + if (swap_utils::sort_token_type()){ + swap::add_swap_event_with_address(sender_addr, amount_x_in, amount_y_in, amount_x_out, amount_y_out); + } else { + swap::add_swap_event_with_address(sender_addr, amount_y_in, amount_x_in, amount_y_out, amount_x_out); + } + } + + /// Swap exact input amount of X to maxiumin possible amount of Y + public entry fun swap_with_exact_input( + sender: &signer, + x_in: u64, + y_min_out: u64, + ) { + assert_token_pair_created(); + let y_out = if (swap_utils::sort_token_type()) { + swap::swap_exact_x_to_y(sender, x_in, signer::address_of(sender)) + } else { + swap::swap_exact_y_to_x(sender, x_in, signer::address_of(sender)) + }; + assert!(y_out >= y_min_out, ErrorInsufficientOutputAmount); + swap_event(address_of(sender), x_in, 0, 0, y_out); + } + + /// Swap miniumn possible amount of X to exact output amount of Y + public entry fun swap_with_exact_output( + sender: &signer, + y_out: u64, + x_max_in: u64, + ) { + assert_token_pair_created(); + let x_in = if (swap_utils::sort_token_type()) { + let (rin, rout, _) = swap::token_reserves(); + let amount_in = swap_utils::get_amount_in(y_out, rin, rout); + swap::swap_x_to_exact_y(sender, amount_in, y_out, signer::address_of(sender)) + } else { + let (rout, rin, _) = swap::token_reserves(); + let amount_in = swap_utils::get_amount_in(y_out, rin, rout); + swap::swap_y_to_exact_x(sender, amount_in, y_out, signer::address_of(sender)) + }; + assert!(x_in <= x_max_in, ErrorInsufficientInputAmount); + swap_event(address_of(sender), x_in, 0, 0, y_out); + } + + fun get_output_coin(is_x_to_y: bool, x_in: coin::Coin): coin::Coin { + if (is_x_to_y) { + let (x_out, y_out) = swap::swap_exact_x_to_y_direct(x_in); + coin::destroy_zero(x_out); + y_out + } + else { + let (y_out, x_out) = swap::swap_exact_y_to_x_direct(x_in); + coin::destroy_zero(x_out); + y_out + } + } + + public fun swap_exact_x_to_y_direct_external(x_in: coin::Coin): coin::Coin { + assert_token_pair_created(); + let x_in_amount = coin::value(&x_in); + let is_x_to_y = swap_utils::sort_token_type(); + let y_out = get_output_coin(is_x_to_y, x_in); + let y_out_amount = coin::value(&y_out); + swap_event(@0x0, (x_in_amount as u64), 0, 0, (y_out_amount as u64)); + y_out + } + + fun get_intermediate_output_x_to_exact_y(is_x_to_y: bool, x_in: coin::Coin, amount_out: u64): coin::Coin { + if (is_x_to_y) { + let (x_out, y_out) = swap::swap_x_to_exact_y_direct(x_in, amount_out); + coin::destroy_zero(x_out); + y_out + } + else { + let (y_out, x_out) = swap::swap_y_to_exact_x_direct(x_in, amount_out); + coin::destroy_zero(x_out); + y_out + } + } + + fun get_amount_in_internal(is_x_to_y:bool, y_out_amount: u64): u64 { + if (is_x_to_y) { + let (rin, rout, _) = swap::token_reserves(); + swap_utils::get_amount_in(y_out_amount, rin, rout) + } else { + let (rout, rin, _) = swap::token_reserves(); + swap_utils::get_amount_in(y_out_amount, rin, rout) + } + } + + fun get_amount_out_internal(is_x_to_y:bool, x_in_amount: u64): u64 { + if (is_x_to_y) { + let (rin, rout, _) = swap::token_reserves(); + swap_utils::get_amount_out(x_in_amount, rin, rout) + } else { + let (rout, rin, _) = swap::token_reserves(); + swap_utils::get_amount_out(x_in_amount, rin, rout) + } + } + + public fun get_amount_in(y_out_amount: u64): u64 { + assert_token_pair_created(); + let is_x_to_y = swap_utils::sort_token_type(); + get_amount_in_internal(is_x_to_y, y_out_amount) + } + + public fun get_amount_out(x_in_amount: u64): u64 { + assert_token_pair_created(); + let is_x_to_y = swap_utils::sort_token_type(); + get_amount_out_internal(is_x_to_y, x_in_amount) + } + + public fun swap_x_to_exact_y_direct_external(x_in: coin::Coin, y_out_amount:u64): (coin::Coin, coin::Coin) { + assert_token_pair_created(); + let is_x_to_y = swap_utils::sort_token_type(); + let x_in_withdraw_amount = get_amount_in_internal(is_x_to_y, y_out_amount); + let x_in_amount = coin::value(&x_in); + assert!(x_in_amount >= (x_in_withdraw_amount as u256), ErrorInsufficientXAmount); + let x_in_left = coin::extract(&mut x_in, x_in_amount - (x_in_withdraw_amount as u256)); + let y_out = get_intermediate_output_x_to_exact_y(is_x_to_y, x_in, y_out_amount); + swap_event(@0x0, x_in_withdraw_amount, 0, 0, y_out_amount); + (x_in_left, y_out) + } + + fun swap_exact_input_double_internal( + sender: &signer, + first_is_x_to_y: bool, + second_is_y_to_z: bool, + x_in: u64, + z_min_out: u64, + ): u64 { + let coin_x = account_coin_store::withdraw(sender, (x_in as u256)); + let coin_y = get_output_coin(first_is_x_to_y, coin_x); + let coins_y_out = (coin::value(&coin_y) as u64); + let coin_z = get_output_coin(second_is_y_to_z, coin_y); + + let coin_z_amt = (coin::value(&coin_z) as u64); + + assert!(coin_z_amt >= z_min_out, ErrorInsufficientOutputAmount); + let sender_addr = signer::address_of(sender); + account_coin_store::deposit(sender_addr, coin_z); + + swap_event(address_of(sender), x_in, 0, 0, coins_y_out); + swap_event(address_of(sender), coins_y_out, 0, 0, coin_z_amt); + coin_z_amt + } + + /// Same as `swap_exact_input` with specify path: X -> Y -> Z + public entry fun swap_exact_input_doublehop( + sender: &signer, + x_in: u64, + z_min_out: u64, + ) { + assert_token_pair_created(); + assert_token_pair_created(); + let first_is_x_to_y: bool = swap_utils::sort_token_type(); + + let second_is_y_to_z: bool = swap_utils::sort_token_type(); + + swap_exact_input_double_internal(sender, first_is_x_to_y, second_is_y_to_z, x_in, z_min_out); + } + + fun swap_exact_output_double_internal( + sender: &signer, + first_is_x_to_y: bool, + second_is_y_to_z: bool, + x_max_in: u64, + z_out: u64, + ): u64 { + let rin; + let rout; + let y_out = if (second_is_y_to_z) { + (rin, rout, _) = swap::token_reserves(); + swap_utils::get_amount_in(z_out, rin, rout) + }else { + (rout, rin, _) = swap::token_reserves(); + swap_utils::get_amount_in(z_out, rin, rout) + }; + let x_in = if (first_is_x_to_y) { + (rin, rout, _) = swap::token_reserves(); + swap_utils::get_amount_in(y_out, rin, rout) + }else { + (rout, rin, _) = swap::token_reserves(); + swap_utils::get_amount_in(y_out, rin, rout) + }; + + assert!(x_in <= x_max_in, ErrorInsufficientInputAmount); + + let coin_x = account_coin_store::withdraw(sender, (x_in as u256)); + let coin_y = get_intermediate_output_x_to_exact_y(first_is_x_to_y, coin_x, y_out); + let coin_z = get_intermediate_output_x_to_exact_y(second_is_y_to_z, coin_y, z_out); + + let coin_z_amt = (coin::value(&coin_z) as u64); + let sender_addr = signer::address_of(sender); + account_coin_store::deposit(sender_addr, coin_z); + + swap_event(address_of(sender), x_in, 0, 0, y_out); + swap_event(address_of(sender), y_out, 0, 0, coin_z_amt); + coin_z_amt + } + + /// Same as `swap_exact_output` with specify path: X -> Y -> Z + public entry fun swap_exact_output_doublehop( + sender: &signer, + z_out: u64, + x_max_in: u64, + ) { + assert_token_pair_created(); + assert_token_pair_created(); + let first_is_x_to_y: bool = swap_utils::sort_token_type(); + + let second_is_y_to_z: bool = swap_utils::sort_token_type(); + + swap_exact_output_double_internal(sender, first_is_x_to_y, second_is_y_to_z, x_max_in, z_out); + } + + fun swap_exact_input_triple_internal( + sender: &signer, + first_is_x_to_y: bool, + second_is_y_to_z: bool, + third_is_z_to_a: bool, + x_in: u64, + a_min_out: u64, + ): u64 { + let coin_x = account_coin_store::withdraw(sender, (x_in as u256)); + let coin_y = get_output_coin(first_is_x_to_y, coin_x); + let coins_y_out = (coin::value(&coin_y) as u64); + + let coin_z = get_output_coin(second_is_y_to_z, coin_y); + let coins_z_out = (coin::value(&coin_z) as u64); + + let coin_a = get_output_coin(third_is_z_to_a, coin_z); + + let coin_a_amt = (coin::value(&coin_a) as u64); + + assert!(coin_a_amt >= a_min_out, ErrorInsufficientOutputAmount); + let sender_addr = signer::address_of(sender); + account_coin_store::deposit(sender_addr, coin_a); + + swap_event(address_of(sender), x_in, 0, 0, coins_y_out); + swap_event(address_of(sender), coins_y_out, 0, 0, coins_z_out); + swap_event(address_of(sender), coins_z_out, 0, 0, coin_a_amt); + coin_a_amt + } + + /// Same as `swap_exact_input` with specify path: X -> Y -> Z -> A + public entry fun swap_exact_input_triplehop( + sender: &signer, + x_in: u64, + a_min_out: u64, + ) { + assert_token_pair_created(); + assert_token_pair_created(); + assert_token_pair_created(); + let first_is_x_to_y: bool = swap_utils::sort_token_type(); + + let second_is_y_to_z: bool = swap_utils::sort_token_type(); + + let third_is_z_to_a: bool = swap_utils::sort_token_type(); + + swap_exact_input_triple_internal(sender, first_is_x_to_y, second_is_y_to_z, third_is_z_to_a, x_in, a_min_out); + } + + fun swap_exact_output_triple_internal( + sender: &signer, + first_is_x_to_y: bool, + second_is_y_to_z: bool, + third_is_z_to_a: bool, + x_max_in: u64, + a_out: u64, + ): u64 { + let rin; + let rout; + let z_out = if (third_is_z_to_a) { + (rin, rout, _) = swap::token_reserves(); + swap_utils::get_amount_in(a_out, rin, rout) + }else { + (rout, rin, _) = swap::token_reserves(); + swap_utils::get_amount_in(a_out, rin, rout) + }; + + let y_out = if (second_is_y_to_z) { + (rin, rout, _) = swap::token_reserves(); + swap_utils::get_amount_in(z_out, rin, rout) + }else { + (rout, rin, _) = swap::token_reserves(); + swap_utils::get_amount_in(z_out, rin, rout) + }; + let x_in = if (first_is_x_to_y) { + (rin, rout, _) = swap::token_reserves(); + swap_utils::get_amount_in(y_out, rin, rout) + }else { + (rout, rin, _) = swap::token_reserves(); + swap_utils::get_amount_in(y_out, rin, rout) + }; + + assert!(x_in <= x_max_in, ErrorInsufficientInputAmount); + + let coin_x = account_coin_store::withdraw(sender, (x_in as u256)); + let coin_y = get_intermediate_output_x_to_exact_y(first_is_x_to_y, coin_x, y_out); + let coin_z = get_intermediate_output_x_to_exact_y(second_is_y_to_z, coin_y, z_out); + let coin_a = get_intermediate_output_x_to_exact_y(third_is_z_to_a, coin_z, a_out); + + let coin_a_amt = (coin::value(&coin_a) as u64); + let sender_addr = signer::address_of(sender); + account_coin_store::deposit(sender_addr, coin_a); + + swap_event(address_of(sender), x_in, 0, 0, y_out); + swap_event(address_of(sender), y_out, 0, 0, z_out); + swap_event(address_of(sender), z_out, 0, 0, coin_a_amt); + coin_a_amt + } + + /// Same as `swap_exact_output` with specify path: X -> Y -> Z -> A + public entry fun swap_exact_output_triplehop( + sender: &signer, + a_out: u64, + x_max_in: u64, + ) { + assert_token_pair_created(); + assert_token_pair_created(); + assert_token_pair_created(); + let first_is_x_to_y: bool = swap_utils::sort_token_type(); + + let second_is_y_to_z: bool = swap_utils::sort_token_type(); + + let third_is_z_to_a: bool = swap_utils::sort_token_type(); + + swap_exact_output_triple_internal(sender, first_is_x_to_y, second_is_y_to_z, third_is_z_to_a, x_max_in, a_out); + } + + + fun swap_exact_input_quadruple_internal( + sender: &signer, + first_is_x_to_y: bool, + second_is_y_to_z: bool, + third_is_z_to_a: bool, + fourth_is_a_to_b: bool, + x_in: u64, + b_min_out: u64, + ): u64 { + let coin_x = account_coin_store::withdraw(sender, (x_in as u256)); + let coin_y = get_output_coin(first_is_x_to_y, coin_x); + let coins_y_out = (coin::value(&coin_y) as u64); + + let coin_z = get_output_coin(second_is_y_to_z, coin_y); + let coins_z_out = (coin::value(&coin_z) as u64); + + let coin_a = get_output_coin(third_is_z_to_a, coin_z); + let coin_a_out = (coin::value(&coin_a) as u64); + + let coin_b = get_output_coin(fourth_is_a_to_b, coin_a); + let coin_b_amt = (coin::value(&coin_b) as u64); + + assert!(coin_b_amt >= b_min_out, ErrorInsufficientOutputAmount); + let sender_addr = signer::address_of(sender); + account_coin_store::deposit(sender_addr, coin_b); + + swap_event(address_of(sender), x_in, 0, 0, coins_y_out); + swap_event(address_of(sender), coins_y_out, 0, 0, coins_z_out); + swap_event(address_of(sender), coins_z_out, 0, 0, coin_a_out); + swap_event(address_of(sender), coin_a_out, 0, 0, coin_b_amt); + coin_b_amt + } + + /// Same as `swap_exact_input` with specify path: X -> Y -> Z -> A -> B + public entry fun swap_exact_input_quadruplehop( + sender: &signer, + x_in: u64, + b_min_out: u64, + ) { + assert_token_pair_created(); + assert_token_pair_created(); + assert_token_pair_created(); + assert_token_pair_created(); + let first_is_x_to_y: bool = swap_utils::sort_token_type(); + + let second_is_y_to_z: bool = swap_utils::sort_token_type(); + + let third_is_z_to_a: bool = swap_utils::sort_token_type(); + + let fourth_is_a_to_b: bool = swap_utils::sort_token_type(); + + swap_exact_input_quadruple_internal(sender, first_is_x_to_y, second_is_y_to_z, third_is_z_to_a, fourth_is_a_to_b, x_in, b_min_out); + } + + fun swap_exact_output_quadruple_internal( + sender: &signer, + first_is_x_to_y: bool, + second_is_y_to_z: bool, + third_is_z_to_a: bool, + fourth_is_a_to_b: bool, + x_max_in: u64, + b_out: u64, + ): u64 { + let rin; + let rout; + + let a_out = if (fourth_is_a_to_b) { + (rin, rout, _) = swap::token_reserves(); + swap_utils::get_amount_in(b_out, rin, rout) + }else { + (rout, rin, _) = swap::token_reserves(); + swap_utils::get_amount_in(b_out, rin, rout) + }; + + let z_out = if (third_is_z_to_a) { + (rin, rout, _) = swap::token_reserves(); + swap_utils::get_amount_in(a_out, rin, rout) + }else { + (rout, rin, _) = swap::token_reserves(); + swap_utils::get_amount_in(a_out, rin, rout) + }; + + let y_out = if (second_is_y_to_z) { + (rin, rout, _) = swap::token_reserves(); + swap_utils::get_amount_in(z_out, rin, rout) + }else { + (rout, rin, _) = swap::token_reserves(); + swap_utils::get_amount_in(z_out, rin, rout) + }; + let x_in = if (first_is_x_to_y) { + (rin, rout, _) = swap::token_reserves(); + swap_utils::get_amount_in(y_out, rin, rout) + }else { + (rout, rin, _) = swap::token_reserves(); + swap_utils::get_amount_in(y_out, rin, rout) + }; + + assert!(x_in <= x_max_in, ErrorInsufficientInputAmount); + + let coin_x = account_coin_store::withdraw(sender, (x_in as u256)); + let coin_y = get_intermediate_output_x_to_exact_y(first_is_x_to_y, coin_x, y_out); + let coin_z = get_intermediate_output_x_to_exact_y(second_is_y_to_z, coin_y, z_out); + let coin_a = get_intermediate_output_x_to_exact_y(third_is_z_to_a, coin_z, a_out); + let coin_b = get_intermediate_output_x_to_exact_y(fourth_is_a_to_b, coin_a, b_out); + + let coin_b_amt = (coin::value(&coin_b) as u64); + let sender_addr = signer::address_of(sender); + account_coin_store::deposit(sender_addr, coin_b); + + swap_event(address_of(sender), x_in, 0, 0, y_out); + swap_event(address_of(sender), y_out, 0, 0, z_out); + swap_event(address_of(sender), z_out, 0, 0, a_out); + swap_event(address_of(sender), a_out, 0, 0, coin_b_amt); + coin_b_amt + } + + /// Same as `swap_exact_output` with specify path: X -> Y -> Z -> A -> B + public entry fun swap_exact_output_quadruplehop( + sender: &signer, + b_out: u64, + x_max_in: u64, + ) { + assert_token_pair_created(); + assert_token_pair_created(); + assert_token_pair_created(); + assert_token_pair_created(); + let first_is_x_to_y: bool = swap_utils::sort_token_type(); + + let second_is_y_to_z: bool = swap_utils::sort_token_type(); + + let third_is_z_to_a: bool = swap_utils::sort_token_type(); + + let fourth_is_a_to_b = swap_utils::sort_token_type(); + + swap_exact_output_quadruple_internal(sender, first_is_x_to_y, second_is_y_to_z, third_is_z_to_a, fourth_is_a_to_b, x_max_in, b_out); + } +} diff --git a/apps/rooch_dex/sources/swap.move b/apps/rooch_dex/sources/swap.move new file mode 100644 index 0000000000..b7a9fdce1a --- /dev/null +++ b/apps/rooch_dex/sources/swap.move @@ -0,0 +1,649 @@ +module rooch_dex::swap { + use std::signer; + use std::option::none; + use std::string; + use std::u128; + use app_admin::admin; + use moveos_std::timestamp; + use moveos_std::account::borrow_mut_resource; + use rooch_dex::swap_utils; + use rooch_framework::account_coin_store; + use moveos_std::event; + use moveos_std::type_info; + use rooch_framework::coin; + use moveos_std::signer::module_signer; + use moveos_std::tx_context::sender; + use moveos_std::object; + use moveos_std::account; + use rooch_framework::coin::{CoinInfo, coin_info}; + use moveos_std::object::{Object, ObjectID}; + use rooch_framework::coin_store::{CoinStore, balance, deposit, withdraw}; + use rooch_framework::coin_store; + + + + friend rooch_dex::router; + + const RESOURCE_ACCOUNT: address = @rooch_dex; + const MINIMUM_LIQUIDITY: u128 = 1000; + const MAX_COIN_NAME_LENGTH: u64 = 32; + + // List of errors + const ErrorAlreadyExists: u64 = 1; + const ErrorTokenPairNotOpen: u64 = 2; + const ErrorInsufficientLiquidityAmount: u64 = 3; + const ErrorInsufficientTokenAmount: u64 = 4; + const ErrorInsufficientLiquidity: u64 = 5; + const ErrorInvalidAmount: u64 = 6; + const ErrorLiquidityBurned: u64 = 7; + const ErrorOutputTokenAmount: u64 = 8; + const ErrorInputTokenAmount: u64 = 9; + const ErrorKValue: u64 = 10; + const ErrorWithdrawFee: u64 = 11; + + const PRECISION: u64 = 10000; + + const MAX_U128: u128 = 340282366920938463463374607431768211455; + + struct LPToken has key, store {} + + struct TokenPair has key { + creator: address, + fee: Object>>, + k_last: u128, + balance_x: Object>, + balance_y: Object>, + is_open: bool + } + + struct TokenPairReserve has key { + reserve_x: u64, + reserve_y: u64, + block_timestamp_last: u64 + } + + struct PairCreatedEvent has drop, store, copy { + user: address, + token_x: string::String, + token_y: string::String + } + + struct AddLiquidityEvent has drop, store, copy { + user: address, + amount_x: u64, + amount_y: u64, + liquidity: u64, + fee: u64 + } + + struct RemoveLiquidityEvent has drop, store, copy { + user: address, + liquidity: u64, + amount_x: u64, + amount_y: u64, + fee: u64 + } + + struct SwapEvent has drop, store, copy { + user: address, + amount_x_in: u64, + amount_y_in: u64, + amount_x_out: u64, + amount_y_out: u64 + } + + struct RoochDexCap has key {} + + fun init() { + object::transfer_extend(object::new_named_object(RoochDexCap{}), sender()) + } + + + public entry fun create_admin(_admin: &mut Object, receiver: address){ + let new_admin = object::new(RoochDexCap{}); + object::transfer_extend(new_admin, receiver) + } + + public entry fun delete_admin(_admin: &mut Object, admin_id: ObjectID){ + let admin_obj = object::take_object_extend(admin_id); + let RoochDexCap{} = object::remove(admin_obj); + } + + /// Create the specified coin pair + public(friend) fun create_pair( + sender: &signer, + ) { + assert!(!is_pair_created(), ErrorAlreadyExists); + + let sender_addr = signer::address_of(sender); + let resource_signer = module_signer(); + + let lp_name: string::String = string::utf8(b"RoochDex-"); + let name_x = coin::symbol(coin_info()); + let name_y = coin::symbol(coin_info()); + string::append(&mut lp_name, name_x); + string::append_utf8(&mut lp_name, b"-"); + string::append(&mut lp_name, name_y); + string::append_utf8(&mut lp_name, b"-LP"); + if (string::length(&lp_name) > MAX_COIN_NAME_LENGTH) { + lp_name = string::utf8(b"RoochDex LPs"); + }; + + let coin_info = coin::register_extend>( + lp_name, + string::utf8(b"RoochDex-LP"), + none(), + 8, + ); + + account::move_resource_to>( + &resource_signer, + TokenPairReserve { + reserve_x: 0, + reserve_y: 0, + block_timestamp_last: 0 + } + ); + + account::move_resource_to>( + &resource_signer, + TokenPair { + creator: sender_addr, + fee: coin_store::create_coin_store(), + k_last: 0, + balance_x: coin_store::create_coin_store(), + balance_y: coin_store::create_coin_store(), + is_open: true + } + ); + + // pair created event + let token_x = type_info::type_name(); + let token_y = type_info::type_name(); + + event::emit( + PairCreatedEvent { + user: sender_addr, + token_x, + token_y + } + ); + + object::to_shared(coin_info); + } + + + public fun is_pair_created(): bool { + account::exists_resource>(RESOURCE_ACCOUNT) + } + + /// Obtain the LP token balance of `addr`. + /// This method can only be used to check other users' balance. + public fun lp_balance(addr: address): u256 { + account_coin_store::balance>(addr) + } + + /// Get the total supply of LP Tokens + public fun total_lp_supply(): u128 { + (coin::supply(coin_info>()) as u128) + } + + /// Get the current reserves of T0 and T1 with the latest updated timestamp + public fun token_reserves(): (u64, u64, u64) { + let reserve = account::borrow_resource>(RESOURCE_ACCOUNT); + ( + reserve.reserve_x, + reserve.reserve_y, + reserve.block_timestamp_last + ) + } + + /// The amount of balance currently in pools of the liquidity pair + public fun token_balances(): (u64, u64) { + let token_pair = + account::borrow_resource>(RESOURCE_ACCOUNT); + ( + (balance(&token_pair.balance_x) as u64), + (balance(&token_pair.balance_y) as u64) + ) + } + + + // ===================== Update functions ====================== + /// Add more liquidity to token types. This method explicitly assumes the + /// min of both tokens are 0. + public(friend) fun add_liquidity( + sender: &signer, + amount_x: u64, + amount_y: u64, + coin_info_id: ObjectID + ): (u64, u64, u64) { + let coin_info = object::borrow_mut_object_shared(coin_info_id); + let (a_x, a_y, coin_lp, fee, coin_left_x, coin_left_y) = add_liquidity_direct(account_coin_store::withdraw(sender, + (amount_x as u256) + ), account_coin_store::withdraw(sender, (amount_y as u256)), coin_info); + let sender_addr = signer::address_of(sender); + let lp_amount = (coin::value(&coin_lp) as u64); + assert!(lp_amount > 0, ErrorInsufficientLiquidity); + account_coin_store::deposit(sender_addr, coin_lp); + account_coin_store::deposit(sender_addr, coin_left_x); + account_coin_store::deposit(sender_addr, coin_left_y); + + event::emit>( + AddLiquidityEvent { + user: sender_addr, + amount_x: a_x, + amount_y: a_y, + liquidity: lp_amount, + fee, + } + ); + + (a_x, a_y, lp_amount) + } + + public(friend) fun add_swap_event( + sender: &signer, + amount_x_in: u64, + amount_y_in: u64, + amount_x_out: u64, + amount_y_out: u64 + ) { + let sender_addr = signer::address_of(sender); + event::emit>( + SwapEvent { + user: sender_addr, + amount_x_in, + amount_y_in, + amount_x_out, + amount_y_out + } + ); + } + + public(friend) fun add_swap_event_with_address( + sender_addr: address, + amount_x_in: u64, + amount_y_in: u64, + amount_x_out: u64, + amount_y_out: u64 + ) { + event::emit>( + SwapEvent { + user: sender_addr, + amount_x_in, + amount_y_in, + amount_x_out, + amount_y_out + } + ); + } + + /// Add more liquidity to token types. This method explicitly assumes the + /// min of both tokens are 0. + fun add_liquidity_direct( + x: coin::Coin, + y: coin::Coin, + coin_info: &mut Object>> + ): (u64, u64, coin::Coin>, u64, coin::Coin, coin::Coin){ + let amount_x = (coin::value(&x) as u64); + let amount_y = (coin::value(&y) as u64); + let (reserve_x, reserve_y, _) = token_reserves(); + let (a_x, a_y) = if (reserve_x == 0 && reserve_y == 0) { + (amount_x, amount_y) + } else { + let amount_y_optimal = swap_utils::quote(amount_x, reserve_x, reserve_y); + if (amount_y_optimal <= amount_y) { + (amount_x, amount_y_optimal) + } else { + let amount_x_optimal = swap_utils::quote(amount_y, reserve_y, reserve_x); + assert!(amount_x_optimal <= amount_x, ErrorInvalidAmount); + (amount_x_optimal, amount_y) + } + }; + + assert!(a_x <= amount_x, ErrorInsufficientTokenAmount); + assert!(a_y <= amount_y, ErrorInsufficientTokenAmount); + + let left_x = coin::extract(&mut x, (amount_x - a_x as u256)); + let left_y = coin::extract(&mut y, (amount_y - a_y as u256)); + deposit_x(x); + deposit_y(y); + let (lp, fee) = mint(coin_info); + (a_x, a_y, lp, fee, left_x, left_y) + } + + /// Remove liquidity to token types. + public(friend) fun remove_liquidity( + sender: &signer, + liquidity: u64, + coin_info_id: ObjectID + ): (u64, u64) { + let coin_info = object::borrow_mut_object_shared(coin_info_id); + let coins = account_coin_store::withdraw>(sender, (liquidity as u256)); + let (coins_x, coins_y, fee) = remove_liquidity_direct(coins, coin_info); + let amount_x = (coin::value(&coins_x) as u64); + let amount_y = (coin::value(&coins_y) as u64); + let sender_addr = signer::address_of(sender); + account_coin_store::deposit(sender_addr, coins_x); + account_coin_store::deposit(sender_addr, coins_y); + // event + event::emit>( + RemoveLiquidityEvent { + user: sender_addr, + amount_x, + amount_y, + liquidity, + fee + } + ); + (amount_x, amount_y) + } + + /// Remove liquidity to token types. + fun remove_liquidity_direct( + liquidity: coin::Coin>, + coin_info: &mut Object>> + ): (coin::Coin, coin::Coin, u64){ + burn(liquidity, coin_info) + } + + /// Swap X to Y, X is in and Y is out. This method assumes amount_out_min is 0 + public(friend) fun swap_exact_x_to_y( + sender: &signer, + amount_in: u64, + to: address + ): u64{ + let coins = account_coin_store::withdraw(sender, (amount_in as u256)); + let (coins_x_out, coins_y_out) = swap_exact_x_to_y_direct(coins); + let amount_out = coin::value(&coins_y_out); + coin::destroy_zero(coins_x_out); // or others ways to drop `coins_x_out` + account_coin_store::deposit(to, coins_y_out); + (amount_out as u64) + } + + /// Swap X to Y, X is in and Y is out. This method assumes amount_out_min is 0 + public(friend) fun swap_exact_x_to_y_direct( + coins_in: coin::Coin + ): (coin::Coin, coin::Coin){ + let amount_in = coin::value(&coins_in); + deposit_x(coins_in); + let (rin, rout, _) = token_reserves(); + let amount_out = swap_utils::get_amount_out((amount_in as u64), rin, rout); + let (coins_x_out, coins_y_out) = swap(0, amount_out); + assert!(coin::value(&coins_x_out) == 0, ErrorOutputTokenAmount); + (coins_x_out, coins_y_out) + } + + public(friend) fun swap_x_to_exact_y( + sender: &signer, + amount_in: u64, + amount_out: u64, + to: address + ): u64 { + let coins_in = account_coin_store::withdraw(sender, (amount_in as u256)); + let (coins_x_out, coins_y_out) = swap_x_to_exact_y_direct(coins_in, amount_out); + coin::destroy_zero(coins_x_out); // or others ways to drop `coins_x_out` + account_coin_store::deposit(to, coins_y_out); + amount_in + } + + public(friend) fun swap_x_to_exact_y_direct( + coins_in: coin::Coin, amount_out: u64 + ): (coin::Coin, coin::Coin) { + deposit_x(coins_in); + let (coins_x_out, coins_y_out) = swap(0, amount_out); + assert!(coin::value(&coins_x_out) == 0, ErrorOutputTokenAmount); + (coins_x_out, coins_y_out) + } + + /// Swap Y to X, Y is in and X is out. This method assumes amount_out_min is 0 + public(friend) fun swap_exact_y_to_x( + sender: &signer, + amount_in: u64, + to: address + ): u64{ + let coins = account_coin_store::withdraw(sender, (amount_in as u256)); + let (coins_x_out, coins_y_out) = swap_exact_y_to_x_direct(coins); + let amount_out = coin::value(&coins_x_out); + account_coin_store::deposit(to, coins_x_out); + coin::destroy_zero(coins_y_out); // or others ways to drop `coins_y_out` + (amount_out as u64) + } + + public(friend) fun swap_y_to_exact_x( + sender: &signer, + amount_in: u64, + amount_out: u64, + to: address + ): u64 { + let coins_in = account_coin_store::withdraw(sender, (amount_in as u256)); + let (coins_x_out, coins_y_out) = swap_y_to_exact_x_direct(coins_in, amount_out); + account_coin_store::deposit(to, coins_x_out); + coin::destroy_zero(coins_y_out); // or others ways to drop `coins_y_out` + amount_in + } + + public(friend) fun swap_y_to_exact_x_direct( + coins_in: coin::Coin, amount_out: u64 + ): (coin::Coin, coin::Coin) { + deposit_y(coins_in); + let (coins_x_out, coins_y_out) = swap(amount_out, 0); + assert!(coin::value(&coins_y_out) == 0, ErrorOutputTokenAmount); + (coins_x_out, coins_y_out) + } + + /// Swap Y to X, Y is in and X is out. This method assumes amount_out_min is 0 + public(friend) fun swap_exact_y_to_x_direct( + coins_in: coin::Coin + ): (coin::Coin, coin::Coin) { + let amount_in = coin::value(&coins_in); + deposit_y(coins_in); + let (rout, rin, _) = token_reserves(); + let amount_out = swap_utils::get_amount_out((amount_in as u64), rin, rout); + let (coins_x_out, coins_y_out) = swap(amount_out, 0); + assert!(coin::value(&coins_y_out) == 0, ErrorOutputTokenAmount); + (coins_x_out, coins_y_out) + } + + fun swap( + amount_x_out: u64, + amount_y_out: u64 + ): (coin::Coin, coin::Coin){ + assert!(amount_x_out > 0 || amount_y_out > 0, ErrorOutputTokenAmount); + + let reserves = account::borrow_mut_resource>(RESOURCE_ACCOUNT); + assert!(amount_x_out < reserves.reserve_x && amount_y_out < reserves.reserve_y, ErrorInsufficientLiquidity); + + let token_pair = account::borrow_mut_resource>(RESOURCE_ACCOUNT); + assert!(token_pair.is_open, ErrorTokenPairNotOpen); + let coins_x_out = coin::zero(); + let coins_y_out = coin::zero(); + if (amount_x_out > 0) coin::merge(&mut coins_x_out, withdraw_x((amount_x_out as u256), token_pair)); + if (amount_y_out > 0) coin::merge(&mut coins_y_out, withdraw_y((amount_y_out as u256), token_pair)); + let (balance_x, balance_y) = token_balances(); + + let amount_x_in = if (balance_x > reserves.reserve_x - amount_x_out) { + balance_x - (reserves.reserve_x - amount_x_out) + } else { 0 }; + let amount_y_in = if (balance_y > reserves.reserve_y - amount_y_out) { + balance_y - (reserves.reserve_y - amount_y_out) + } else { 0 }; + + assert!(amount_x_in > 0 || amount_y_in > 0, ErrorInputTokenAmount); + + let prec = (PRECISION as u128); + let balance_x_adjusted = (balance_x as u128) * prec - (amount_x_in as u128) * 25u128; + let balance_y_adjusted = (balance_y as u128) * prec - (amount_y_in as u128) * 25u128; + let reserve_x_adjusted = (reserves.reserve_x as u128) * prec; + let reserve_y_adjusted = (reserves.reserve_y as u128) * prec; + + let compare_result = if(balance_x_adjusted > 0 && reserve_x_adjusted > 0 && MAX_U128 / balance_x_adjusted > balance_y_adjusted && MAX_U128 / reserve_x_adjusted > reserve_y_adjusted){ + balance_x_adjusted * balance_y_adjusted >= reserve_x_adjusted * reserve_y_adjusted + }else{ + let p: u256 = (balance_x_adjusted as u256) * (balance_y_adjusted as u256); + let k: u256 = (reserve_x_adjusted as u256) * (reserve_y_adjusted as u256); + p >= k + }; + assert!(compare_result, ErrorKValue); + + update(balance_x, balance_y, reserves); + + (coins_x_out, coins_y_out) + } + + /// Mint LP Token. + /// This low-level function should be called from a contract which performs important safety checks + fun mint( + coin_info: &mut Object>> + ): (coin::Coin>, u64) { + let token_pair = borrow_mut_resource>(RESOURCE_ACCOUNT); + assert!(token_pair.is_open, ErrorTokenPairNotOpen); + let (balance_x, balance_y) = (balance(&token_pair.balance_x), balance(&token_pair.balance_y)); + let reserves = borrow_mut_resource>(RESOURCE_ACCOUNT); + let amount_x = (balance_x as u128) - (reserves.reserve_x as u128); + let amount_y = (balance_y as u128) - (reserves.reserve_y as u128); + + let fee = mint_fee(reserves.reserve_x, reserves.reserve_y, token_pair, coin_info); + + //Need to add fee amount which have not been mint. + let total_supply = total_lp_supply(); + let liquidity = if (total_supply == 0u128) { + let sqrt = u128::sqrt(amount_x * amount_y); + assert!(sqrt > MINIMUM_LIQUIDITY, ErrorInsufficientLiquidityAmount); + let l = sqrt - MINIMUM_LIQUIDITY; + // permanently lock the first MINIMUM_LIQUIDITY tokens + mint_lp_to(RESOURCE_ACCOUNT, (MINIMUM_LIQUIDITY as u64), coin_info); + l + } else { + let liquidity = u128::min(amount_x * total_supply / (reserves.reserve_x as u128), amount_y * total_supply / (reserves.reserve_y as u128)); + assert!(liquidity > 0u128, ErrorInsufficientLiquidityAmount); + liquidity + }; + + + let lp = mint_lp((liquidity as u64), coin_info); + + update((balance_x as u64), (balance_y as u64), reserves); + + token_pair.k_last = (reserves.reserve_x as u128) * (reserves.reserve_y as u128); + + (lp, fee) + } + + fun burn(lp_tokens: coin::Coin>, coin_info: &mut Object>>): (coin::Coin, coin::Coin, u64){ + let token_pair = account::borrow_mut_resource>(RESOURCE_ACCOUNT); + assert!(token_pair.is_open, ErrorTokenPairNotOpen); + let reserves = account::borrow_mut_resource>(RESOURCE_ACCOUNT); + let liquidity = coin::value(&lp_tokens); + + let fee = mint_fee(reserves.reserve_x, reserves.reserve_y, token_pair, coin_info); + + //Need to add fee amount which have not been mint. + let total_lp_supply = total_lp_supply(); + let amount_x = ((coin_store::balance(&token_pair.balance_x) as u128) * (liquidity as u128) / total_lp_supply as u256); + let amount_y = ((coin_store::balance(&token_pair.balance_x) as u128) * (liquidity as u128) / total_lp_supply as u256); + assert!(amount_x > 0 && amount_y > 0, ErrorLiquidityBurned); + coin::burn>(coin_info, lp_tokens); + + let w_x = withdraw_x(amount_x, token_pair); + let w_y = withdraw_y(amount_y, token_pair); + + update((coin_store::balance(&token_pair.balance_x) as u64), (coin_store::balance(&token_pair.balance_y) as u64), reserves); + + token_pair.k_last = (reserves.reserve_x as u128) * (reserves.reserve_y as u128); + + (w_x, w_y, fee) + } + + fun update(balance_x: u64, balance_y: u64, reserve: &mut TokenPairReserve) { + let block_timestamp = timestamp::now_seconds(); + + reserve.reserve_x = balance_x; + reserve.reserve_y = balance_y; + reserve.block_timestamp_last = block_timestamp; + } + + /// Mint LP Tokens to account + fun mint_lp_to( + to: address, + amount: u64, + mint_cap: &mut Object>>, + ) { + let coins = coin::mint>(mint_cap, (amount as u256)); + account_coin_store::deposit(to, coins); + } + + /// Mint LP Tokens to account + fun mint_lp(amount: u64, mint_cap: &mut Object>>): coin::Coin> { + coin::mint>(mint_cap, (amount as u256)) + } + + fun deposit_x(amount: coin::Coin){ + let token_pair = + borrow_mut_resource>(RESOURCE_ACCOUNT); + deposit(&mut token_pair.balance_x, amount); + } + + fun deposit_y(amount: coin::Coin) { + let token_pair = + borrow_mut_resource>(RESOURCE_ACCOUNT); + deposit(&mut token_pair.balance_y, amount); + } + + fun withdraw_x(amount: u256, token_pair: &mut TokenPair): coin::Coin { + assert!(balance(&token_pair.balance_x) > amount, ErrorInsufficientTokenAmount); + withdraw(&mut token_pair.balance_x, amount) + } + + fun withdraw_y(amount: u256, token_pair: &mut TokenPair): coin::Coin { + assert!(balance(&token_pair.balance_y) > amount, ErrorInsufficientTokenAmount); + withdraw(&mut token_pair.balance_y, amount) + } + + fun mint_fee(reserve_x: u64, reserve_y: u64, token_pair: &mut TokenPair, coin_info: &mut Object>>): u64 { + let fee = 0u64; + if (token_pair.k_last != 0) { + let root_k = u128::sqrt((reserve_x as u128) * (reserve_y as u128)); + let root_k_last = u128::sqrt(token_pair.k_last); + if (root_k > root_k_last) { + let numerator = total_lp_supply() * (root_k - root_k_last) * 8u128; + let denominator = root_k_last * 17u128 + (root_k * 8u128); + let liquidity = numerator / denominator; + fee = (liquidity as u64); + if (fee > 0) { + let coin = mint_lp(fee, coin_info); + deposit(&mut token_pair.fee, coin); + } + }; + }; + + fee + } + + public entry fun withdraw_fee(admin_cap: &mut Object){ + if (swap_utils::sort_token_type()) { + let token_pair = account::borrow_mut_resource>(RESOURCE_ACCOUNT); + assert!(balance(&token_pair.fee) > 0, ErrorWithdrawFee); + let fee = balance(&token_pair.fee); + let coin = withdraw(&mut token_pair.fee, fee); + account_coin_store::deposit(object::owner(admin_cap), coin); + } else { + let token_pair = account::borrow_mut_resource>(RESOURCE_ACCOUNT); + assert!(balance(&token_pair.fee) > 0, ErrorWithdrawFee); + let fee = balance(&token_pair.fee); + let coin = withdraw(&mut token_pair.fee, fee); + account_coin_store::deposit(object::owner(admin_cap), coin); + }; + } + + public entry fun update_token_pair_status(_admin_cap: &mut Object, status: bool){ + if (swap_utils::sort_token_type()) { + let token_pair = account::borrow_mut_resource>(RESOURCE_ACCOUNT); + token_pair.is_open = status + } else { + let token_pair = account::borrow_mut_resource>(RESOURCE_ACCOUNT); + token_pair.is_open = status + }; + } +} diff --git a/apps/rooch_dex/sources/swap_utils.move b/apps/rooch_dex/sources/swap_utils.move new file mode 100644 index 0000000000..aa8129eb1e --- /dev/null +++ b/apps/rooch_dex/sources/swap_utils.move @@ -0,0 +1,79 @@ +/// Uniswap v2 like token swap program +module rooch_dex::swap_utils { + use std::string; + use moveos_std::type_info; + use moveos_std::compare::compare; + + + const EQUAL: u8 = 0; + const SMALLER: u8 = 1; + const GREATER: u8 = 2; + + const ErrorInputTokenAmount: u64 = 1; + const ErrorInsufficientLiquidity: u64 = 2; + const ErrorInsufficientXAmount: u64 = 3; + const ErrorOutputTokenAmount: u64 = 4; + const ErrorTokenPairAleardyExist: u64 = 5; + + public fun get_amount_out( + amount_in: u64, + reserve_in: u64, + reserve_out: u64 + ): u64 { + assert!(amount_in > 0, ErrorInputTokenAmount); + assert!(reserve_in > 0 && reserve_out > 0, ErrorInsufficientLiquidity); + + let amount_in_with_fee = (amount_in as u128) * 9975u128; + let numerator = amount_in_with_fee * (reserve_out as u128); + let denominator = (reserve_in as u128) * 10000u128 + amount_in_with_fee; + ((numerator / denominator) as u64) + } + + public fun get_amount_in( + amount_out: u64, + reserve_in: u64, + reserve_out: u64 + ): u64 { + assert!(amount_out > 0, ErrorOutputTokenAmount); + assert!(reserve_in > 0 && reserve_out > 0, ErrorInsufficientLiquidity); + + let numerator = (reserve_in as u128) * (amount_out as u128) * 10000u128; + let denominator = ((reserve_out as u128) - (amount_out as u128)) * 9975u128; + (((numerator / denominator) as u64) + 1u64) + } + + public fun quote(amount_x: u64, reserve_x: u64, reserve_y: u64): u64 { + assert!(amount_x > 0, ErrorInsufficientXAmount); + assert!(reserve_x > 0 && reserve_y > 0, ErrorInsufficientLiquidity); + (((amount_x as u128) * (reserve_y as u128) / (reserve_x as u128)) as u64) + } + + public fun get_token_info(): vector { + let type_name = type_info::type_name(); + *string::bytes(&type_name) + } + + fun compare_struct(): u8 { + let struct_x_bytes: vector = get_token_info(); + let struct_y_bytes: vector = get_token_info(); + compare(&struct_x_bytes, &struct_y_bytes) + } + + public fun get_smaller_enum(): u8 { + SMALLER + } + + public fun get_greater_enum(): u8 { + GREATER + } + + public fun get_equal_enum(): u8 { + EQUAL + } + + public fun sort_token_type(): bool { + let compare_x_y: u8 = compare_struct(); + assert!(compare_x_y != get_equal_enum(), ErrorTokenPairAleardyExist); + (compare_x_y == get_smaller_enum()) + } +}