From ebdfdca5f93adf1352b0badbe0fe845362c28a00 Mon Sep 17 00:00:00 2001 From: mx819812523 Date: Sun, 12 Jan 2025 19:11:29 +0800 Subject: [PATCH] feat: add liquidity incentive for rooch dex --- .../sources/liquidity_incentive.move | 526 ++++++++++++++++++ apps/rooch_dex/sources/swap.move | 24 +- 2 files changed, 549 insertions(+), 1 deletion(-) create mode 100644 apps/rooch_dex/sources/liquidity_incentive.move diff --git a/apps/rooch_dex/sources/liquidity_incentive.move b/apps/rooch_dex/sources/liquidity_incentive.move new file mode 100644 index 0000000000..1bee1433fc --- /dev/null +++ b/apps/rooch_dex/sources/liquidity_incentive.move @@ -0,0 +1,526 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +module rooch_dex::liquidity_incentive { + + use std::signer::address_of; + use app_admin::admin::AdminCap; + use moveos_std::table; + use rooch_framework::coin; + use moveos_std::table::{Table, new}; + use moveos_std::signer; + use rooch_framework::account_coin_store; + use moveos_std::timestamp::now_seconds; + use rooch_dex::swap_utils; + use rooch_dex::swap::LPToken; + use rooch_framework::coin_store; + use rooch_framework::coin_store::CoinStore; + use moveos_std::object::Object; + use moveos_std::object; + use rooch_framework::coin::Coin; + + #[test_only] + use std::option::none; + #[test_only] + use std::string::utf8; + #[test_only] + use moveos_std::account::create_account_for_testing; + #[test_only] + use moveos_std::object::to_shared; + #[test_only] + use moveos_std::timestamp; + + #[test_only] + use rooch_dex::swap::{init_lp_for_test, TestCoinX, TestCoinY}; + + const ErrorFarmingNotStillFreeze: u64 = 1; + const ErrorFarmingTotalWeightIsZero: u64 = 2; + const ErrorExpDivideByZero: u64 = 3; + const ErrorFarmingNotEnoughAsset: u64 = 4; + const ErrorFarmingTimestampInvalid: u64 = 5; + const ErrorFarmingCalcLastIdxBiggerThanNow: u64 = 6; + const ErrorFarmingNotAlive: u64 = 7; + const ErrorFarmingAliveStateInvalid: u64 = 8; + const ErrorFarmingNotStake: u64 = 9; + + const EXP_MAX_SCALE: u128 = 9; + + ////////////////////////////////////////////////////////////////////// + // Exponential functions + + const EXP_SCALE: u128 = 1000000000000000000;// e18 + + struct Exp has copy, store, drop { + mantissa: u128 + } + + fun exp_direct(num: u128): Exp { + Exp { + mantissa: num + } + } + + fun exp_direct_expand(num: u128): Exp { + Exp { + mantissa: mul_u128(num, EXP_SCALE) + } + } + + + fun mantissa(a: Exp): u128 { + a.mantissa + } + + fun add_exp(a: Exp, b: Exp): Exp { + Exp { + mantissa: add_u128(a.mantissa, b.mantissa) + } + } + + fun exp(num: u128, denom: u128): Exp { + // if overflow move will abort + let scaledNumerator = mul_u128(num, EXP_SCALE); + let rational = div_u128(scaledNumerator, denom); + Exp { + mantissa: rational + } + } + + fun add_u128(a: u128, b: u128): u128 { + a + b + } + + fun sub_u128(a: u128, b: u128): u128 { + a - b + } + + fun mul_u128(a: u128, b: u128): u128 { + if (a == 0 || b == 0) { + return 0 + }; + a * b + } + + fun div_u128(a: u128, b: u128): u128 { + if (b == 0) { + abort ErrorExpDivideByZero + }; + if (a == 0) { + return 0 + }; + a / b + } + + fun truncate(exp: Exp): u128 { + return exp.mantissa / EXP_SCALE + } + + + struct FarmingAsset has key, store { + asset_total_weight: u128, + harvest_index: u128, + last_update_timestamp: u64, + // Release count per seconds + release_per_second: u128, + // Start time, by seconds, user can operate stake only after this timestamp + start_time: u64, + coin_store: Object>, + stake_info: Table>, + // Representing the pool is alive, false: not alive, true: alive. + alive: bool, + } + + /// To store user's asset token + struct Stake has key, store { + asset: Object>>, + asset_weight: u128, + last_harvest_index: u128, + gain: u128, + } + + public entry fun create_pool( + account: &signer, + release_per_second: u128, + coin_amount: u256, + start_time: u64, + admin: &mut Object + ){ + let reward_coin = account_coin_store::withdraw(account, coin_amount); + create_pool_with_coin(release_per_second, reward_coin, start_time, admin) + } + + + /// Add asset pools + public fun create_pool_with_coin( + release_per_second: u128, + coin: Coin, + start_time: u64, + _admin: &mut Object + ) { + let coin_store = coin_store::create_coin_store(); + coin_store::deposit(&mut coin_store, coin); + if (swap_utils::sort_token_type()) { + let farming_asset = object::new(FarmingAsset { + asset_total_weight: 0, + harvest_index: 0, + last_update_timestamp: start_time, + release_per_second, + start_time, + coin_store, + stake_info: new(), + alive: true + }); + object::to_shared(farming_asset) + }else { + let farming_asset = object::new(FarmingAsset { + asset_total_weight: 0, + harvest_index: 0, + last_update_timestamp: start_time, + release_per_second, + start_time, + coin_store, + stake_info: new(), + alive: true + }); + object::to_shared(farming_asset) + }; + } + + public fun modify_parameter( + release_per_second: u128, + alive: bool, + farming_asset_obj: &mut Object>, + _admin: &mut Object + ) { + // Not support to shuttingdown alive state. + assert!(alive, ErrorFarmingAliveStateInvalid); + + let farming_asset = object::borrow_mut>(farming_asset_obj); + + let now_seconds = now_seconds(); + + farming_asset.release_per_second = release_per_second; + farming_asset.last_update_timestamp = now_seconds; + + // if the pool is alive, then update index + if (farming_asset.alive) { + farming_asset.harvest_index = calculate_harvest_index_with_asset( + farming_asset, + now_seconds + ); + }; + farming_asset.alive = alive; + } + + /// Call by stake user, staking amount of asset in order to get yield farming token + public entry fun stake( + signer: &signer, + lp_amount: u256, + farming_asset_obj: &mut Object>, + ) { + let lp_token = account_coin_store::withdraw>(signer, lp_amount); + do_stake(signer, lp_token, farming_asset_obj); + + } + + public fun do_stake( + signer: &signer, + asset: Coin>, + farming_asset_obj: &mut Object>, + ) { + let account = signer::address_of(signer); + let asset_weight = (coin::value(&asset) as u128); + let farming_asset = object::borrow_mut>(farming_asset_obj); + assert!(farming_asset.alive, ErrorFarmingNotAlive); + + // Check locking time + let now_seconds = now_seconds(); + assert!(farming_asset.start_time <= now_seconds, ErrorFarmingNotStillFreeze); + + let time_period = now_seconds - farming_asset.last_update_timestamp; + + if (farming_asset.asset_total_weight <= 0) { + // Stake as first user + let gain = farming_asset.release_per_second * (time_period as u128); + let asset_coin_store = coin_store::create_coin_store>(); + coin_store::deposit(&mut asset_coin_store, asset); + table::add(&mut farming_asset.stake_info, account, Stake { + asset: asset_coin_store, + asset_weight, + last_harvest_index: 0, + gain, + }); + farming_asset.harvest_index = 0; + farming_asset.asset_total_weight = asset_weight; + } else { + let new_harvest_index = calculate_harvest_index_with_asset(farming_asset, now_seconds); + if (!table::contains(&farming_asset.stake_info, account)) { + let asset_coin_store = coin_store::create_coin_store>(); + coin_store::deposit(&mut asset_coin_store, asset); + table::add(&mut farming_asset.stake_info, account, Stake { + asset: asset_coin_store, + asset_weight, + last_harvest_index: new_harvest_index, + gain: 0, + }); + }else { + let stake = table::borrow_mut(&mut farming_asset.stake_info, account); + let period_gain = calculate_withdraw_amount( + new_harvest_index, + stake.last_harvest_index, + stake.asset_weight + ); + stake.gain = stake.gain + period_gain; + stake.asset_weight = stake.asset_weight + asset_weight; + stake.last_harvest_index = new_harvest_index; + coin_store::deposit(&mut stake.asset, asset) + }; + farming_asset.asset_total_weight = farming_asset.asset_total_weight + asset_weight; + farming_asset.harvest_index = new_harvest_index; + }; + farming_asset.last_update_timestamp = now_seconds; + } + + /// Unstake asset from farming pool + public entry fun unstake(signer: &signer, farming_asset_obj: &mut Object>) { + let (lp_token, reward_token) = do_unstake(signer, farming_asset_obj); + account_coin_store::deposit(address_of(signer), lp_token); + account_coin_store::deposit(address_of(signer), reward_token); + } + + public fun do_unstake( + signer: &signer, + farming_asset_obj: &mut Object> + ): (Coin>, Coin){ + + let farming_asset = object::borrow_mut(farming_asset_obj); + assert!(table::contains(&farming_asset.stake_info, signer::address_of(signer)), ErrorFarmingNotStake); + let Stake { last_harvest_index, asset_weight, asset, gain } = + table::remove(&mut farming_asset.stake_info, signer::address_of(signer)); + + let now_seconds = now_seconds(); + let new_harvest_index = calculate_harvest_index_with_asset(farming_asset, now_seconds); + + let period_gain = calculate_withdraw_amount(new_harvest_index, last_harvest_index, asset_weight); + let total_gain = gain + period_gain; + let withdraw_token = coin_store::withdraw(&mut farming_asset.coin_store, (total_gain as u256)); + + assert!(farming_asset.asset_total_weight >= asset_weight, ErrorFarmingNotEnoughAsset); + + // Update farm asset + farming_asset.asset_total_weight = farming_asset.asset_total_weight - asset_weight; + farming_asset.last_update_timestamp = now_seconds; + + if (farming_asset.alive) { + farming_asset.harvest_index = new_harvest_index; + }; + + (coin_store::remove_coin_store(asset), withdraw_token) + } + + /// Harvest yield farming token from stake + public entry fun harvest( + signer: &signer, + farming_asset_obj: &mut Object> + ) { + let reward_token = do_harvest(signer, farming_asset_obj); + account_coin_store::deposit(address_of(signer), reward_token); + } + + public fun do_harvest( + signer: &signer, + farming_asset_obj: &mut Object> + ): Coin { + let farming_asset = object::borrow_mut(farming_asset_obj); + assert!(table::contains(&farming_asset.stake_info, signer::address_of(signer)), ErrorFarmingNotStake); + let now_seconds = now_seconds(); + let new_harvest_index = calculate_harvest_index_with_asset(farming_asset, now_seconds); + let stake = table::borrow_mut(&mut farming_asset.stake_info, signer::address_of(signer)); + let period_gain = calculate_withdraw_amount( + new_harvest_index, + stake.last_harvest_index, + stake.asset_weight + ); + + let total_gain = stake.gain + period_gain; + + let withdraw_token = coin_store::withdraw(&mut farming_asset.coin_store, (total_gain as u256)); + stake.gain = 0; + stake.last_harvest_index = new_harvest_index; + + if (farming_asset.alive) { + farming_asset.harvest_index = new_harvest_index; + }; + farming_asset.last_update_timestamp = now_seconds; + + withdraw_token + } + + /// The user can quering all yield farming amount in any time and scene + public fun query_gov_token_amount( + account: address, + farming_asset_obj: &Object> + ): u128 { + let farming_asset = object::borrow>(farming_asset_obj); + if (!table::contains(&farming_asset.stake_info, account)){ + return 0 + }; + let stake = table::borrow(&farming_asset.stake_info, account); + let now_seconds = now_seconds(); + + let new_harvest_index = calculate_harvest_index_with_asset( + farming_asset, + now_seconds + ); + + let new_gain = calculate_withdraw_amount( + new_harvest_index, + stake.last_harvest_index, + stake.asset_weight + ); + stake.gain + new_gain + } + + /// Query total stake count from yield farming resource + public fun query_total_stake(farming_asset_obj: &Object>): u128{ + let farming_asset = object::borrow>(farming_asset_obj); + farming_asset.asset_total_weight + } + + /// Query stake weight from user staking objects. + public fun query_stake(farming_asset_obj: &Object>, account: address): u128 { + let farming_asset = object::borrow>(farming_asset_obj); + if (!table::contains(&farming_asset.stake_info, account)){ + return 0 + }; + let stake = table::borrow(&farming_asset.stake_info, account); + stake.asset_weight + } + + /// Queyry pool info from pool type + /// return value: (alive, release_per_second, asset_total_weight, harvest_index) + public fun query_info(farming_asset_obj: &Object>): (bool, u128, u128, u128) { + let asset = object::borrow>(farming_asset_obj); + ( + asset.alive, + asset.release_per_second, + asset.asset_total_weight, + asset.harvest_index + ) + } + + /// Update farming asset + fun calculate_harvest_index_with_asset( + farming_asset: &FarmingAsset, + now_seconds: u64 + ): u128 { + // Recalculate harvest index + if (farming_asset.asset_total_weight <= 0) { + calculate_harvest_index_weight_zero( + farming_asset.harvest_index, + farming_asset.last_update_timestamp, + now_seconds, + farming_asset.release_per_second + ) + } else { + calculate_harvest_index( + farming_asset.harvest_index, + farming_asset.asset_total_weight, + farming_asset.last_update_timestamp, + now_seconds, + farming_asset.release_per_second + ) + } + } + + /// There is calculating from harvest index and global parameters without asset_total_weight + public fun calculate_harvest_index_weight_zero( + harvest_index: u128, + last_update_timestamp: u64, + now_seconds: u64, + release_per_second: u128 + ): u128 { + assert!(last_update_timestamp <= now_seconds, ErrorFarmingTimestampInvalid); + let time_period = now_seconds - last_update_timestamp; + let addtion_index = release_per_second * ((time_period as u128)); + harvest_index + mantissa(exp_direct_expand(addtion_index)) + } + + /// There is calculating from harvest index and global parameters + public fun calculate_harvest_index( + harvest_index: u128, + asset_total_weight: u128, + last_update_timestamp: u64, + now_seconds: u64, + release_per_second: u128): u128 { + assert!(asset_total_weight > 0, ErrorFarmingTotalWeightIsZero); + assert!(last_update_timestamp <= now_seconds, ErrorFarmingTimestampInvalid); + + let time_period = now_seconds - last_update_timestamp; + let numr = (release_per_second * (time_period as u128)); + let denom = asset_total_weight; + harvest_index + mantissa(exp(numr, denom)) + } + + /// This function will return a gain index + public fun calculate_withdraw_amount( + harvest_index: u128, + last_harvest_index: u128, + asset_weight: u128 + ): u128 { + assert!( + harvest_index >= last_harvest_index, + ErrorFarmingCalcLastIdxBiggerThanNow + ); + let amount = asset_weight * (harvest_index - last_harvest_index); + truncate(exp_direct(amount)) + } + + #[test_only] + struct TestRewardCoin has key, store{} + + #[test(sender=@0x42)] + fun test_stake(sender: signer) { + let account_b = create_account_for_testing(@0x43); + rooch_framework::genesis::init_for_test(); + let lp_reward_info = coin::register_extend(utf8(b"Test Lp Reward Coin"), utf8(b"TLR"), none(), 8); + let lp_reward_coin = coin::mint(&mut lp_reward_info, 100000); + to_shared(lp_reward_info); + let lp_coin = init_lp_for_test(10000); + let lp_coin2 = coin::extract(&mut lp_coin, 5000); + account_coin_store::deposit(address_of(&sender), lp_coin); + account_coin_store::deposit(address_of(&account_b), lp_coin2); + let coin_store = coin_store::create_coin_store(); + coin_store::deposit(&mut coin_store, lp_reward_coin); + let farming_asset_obj = object::new( + FarmingAsset { + asset_total_weight: 0, + harvest_index: 0, + last_update_timestamp: now_seconds(), + release_per_second: 100, + start_time:now_seconds(), + coin_store, + stake_info: new(), + alive: true + }); + stake(&sender, 100, &mut farming_asset_obj); + let seconds = 100; + timestamp::fast_forward_seconds_for_test(seconds); + let reward_a = query_gov_token_amount(address_of(&sender), &farming_asset_obj); + stake(&account_b, 100, &mut farming_asset_obj); + let total_weight = query_total_stake(&farming_asset_obj); + assert!(total_weight == 200, 1); + assert!(reward_a == 10000, 2); + timestamp::fast_forward_seconds_for_test(seconds); + reward_a = query_gov_token_amount(address_of(&sender), &farming_asset_obj); + assert!(reward_a == 15000, 3); + let reward_b = query_gov_token_amount(address_of(&account_b), &farming_asset_obj); + assert!(reward_b == 5000, 4); + let reward_coin = do_harvest(&sender, &mut farming_asset_obj); + assert!(coin::value(&reward_coin) == 15000, 5); + account_coin_store::deposit(address_of(&sender), reward_coin); + to_shared(farming_asset_obj); + + + } +} \ No newline at end of file diff --git a/apps/rooch_dex/sources/swap.move b/apps/rooch_dex/sources/swap.move index b7a9fdce1a..29b2a564b8 100644 --- a/apps/rooch_dex/sources/swap.move +++ b/apps/rooch_dex/sources/swap.move @@ -19,6 +19,10 @@ module rooch_dex::swap { use moveos_std::object::{Object, ObjectID}; use rooch_framework::coin_store::{CoinStore, balance, deposit, withdraw}; use rooch_framework::coin_store; + #[test_only] + use moveos_std::object::to_shared; + #[test_only] + use rooch_framework::coin::Coin; @@ -131,7 +135,7 @@ module rooch_dex::swap { let coin_info = coin::register_extend>( lp_name, - string::utf8(b"RoochDex-LP"), + string::utf8(b"RDex-LP"), none(), 8, ); @@ -646,4 +650,22 @@ module rooch_dex::swap { token_pair.is_open = status }; } + + #[test_only] + struct TestCoinX has key, store{} + #[test_only] + struct TestCoinY has key, store{} + + #[test_only] + public fun init_lp_for_test(amount: u256) : Coin> { + let coin_info = coin::register_extend>( + string::utf8(b"RoochDex LPs"), + string::utf8(b"RDex-LP"), + none(), + 8, + ); + let lp_coin = coin::mint(&mut coin_info, amount); + to_shared(coin_info); + return lp_coin + } }