diff --git a/Cargo.lock b/Cargo.lock index a6d3f8bb3..974c2f8fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4176,7 +4176,7 @@ dependencies = [ [[package]] name = "hydradx-adapters" -version = "0.6.8" +version = "0.6.9" dependencies = [ "cumulus-pallet-parachain-system", "cumulus-primitives-core", @@ -4199,9 +4199,10 @@ dependencies = [ "pallet-liquidity-mining", "pallet-omnipool", "pallet-omnipool-liquidity-mining", + "pallet-referrals", "pallet-route-executor", "pallet-stableswap", - "pallet-staking 2.1.0", + "pallet-staking 2.1.1", "pallet-transaction-multi-payment", "pallet-uniques", "pallet-xyk", @@ -4222,7 +4223,7 @@ dependencies = [ [[package]] name = "hydradx-runtime" -version = "195.0.0" +version = "196.0.0" dependencies = [ "cumulus-pallet-aura-ext", "cumulus-pallet-dmp-queue", @@ -4288,12 +4289,13 @@ dependencies = [ "pallet-otc", "pallet-preimage", "pallet-proxy", + "pallet-referrals", "pallet-relaychain-info", "pallet-route-executor", "pallet-scheduler", "pallet-session", "pallet-stableswap", - "pallet-staking 2.1.0", + "pallet-staking 2.1.1", "pallet-timestamp", "pallet-tips", "pallet-transaction-multi-payment", @@ -4337,7 +4339,7 @@ dependencies = [ [[package]] name = "hydradx-traits" -version = "2.8.1" +version = "2.8.2" dependencies = [ "frame-support", "impl-trait-for-tuples", @@ -6901,7 +6903,7 @@ dependencies = [ [[package]] name = "pallet-circuit-breaker" -version = "1.1.17" +version = "1.1.18" dependencies = [ "frame-benchmarking", "frame-support", @@ -7641,7 +7643,7 @@ dependencies = [ [[package]] name = "pallet-omnipool" -version = "4.0.1" +version = "4.0.3" dependencies = [ "bitflags", "frame-benchmarking", @@ -7801,6 +7803,27 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-referrals" +version = "1.0.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "hydra-dx-math", + "hydradx-traits", + "orml-tokens", + "orml-traits", + "parity-scale-codec", + "pretty_assertions", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "sp-tracing", +] + [[package]] name = "pallet-relaychain-info" version = "0.3.3" @@ -7934,7 +7957,7 @@ dependencies = [ [[package]] name = "pallet-staking" -version = "2.1.0" +version = "2.1.1" dependencies = [ "frame-benchmarking", "frame-support", @@ -10768,7 +10791,7 @@ dependencies = [ [[package]] name = "runtime-integration-tests" -version = "1.16.4" +version = "1.16.5" dependencies = [ "cumulus-pallet-aura-ext", "cumulus-pallet-dmp-queue", @@ -10820,12 +10843,13 @@ dependencies = [ "pallet-omnipool", "pallet-omnipool-liquidity-mining", "pallet-otc", + "pallet-referrals", "pallet-relaychain-info", "pallet-route-executor", "pallet-scheduler", "pallet-session", "pallet-stableswap", - "pallet-staking 2.1.0", + "pallet-staking 2.1.1", "pallet-sudo", "pallet-timestamp", "pallet-tips", diff --git a/Cargo.toml b/Cargo.toml index 042df7995..30a4ae095 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ members = [ 'pallets/staking', 'pallets/democracy', 'runtime/hydradx/src/evm/evm-utility/macro', + 'pallets/referrals', ] [workspace.dependencies] @@ -76,6 +77,7 @@ warehouse-liquidity-mining = { package = "pallet-liquidity-mining", path = "pall pallet-bonds = { path = "pallets/bonds", default-features = false} pallet-lbp = { path = "pallets/lbp", default-features = false} pallet-xyk = { path = "pallets/xyk", default-features = false} +pallet-referrals = { path = "pallets/referrals", default-features = false} hydra-dx-build-script-utils = { path = "utils/build-script-utils", default-features = false } scraper = { path = "scraper", default-features = false } diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 0d1881a3a..e8a6d0565 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "runtime-integration-tests" -version = "1.16.4" +version = "1.16.5" description = "Integration tests" authors = ["GalacticCouncil"] edition = "2021" @@ -20,6 +20,7 @@ pallet-circuit-breaker = { workspace = true } pallet-omnipool-liquidity-mining = { workspace = true } pallet-bonds = { workspace = true } pallet-stableswap = { workspace = true } +pallet-referrals = { workspace = true } # Warehouse dependencies pallet-asset-registry = { workspace = true } diff --git a/integration-tests/src/dca.rs b/integration-tests/src/dca.rs index 4d2799d0d..b5c230aff 100644 --- a/integration-tests/src/dca.rs +++ b/integration-tests/src/dca.rs @@ -1326,6 +1326,7 @@ mod stableswap { Permill::from_percent(100), AccountId::from(BOB), )); + set_zero_reward_for_referrals(pool_id); do_trade_to_populate_oracle(DAI, HDX, UNITS); set_relaychain_block_number(10); @@ -1426,7 +1427,7 @@ mod stableswap { Permill::from_percent(100), AccountId::from(BOB), )); - + set_zero_reward_for_referrals(pool_id); do_trade_to_populate_oracle(DAI, HDX, UNITS); set_relaychain_block_number(10); @@ -1490,6 +1491,7 @@ mod stableswap { Permill::from_percent(100), AccountId::from(BOB), )); + set_zero_reward_for_referrals(pool_id); do_trade_to_populate_oracle(DAI, HDX, UNITS); set_relaychain_block_number(10); @@ -1578,7 +1580,7 @@ mod stableswap { Permill::from_percent(100), AccountId::from(BOB), )); - + set_zero_reward_for_referrals(pool_id); //Populate oracle with omnipool source assert_ok!(Tokens::set_balance( RawOrigin::Root.into(), @@ -1693,6 +1695,7 @@ mod stableswap { Permill::from_percent(100), AccountId::from(BOB), )); + set_zero_reward_for_referrals(pool_id); //Populate oracle with omnipool source assert_ok!(Tokens::set_balance( @@ -1788,6 +1791,7 @@ mod stableswap { Permill::from_percent(100), AccountId::from(BOB), )); + set_zero_reward_for_referrals(pool_id); //Populate oracle with omnipool source assert_ok!(Tokens::set_balance( @@ -1890,6 +1894,7 @@ mod stableswap { AccountId::from(BOB), )); do_trade_to_populate_oracle(DAI, HDX, UNITS); + set_zero_reward_for_referrals(pool_id); set_relaychain_block_number(10); @@ -2061,7 +2066,7 @@ mod stableswap { Permill::from_percent(100), AccountId::from(BOB), )); - + set_zero_reward_for_referrals(pool_id); do_trade_to_populate_oracle(pool_id, HDX, 100 * UNITS); set_relaychain_block_number(10); @@ -2293,6 +2298,7 @@ mod all_pools { Permill::from_percent(100), AccountId::from(BOB), )); + set_zero_reward_for_referrals(pool_id); do_trade_to_populate_oracle(DAI, HDX, UNITS); //Create xyk and populate oracle @@ -2445,6 +2451,7 @@ mod with_onchain_route { Permill::from_percent(100), AccountId::from(BOB), )); + set_zero_reward_for_referrals(pool_id); do_trade_to_populate_oracle(DAI, HDX, UNITS); set_relaychain_block_number(10); @@ -2555,6 +2562,7 @@ mod with_onchain_route { Permill::from_percent(100), AccountId::from(BOB), )); + set_zero_reward_for_referrals(pool_id); do_trade_to_populate_oracle(DAI, HDX, UNITS); set_relaychain_block_number(10); @@ -2656,6 +2664,7 @@ mod with_onchain_route { Permill::from_percent(100), AccountId::from(BOB), )); + set_zero_reward_for_referrals(DOT); do_trade_to_populate_oracle(DAI, HDX, UNITS); assert_ok!(Currencies::update_balance( @@ -2775,6 +2784,7 @@ mod with_onchain_route { Permill::from_percent(100), AccountId::from(BOB), )); + set_zero_reward_for_referrals(pool_id); do_trade_to_populate_oracle(pool_id, HDX, 10000000 * UNITS); set_relaychain_block_number(10); @@ -2987,8 +2997,8 @@ mod with_onchain_route { assert!(fee > 0, "The treasury did not receive the fee"); //The fee would be 5310255478763 in HDX, so it is less in DOT, which checks out - assert!(fee < 38 * UNITS / 10); - assert!(fee > 37 * UNITS / 10); + assert!(fee < 40 * UNITS / 10); + assert!(fee > 36 * UNITS / 10); assert_balance!(ALICE.into(), HDX, alice_init_hdx_balance + 278060378846663); assert_reserved_balance!(&ALICE.into(), DOT, dca_budget - amount_to_sell - fee); @@ -3172,6 +3182,9 @@ pub fn init_omnipol() { TREASURY_ACCOUNT_INIT_BALANCE, 0, )); + + set_zero_reward_for_referrals(HDX); + set_zero_reward_for_referrals(DAI); } fn init_omnipool_with_oracle_for_block_10() { diff --git a/integration-tests/src/dynamic_fees.rs b/integration-tests/src/dynamic_fees.rs index 1f7eb55b5..4fde05718 100644 --- a/integration-tests/src/dynamic_fees.rs +++ b/integration-tests/src/dynamic_fees.rs @@ -365,6 +365,10 @@ fn init_omnipool() { Permill::from_percent(100), AccountId::from(BOB), )); + set_zero_reward_for_referrals(HDX); + set_zero_reward_for_referrals(DAI); + set_zero_reward_for_referrals(DOT); + set_zero_reward_for_referrals(ETH); } /// This function executes one sell and buy with HDX for all assets in the omnipool. This is necessary to diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs index 1dca8de52..553a10b8d 100644 --- a/integration-tests/src/lib.rs +++ b/integration-tests/src/lib.rs @@ -16,6 +16,7 @@ mod omnipool_liquidity_mining; mod oracle; mod otc; mod polkadot_test_net; +mod referrals; mod router; mod staking; mod transact_call_filter; diff --git a/integration-tests/src/polkadot_test_net.rs b/integration-tests/src/polkadot_test_net.rs index 9ac5e1dad..20615999b 100644 --- a/integration-tests/src/polkadot_test_net.rs +++ b/integration-tests/src/polkadot_test_net.rs @@ -17,10 +17,13 @@ pub use primitives::{constants::chain::CORE_ASSET_ID, AssetId, Balance, Moment}; use cumulus_primitives_core::ParaId; use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder; +use frame_system::RawOrigin; use hex_literal::hex; use hydradx_runtime::evm::WETH_ASSET_LOCATION; +use hydradx_runtime::Referrals; use hydradx_runtime::RuntimeOrigin; use pallet_evm::AddressMapping; +use pallet_referrals::{FeeDistribution, Level}; use polkadot_primitives::v2::{BlockNumber, MAX_CODE_SIZE, MAX_POV_SIZE}; use polkadot_runtime_parachains::configuration::HostConfiguration; use sp_core::H160; @@ -525,6 +528,9 @@ pub fn init_omnipool() { hydradx_runtime::RuntimeOrigin::signed(ALICE.into()), stable_position_id, )); + + set_zero_reward_for_referrals(DAI); + set_zero_reward_for_referrals(HDX); } #[macro_export] @@ -540,3 +546,12 @@ macro_rules! assert_reserved_balance { assert_eq!(Currencies::reserved_balance($asset, &$who), $amount); }}; } + +pub fn set_zero_reward_for_referrals(asset_id: AssetId) { + assert_ok!(Referrals::set_reward_percentage( + RawOrigin::Root.into(), + asset_id, + Level::None, + FeeDistribution::default(), + )); +} diff --git a/integration-tests/src/referrals.rs b/integration-tests/src/referrals.rs new file mode 100644 index 000000000..d74cc32f3 --- /dev/null +++ b/integration-tests/src/referrals.rs @@ -0,0 +1,372 @@ +#![cfg(test)] +use crate::polkadot_test_net::*; +use frame_support::assert_ok; +use frame_system::RawOrigin; +use hydradx_runtime::{Currencies, Omnipool, Referrals, Runtime, RuntimeOrigin, Staking, Tokens}; +use orml_traits::MultiCurrency; +use pallet_referrals::{FeeDistribution, ReferralCode}; +use primitives::AccountId; +use sp_runtime::FixedU128; +use sp_runtime::Permill; +use xcm_emulator::TestExt; + +#[test] +fn registering_a_code_should_charge_registration_fee() { + Hydra::execute_with(|| { + let code = + ReferralCode::<::CodeLength>::truncate_from(b"BALLS69".to_vec()); + let (reg_asset, reg_fee, reg_account) = ::RegistrationFee::get(); + let balance = Currencies::free_balance(reg_asset, ®_account); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE.into()), code)); + let balance_after = Currencies::free_balance(reg_asset, ®_account); + let diff = balance_after - balance; + assert_eq!(diff, reg_fee); + }); +} + +#[test] +fn trading_in_omnipool_should_transfer_portion_of_fee_to_reward_pot() { + Hydra::execute_with(|| { + init_omnipool_with_oracle_for_block_10(); + let code = + ReferralCode::<::CodeLength>::truncate_from(b"BALLS69".to_vec()); + assert_ok!(Referrals::register_code( + RuntimeOrigin::signed(ALICE.into()), + code.clone() + )); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB.into()), code)); + assert_ok!(Omnipool::sell( + RuntimeOrigin::signed(BOB.into()), + HDX, + DAI, + 1_000_000_000_000, + 0 + )); + let pot_balance = Currencies::free_balance(DAI, &Referrals::pot_account_id()); + assert_eq!(pot_balance, 28_540_796_051_302_768); + }); +} + +#[test] +fn trading_in_omnipool_should_increase_referrer_shares() { + Hydra::execute_with(|| { + init_omnipool_with_oracle_for_block_10(); + let code = + ReferralCode::<::CodeLength>::truncate_from(b"BALLS69".to_vec()); + assert_ok!(Referrals::register_code( + RuntimeOrigin::signed(ALICE.into()), + code.clone() + )); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB.into()), code)); + assert_ok!(Omnipool::sell( + RuntimeOrigin::signed(BOB.into()), + HDX, + DAI, + 1_000_000_000_000, + 0 + )); + let referrer_shares = Referrals::account_shares::(ALICE.into()); + assert_eq!(referrer_shares, 128_499_283); + }); +} +#[test] +fn trading_in_omnipool_should_increase_trader_shares() { + Hydra::execute_with(|| { + init_omnipool_with_oracle_for_block_10(); + let code = + ReferralCode::<::CodeLength>::truncate_from(b"BALLS69".to_vec()); + assert_ok!(Referrals::register_code( + RuntimeOrigin::signed(ALICE.into()), + code.clone() + )); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB.into()), code)); + assert_ok!(Omnipool::sell( + RuntimeOrigin::signed(BOB.into()), + HDX, + DAI, + 1_000_000_000_000, + 0 + )); + let trader_shares = Referrals::account_shares::(BOB.into()); + assert_eq!(trader_shares, 256_998_567); + }); +} +#[test] +fn trading_in_omnipool_should_increase_external_shares() { + Hydra::execute_with(|| { + init_omnipool_with_oracle_for_block_10(); + let code = + ReferralCode::<::CodeLength>::truncate_from(b"BALLS69".to_vec()); + assert_ok!(Referrals::register_code( + RuntimeOrigin::signed(ALICE.into()), + code.clone() + )); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB.into()), code)); + assert_ok!(Omnipool::sell( + RuntimeOrigin::signed(BOB.into()), + HDX, + DAI, + 1_000_000_000_000, + 0 + )); + let external_shares = Referrals::account_shares::(Staking::pot_account_id().into()); + assert_eq!(external_shares, 1_067_610_243_609); + }); +} + +#[test] +fn trading_in_omnipool_should_increase_total_shares_correctly() { + Hydra::execute_with(|| { + init_omnipool_with_oracle_for_block_10(); + let code = + ReferralCode::<::CodeLength>::truncate_from(b"BALLS69".to_vec()); + assert_ok!(Referrals::register_code( + RuntimeOrigin::signed(ALICE.into()), + code.clone() + )); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB.into()), code)); + assert_ok!(Omnipool::sell( + RuntimeOrigin::signed(BOB.into()), + HDX, + DAI, + 1_000_000_000_000, + 0 + )); + let total_shares = Referrals::total_shares(); + assert_eq!(total_shares, 1_067_995_741_459); + }); +} + +#[test] +fn claiming_rewards_should_convert_all_assets_to_reward_asset() { + Hydra::execute_with(|| { + init_omnipool_with_oracle_for_block_10(); + let code = + ReferralCode::<::CodeLength>::truncate_from(b"BALLS69".to_vec()); + assert_ok!(Referrals::register_code( + RuntimeOrigin::signed(ALICE.into()), + code.clone() + )); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB.into()), code)); + assert_ok!(Omnipool::sell( + RuntimeOrigin::signed(BOB.into()), + HDX, + DAI, + 1_000_000_000_000, + 0 + )); + let pot_balance = Currencies::free_balance(DAI, &Referrals::pot_account_id()); + assert!(pot_balance > 0); + + assert_ok!(Referrals::claim_rewards(RuntimeOrigin::signed(ALICE.into()))); + let pot_balance = Currencies::free_balance(DAI, &Referrals::pot_account_id()); + assert_eq!(pot_balance, 0); + }); +} + +#[test] +fn trading_hdx_in_omnipool_should_skip_referrals_program() { + Hydra::execute_with(|| { + init_omnipool_with_oracle_for_block_10(); + let code = + ReferralCode::<::CodeLength>::truncate_from(b"BALLS69".to_vec()); + assert_ok!(Referrals::register_code( + RuntimeOrigin::signed(ALICE.into()), + code.clone() + )); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB.into()), code)); + assert_ok!(Omnipool::sell( + RuntimeOrigin::signed(BOB.into()), + DAI, + HDX, + 10_000_000_000_000_000_000, + 0 + )); + let referrer_shares = Referrals::account_shares::(BOB.into()); + assert_eq!(referrer_shares, 0); + }); +} + +#[test] +fn trading_in_omnipool_should_transfer_some_portion_of_fee_when_no_code_linked() { + Hydra::execute_with(|| { + init_omnipool_with_oracle_for_block_10(); + assert_ok!(Omnipool::sell( + RuntimeOrigin::signed(BOB.into()), + HDX, + DAI, + 1_000_000_000_000, + 0 + )); + let pot_balance = Currencies::free_balance(DAI, &Referrals::pot_account_id()); + assert_eq!(pot_balance, 28_540_796_051_302_770); + let external_shares = Referrals::account_shares::(Staking::pot_account_id()); + let total_shares = Referrals::total_shares(); + assert_eq!(total_shares, external_shares); + }); +} + +#[test] +fn trading_in_omnipool_should_use_global_rewards_when_not_set() { + Hydra::execute_with(|| { + init_omnipool_with_oracle_for_block_10(); + let code = + ReferralCode::<::CodeLength>::truncate_from(b"BALLS69".to_vec()); + assert_ok!(Referrals::register_code( + RuntimeOrigin::signed(ALICE.into()), + code.clone() + )); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB.into()), code)); + assert_ok!(Omnipool::sell( + RuntimeOrigin::signed(BOB.into()), + HDX, + DAI, + 1_000_000_000_000, + 0 + )); + let referrer_shares = Referrals::account_shares::(ALICE.into()); + assert_eq!(referrer_shares, 128_499_283); + let trader_shares = Referrals::account_shares::(BOB.into()); + assert_eq!(trader_shares, 256_998_567); + let external_shares = Referrals::account_shares::(Staking::pot_account_id()); + assert_eq!(external_shares, 1_067_610_243_609); + let total_shares = Referrals::total_shares(); + assert_eq!(total_shares, referrer_shares + trader_shares + external_shares); + }); +} + +#[test] +fn trading_in_omnipool_should_use_asset_rewards_when_set() { + Hydra::execute_with(|| { + init_omnipool_with_oracle_for_block_10(); + assert_ok!(Referrals::set_reward_percentage( + RuntimeOrigin::root(), + DAI, + pallet_referrals::Level::Tier0, + FeeDistribution { + referrer: Permill::from_percent(2), + trader: Permill::from_percent(1), + external: Permill::from_percent(10), + } + )); + let code = + ReferralCode::<::CodeLength>::truncate_from(b"BALLS69".to_vec()); + assert_ok!(Referrals::register_code( + RuntimeOrigin::signed(ALICE.into()), + code.clone() + )); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB.into()), code)); + assert_ok!(Omnipool::sell( + RuntimeOrigin::signed(BOB.into()), + HDX, + DAI, + 1_000_000_000_000, + 0 + )); + let referrer_shares = Referrals::account_shares::(ALICE.into()); + assert_eq!(referrer_shares, 51_399_713); + let trader_shares = Referrals::account_shares::(BOB.into()); + assert_eq!(trader_shares, 25_699_856); + let external_shares = Referrals::account_shares::(Staking::pot_account_id()); + assert_eq!(external_shares, 1_066_967_747_190); + let total_shares = Referrals::total_shares(); + assert_eq!(total_shares, referrer_shares + trader_shares + external_shares); + }); +} + +#[test] +fn trading_in_omnipool_should_increase_staking_shares_when_no_code_linked() { + Hydra::execute_with(|| { + init_omnipool_with_oracle_for_block_10(); + assert_ok!(Omnipool::sell( + RuntimeOrigin::signed(BOB.into()), + HDX, + DAI, + 1_000_000_000_000, + 0 + )); + let staking_acc = Staking::pot_account_id(); + let staking_shares = Referrals::account_shares::(staking_acc.into()); + assert_eq!(staking_shares, 1_067_995_741_461); + let total_shares = Referrals::total_shares(); + assert_eq!(total_shares, staking_shares); + }); +} + +fn init_omnipool() { + let native_price = FixedU128::from_inner(1201500000000000); + let stable_price = FixedU128::from_inner(45_000_000_000); + + let native_position_id = hydradx_runtime::Omnipool::next_position_id(); + + assert_ok!(hydradx_runtime::Omnipool::add_token( + hydradx_runtime::RuntimeOrigin::root(), + HDX, + native_price, + Permill::from_percent(10), + AccountId::from(ALICE), + )); + + let stable_position_id = hydradx_runtime::Omnipool::next_position_id(); + + assert_ok!(hydradx_runtime::Omnipool::add_token( + hydradx_runtime::RuntimeOrigin::root(), + DAI, + stable_price, + Permill::from_percent(100), + AccountId::from(ALICE), + )); + + assert_ok!(hydradx_runtime::Omnipool::sacrifice_position( + hydradx_runtime::RuntimeOrigin::signed(ALICE.into()), + native_position_id, + )); + + assert_ok!(hydradx_runtime::Omnipool::sacrifice_position( + hydradx_runtime::RuntimeOrigin::signed(ALICE.into()), + stable_position_id, + )); +} + +fn init_omnipool_with_oracle_for_block_10() { + init_omnipool(); + do_trade_to_populate_oracle(DAI, HDX, UNITS); + set_relaychain_block_number(10); + do_trade_to_populate_oracle(DAI, HDX, UNITS); +} + +fn do_trade_to_populate_oracle(asset_1: AssetId, asset_2: AssetId, amount: Balance) { + assert_ok!(Tokens::set_balance( + RawOrigin::Root.into(), + CHARLIE.into(), + LRNA, + 1000000000000 * UNITS, + 0, + )); + + assert_ok!(Omnipool::sell( + RuntimeOrigin::signed(CHARLIE.into()), + LRNA, + asset_1, + amount, + Balance::MIN + )); + + assert_ok!(Omnipool::sell( + RuntimeOrigin::signed(CHARLIE.into()), + LRNA, + asset_2, + amount, + Balance::MIN + )); + seed_pot_account(); +} + +fn seed_pot_account() { + assert_ok!(Currencies::update_balance( + RawOrigin::Root.into(), + Referrals::pot_account_id(), + HDX, + (10 * UNITS) as i128, + )); +} diff --git a/integration-tests/src/router.rs b/integration-tests/src/router.rs index 97fc4c94a..a71c55049 100644 --- a/integration-tests/src/router.rs +++ b/integration-tests/src/router.rs @@ -625,23 +625,21 @@ mod router_different_pools_tests { assert_eq!( RouterWeightInfo::sell_weight(trades.as_slice()), hydradx_runtime::weights::omnipool::HydraWeight::::router_execution_sell(1, 1) - .checked_add( - &, Runtime> as OmnipoolHooks::< - RuntimeOrigin, - AccountId, - AssetId, - Balance, - >>::on_trade_weight() - ) + .checked_add(&, + ConstU32, + Runtime, + > as OmnipoolHooks::>::on_trade_weight( + )) .unwrap() - .checked_add( - &, Runtime> as OmnipoolHooks::< - RuntimeOrigin, - AccountId, - AssetId, - Balance, - >>::on_liquidity_changed_weight() - ) + .checked_add(&, + ConstU32, + Runtime, + > as OmnipoolHooks::>::on_liquidity_changed_weight( + )) .unwrap() .checked_add(&hydradx_runtime::weights::lbp::HydraWeight::::router_execution_sell(1, 1)) .unwrap() @@ -655,23 +653,21 @@ mod router_different_pools_tests { assert_eq!( RouterWeightInfo::buy_weight(trades.as_slice()), hydradx_runtime::weights::omnipool::HydraWeight::::router_execution_buy(1, 1) - .checked_add( - &, Runtime> as OmnipoolHooks::< - RuntimeOrigin, - AccountId, - AssetId, - Balance, - >>::on_trade_weight() - ) + .checked_add(&, + ConstU32, + Runtime, + > as OmnipoolHooks::>::on_trade_weight( + )) .unwrap() - .checked_add( - &, Runtime> as OmnipoolHooks::< - RuntimeOrigin, - AccountId, - AssetId, - Balance, - >>::on_liquidity_changed_weight() - ) + .checked_add(&, + ConstU32, + Runtime, + > as OmnipoolHooks::>::on_liquidity_changed_weight( + )) .unwrap() .checked_add(&hydradx_runtime::weights::lbp::HydraWeight::::router_execution_buy(1, 1)) .unwrap() diff --git a/pallets/circuit-breaker/Cargo.toml b/pallets/circuit-breaker/Cargo.toml index c942ae40e..dac9d9e92 100644 --- a/pallets/circuit-breaker/Cargo.toml +++ b/pallets/circuit-breaker/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-circuit-breaker" -version = "1.1.17" +version = "1.1.18" authors = ["GalacticCouncil "] edition = "2021" license = "Apache-2.0" diff --git a/pallets/circuit-breaker/src/tests/mock.rs b/pallets/circuit-breaker/src/tests/mock.rs index 81c53b3a8..46a0a69aa 100644 --- a/pallets/circuit-breaker/src/tests/mock.rs +++ b/pallets/circuit-breaker/src/tests/mock.rs @@ -304,7 +304,12 @@ where todo!() } - fn on_trade_fee(_fee_account: AccountId, _asset: AssetId, _amount: Balance) -> Result { + fn on_trade_fee( + _fee_account: AccountId, + _trader: AccountId, + _asset: AssetId, + _amount: Balance, + ) -> Result { Ok(Balance::zero()) } } diff --git a/pallets/omnipool/Cargo.toml b/pallets/omnipool/Cargo.toml index d59a416be..1771f3e56 100644 --- a/pallets/omnipool/Cargo.toml +++ b/pallets/omnipool/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-omnipool" -version = "4.0.1" +version = "4.0.3" authors = ['GalacticCouncil'] edition = "2021" license = "Apache-2.0" diff --git a/pallets/omnipool/src/lib.rs b/pallets/omnipool/src/lib.rs index b7ac14134..4192807ed 100644 --- a/pallets/omnipool/src/lib.rs +++ b/pallets/omnipool/src/lib.rs @@ -404,6 +404,8 @@ pub mod pallet { AssetNotFrozen, /// Configured stable asset cannot be removed from Omnipool. StableAssetCannotBeRemoved, + /// Calculated amount out from sell trade is zero. + ZeroAmountOut, } #[pallet::call] @@ -983,6 +985,11 @@ pub mod pallet { ) .ok_or(ArithmeticError::Overflow)?; + ensure!( + *state_changes.asset_out.delta_reserve > Balance::zero(), + Error::::ZeroAmountOut + ); + ensure!( *state_changes.asset_out.delta_reserve >= min_buy_amount, Error::::BuyLimitNotReached @@ -1076,7 +1083,7 @@ pub mod pallet { Self::update_hdx_subpool_hub_asset(origin, state_changes.hdx_hub_amount)?; - Self::process_trade_fee(asset_out, state_changes.fee.asset_fee)?; + Self::process_trade_fee(&who, asset_out, state_changes.fee.asset_fee)?; Self::deposit_event(Event::SellExecuted { who, @@ -1268,7 +1275,7 @@ pub mod pallet { Self::update_hdx_subpool_hub_asset(origin, state_changes.hdx_hub_amount)?; - Self::process_trade_fee(asset_in, state_changes.fee.asset_fee)?; + Self::process_trade_fee(&who, asset_in, state_changes.fee.asset_fee)?; Self::deposit_event(Event::BuyExecuted { who, @@ -1727,7 +1734,7 @@ impl Pallet { Self::set_asset_state(asset_out, new_asset_out_state); - Self::process_trade_fee(asset_out, state_changes.fee.asset_fee)?; + Self::process_trade_fee(who, asset_out, state_changes.fee.asset_fee)?; Self::deposit_event(Event::SellExecuted { who: who.clone(), @@ -1833,7 +1840,7 @@ impl Pallet { Self::set_asset_state(asset_out, new_asset_out_state); - Self::process_trade_fee(T::HubAssetId::get(), state_changes.fee.asset_fee)?; + Self::process_trade_fee(who, T::HubAssetId::get(), state_changes.fee.asset_fee)?; Self::deposit_event(Event::BuyExecuted { who: who.clone(), @@ -1945,16 +1952,14 @@ impl Pallet { } /// Calls `on_trade_fee` hook and ensures that no more than the fee amount is transferred. - fn process_trade_fee(asset: T::AssetId, amount: Balance) -> DispatchResult { + fn process_trade_fee(trader: &T::AccountId, asset: T::AssetId, amount: Balance) -> DispatchResult { let account = Self::protocol_account(); let original_asset_reserve = T::Currency::free_balance(asset, &account); - let unused = T::OmnipoolHooks::on_trade_fee(account.clone(), asset, amount)?; + let used = T::OmnipoolHooks::on_trade_fee(account.clone(), trader.clone(), asset, amount)?; let asset_reserve = T::Currency::free_balance(asset, &account); - let updated_asset_reserve = asset_reserve.saturating_add(amount.saturating_sub(unused)); - ensure!( - updated_asset_reserve == original_asset_reserve, - Error::::FeeOverdraft - ); + let diff = original_asset_reserve.saturating_sub(asset_reserve); + ensure!(diff <= amount, Error::::FeeOverdraft); + ensure!(diff == used, Error::::FeeOverdraft); Ok(()) } diff --git a/pallets/omnipool/src/provider.rs b/pallets/omnipool/src/provider.rs index b18d88c9c..63a29d6a0 100644 --- a/pallets/omnipool/src/provider.rs +++ b/pallets/omnipool/src/provider.rs @@ -1,7 +1,7 @@ use crate::pallet::Assets; use crate::{Config, Pallet}; use hydradx_traits::pools::SpotPriceProvider; -use sp_runtime::traits::{CheckedMul, Get}; +use sp_runtime::traits::{CheckedMul, Get, One}; use sp_runtime::{FixedPointNumber, FixedU128}; impl SpotPriceProvider for Pallet { @@ -12,6 +12,9 @@ impl SpotPriceProvider for Pallet { } fn spot_price(asset_a: T::AssetId, asset_b: T::AssetId) -> Option { + if asset_a == asset_b { + return Some(FixedU128::one()); + } if asset_a == T::HubAssetId::get() { let asset_b = Self::load_asset_state(asset_b).ok()?; FixedU128::checked_from_rational(asset_b.hub_reserve, asset_b.reserve) diff --git a/pallets/omnipool/src/traits.rs b/pallets/omnipool/src/traits.rs index 7e3d5f56e..0265edd61 100644 --- a/pallets/omnipool/src/traits.rs +++ b/pallets/omnipool/src/traits.rs @@ -5,7 +5,7 @@ use frame_support::traits::Contains; use frame_support::weights::Weight; use hydra_dx_math::ema::EmaPrice; use hydra_dx_math::omnipool::types::AssetStateChange; -use sp_runtime::traits::{CheckedAdd, CheckedMul, Get, Saturating}; +use sp_runtime::traits::{CheckedAdd, CheckedMul, Get, Saturating, Zero}; use sp_runtime::{DispatchError, FixedPointNumber, FixedU128, Permill}; pub struct AssetInfo @@ -57,13 +57,18 @@ where fn on_liquidity_changed_weight() -> Weight; fn on_trade_weight() -> Weight; - /// Returns unused amount - fn on_trade_fee(fee_account: AccountId, asset: AssetId, amount: Balance) -> Result; + /// Returns used amount + fn on_trade_fee( + fee_account: AccountId, + trader: AccountId, + asset: AssetId, + amount: Balance, + ) -> Result; } impl OmnipoolHooks for () where - Balance: Default + Clone, + Balance: Default + Clone + Zero, { type Error = DispatchError; @@ -91,8 +96,13 @@ where Weight::zero() } - fn on_trade_fee(_fee_account: AccountId, _asset: AssetId, amount: Balance) -> Result { - Ok(amount) + fn on_trade_fee( + _fee_account: AccountId, + _trader: AccountId, + _asset: AssetId, + _amount: Balance, + ) -> Result { + Ok(Balance::zero()) } } diff --git a/pallets/referrals/Cargo.toml b/pallets/referrals/Cargo.toml new file mode 100644 index 000000000..de2678fab --- /dev/null +++ b/pallets/referrals/Cargo.toml @@ -0,0 +1,63 @@ +[package] +name = "pallet-referrals" +version = "1.0.0" +authors = ['GalacticCouncil'] +edition = "2021" +license = "Apache-2.0" +homepage = '/~https://github.com/galacticcouncil/hydradx-node' +repository = '/~https://github.com/galacticcouncil/hydradx-node' +description = "HydraDX Referrals pallet" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +# parity +scale-info = { version = "2.3.1", default-features = false, features = ["derive"] } +codec = { default-features = false, features = ["derive"], package = "parity-scale-codec", version = "3.4.0" } + +# HydraDX +hydradx-traits = { workspace = true } +hydra-dx-math = { workspace = true } + +# primitives +sp-runtime = { workspace = true } +sp-std = { workspace = true } +sp-core = { workspace = true } + +# FRAME +frame-support = { workspace = true } +frame-system = { workspace = true } + +# Optional imports for benchmarking +frame-benchmarking = { workspace = true, optional = true } +sp-io = { workspace = true, optional = true } +orml-traits = { workspace = true } + +[dev-dependencies] +sp-io = { workspace = true } +sp-tracing = { workspace = true } +pretty_assertions = "1.2.1" +orml-tokens = { workspace = true } + +[features] +default = ["std"] +std = [ + "codec/std", + "sp-runtime/std", + "sp-std/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "orml-tokens/std", + "frame-benchmarking/std" +] + +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "sp-io", +] +try-runtime = [ "frame-support/try-runtime" ] diff --git a/pallets/referrals/README.md b/pallets/referrals/README.md new file mode 100644 index 000000000..896b53ce1 --- /dev/null +++ b/pallets/referrals/README.md @@ -0,0 +1,27 @@ +# pallet-referrals + +## Referrals pallet + +Support for referrals, referral codes and rewards distribution. + +### Overview + +Referrals give an opportunity to users to earn some rewards from trading activity if the trader +used their referral code to link their account to the referrer account. + +The trader can get back part of the trade fee too if configured. + +Pallet also provides support for volume-based tiering. Referrer can reached higher Level based on the total amount generated by users of the referrer code. +The higher level, the better reward. + +Rewards are accumulated in the pallet's account and if it is not RewardAsset, it is converted to RewardAsset prior to claim. + +//! ### Terminology + +* **Referral code:** a string of certain size that identifies the referrer. Must be alphanumeric and upper case. +* **Referrer:** user that registered a code +* **Trader:** user that does a trade +* **Reward Asset:** id of an asset which rewards are paid in. Usually native asset. + + +License: Apache-2.0 diff --git a/pallets/referrals/src/benchmarking.rs b/pallets/referrals/src/benchmarking.rs new file mode 100644 index 000000000..f76529a89 --- /dev/null +++ b/pallets/referrals/src/benchmarking.rs @@ -0,0 +1,123 @@ +// Copyright (C) 2020-2023 Intergalactic, Limited (GIB). +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; + +use frame_benchmarking::account; +use frame_benchmarking::benchmarks; +use frame_support::traits::tokens::fungibles::{Inspect, Mutate}; +use frame_system::RawOrigin; +use sp_std::vec; + +benchmarks! { + where_clause { where + T::Currency: Mutate, + T::AssetId: From, + } + + register_code{ + let caller: T::AccountId = account("caller", 0, 1); + let code: ReferralCode = vec![b'x'; T::CodeLength::get() as usize].try_into().unwrap(); + let (asset, fee, _) = T::RegistrationFee::get(); + T::Currency::mint_into(asset, &caller, 2 * fee)?; + + }: _(RawOrigin::Signed(caller.clone()), code.clone()) + verify { + let entry = Pallet::::referrer_level(caller.clone()); + assert_eq!(entry, Some((Level::Tier0, 0))); + let c = Pallet::::normalize_code(code); + let entry = Pallet::::referral_account(c); + assert_eq!(entry, Some(caller)); + } + + link_code{ + let caller: T::AccountId = account("caller", 0, 1); + let user: T::AccountId = account("user", 0, 1); + let code: ReferralCode = vec![b'x'; T::CodeLength::get() as usize].try_into().unwrap(); + let (asset, fee, _) = T::RegistrationFee::get(); + T::Currency::mint_into(asset, &caller, 2 * fee)?; + Pallet::::register_code(RawOrigin::Signed(caller.clone()).into(), code.clone())?; + }: _(RawOrigin::Signed(user.clone()), code) + verify { + let entry = Pallet::::linked_referral_account(user); + assert_eq!(entry, Some(caller)); + } + + convert{ + let caller: T::AccountId = account("caller", 0, 1); + let (asset_id, amount) = T::BenchmarkHelper::prepare_convertible_asset_and_amount(); + T::Currency::mint_into(asset_id, &Pallet::::pot_account_id(), amount)?; + PendingConversions::::insert(asset_id,()); + }: _(RawOrigin::Signed(caller), asset_id) + verify { + let count = PendingConversions::::iter().count(); + assert_eq!(count , 0); + let balance = T::Currency::balance(asset_id, &Pallet::::pot_account_id()); + assert_eq!(balance, 0); + } + + claim_rewards{ + let caller: T::AccountId = account("caller", 0, 1); + let code: ReferralCode = vec![b'x'; T::CodeLength::get() as usize].try_into().unwrap(); + let (asset, fee, _) = T::RegistrationFee::get(); + T::Currency::mint_into(asset, &caller, 2 * fee)?; + Pallet::::register_code(RawOrigin::Signed(caller.clone()).into(), code)?; + let caller_balance = T::Currency::balance(T::RewardAsset::get(), &caller); + + // The worst case is when referrer account is updated to the top tier in one call + // So we need to have enough RewardAsset in the pot. And give all the shares to the caller. + let top_tier_volume = T::LevelVolumeAndRewardPercentages::get(&Level::Tier4).0; + T::Currency::mint_into(T::RewardAsset::get(), &Pallet::::pot_account_id(), top_tier_volume + T::SeedNativeAmount::get())?; + Shares::::insert(caller.clone(), 1_000_000_000_000); + TotalShares::::put(1_000_000_000_000); + }: _(RawOrigin::Signed(caller.clone())) + verify { + let count = PendingConversions::::iter().count(); + assert_eq!(count , 0); + let balance = T::Currency::balance(T::RewardAsset::get(), &caller); + assert!(balance > caller_balance); + let (level, total) = Referrer::::get(&caller).expect("correct entry"); + assert_eq!(level, Level::Tier4); + assert_eq!(total, top_tier_volume); + } + + set_reward_percentage{ + let referrer_percentage = Permill::from_percent(40); + let trader_percentage = Permill::from_percent(30); + let external_percentage = Permill::from_percent(30); + }: _(RawOrigin::Root, T::RewardAsset::get(), Level::Tier2, FeeDistribution{referrer: referrer_percentage, trader: trader_percentage, external: external_percentage}) + verify { + let entry = Pallet::::asset_rewards(T::RewardAsset::get(), Level::Tier2); + assert_eq!(entry, Some(FeeDistribution{ + referrer: referrer_percentage, + trader: trader_percentage, + external: external_percentage, + })); + } +} + +#[cfg(test)] +mod tests { + use super::Pallet; + use crate::tests::*; + use frame_benchmarking::impl_benchmark_test_suite; + impl_benchmark_test_suite!( + Pallet, + super::ExtBuilder::default().with_default_volumes().build(), + super::Test + ); +} diff --git a/pallets/referrals/src/lib.rs b/pallets/referrals/src/lib.rs new file mode 100644 index 000000000..d81a3ad83 --- /dev/null +++ b/pallets/referrals/src/lib.rs @@ -0,0 +1,688 @@ +// Copyright (C) 2020-2023 Intergalactic, Limited (GIB). +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Referrals pallet +//! +//! Support for referrals, referral codes and rewards distribution. +//! +//! ## Overview +//! +//! Referrals give an opportunity to users to earn some rewards from trading activity if the trader +//! used their referral code to link their account to the referrer account. +//! +//! The trader can get back part of the trade fee too if configured. +//! +//! Pallet also provides support for volume-based tiering. Referrer can reached higher Level based on the total amount generated by users of the referrer code. +//! The higher level, the better reward. +//! +//! Rewards are accumulated in the pallet's account and if it is not RewardAsset, it is converted to RewardAsset prior to claim. +//! +//! ### Terminology +//! +//! * **Referral code:** a string of certain size that identifies the referrer. Must be alphanumeric and upper case. +//! * **Referrer:** user that registered a code +//! * **Trader:** user that does a trade +//! * **Reward Asset:** id of an asset which rewards are paid in. Usually native asset. +//! + +#![cfg_attr(not(feature = "std"), no_std)] + +pub mod weights; + +#[cfg(any(feature = "runtime-benchmarks", test))] +mod benchmarking; +#[cfg(test)] +mod tests; +pub mod traits; + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::pallet_prelude::{DispatchResult, Get}; +use frame_support::traits::fungibles::Transfer; +use frame_support::{defensive, ensure, transactional, RuntimeDebug}; +use frame_system::{ensure_signed, pallet_prelude::OriginFor}; +use hydradx_traits::price::PriceProvider; +use orml_traits::GetByKey; +use scale_info::TypeInfo; +use sp_core::bounded::BoundedVec; +use sp_core::U256; +use sp_runtime::helpers_128bit::multiply_by_rational_with_rounding; +use sp_runtime::traits::AccountIdConversion; +use sp_runtime::Rounding; +use sp_runtime::{ + traits::{CheckedAdd, Zero}, + ArithmeticError, DispatchError, Permill, +}; + +#[cfg(feature = "runtime-benchmarks")] +pub use crate::traits::BenchmarkHelper; + +pub use pallet::*; + +use weights::WeightInfo; + +pub type Balance = u128; +pub type ReferralCode = BoundedVec; + +const MIN_CODE_LENGTH: usize = 5; + +/// Referrer level. +/// Indicates current level of the referrer to determine which reward percentages are used. +#[derive(Hash, Clone, Copy, Default, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub enum Level { + None, + #[default] + Tier0, + Tier1, + Tier2, + Tier3, + Tier4, +} + +impl Level { + pub fn next_level(&self) -> Self { + match self { + Self::Tier0 => Self::Tier1, + Self::Tier1 => Self::Tier2, + Self::Tier2 => Self::Tier3, + Self::Tier3 => Self::Tier4, + Self::Tier4 => Self::Tier4, + Self::None => Self::None, + } + } + + pub fn is_max_level(&self) -> bool { + *self == Self::Tier4 + } + + pub fn increase(self, amount: Balance) -> Self { + if self.is_max_level() { + self + } else { + let next_level = self.next_level(); + let required = T::LevelVolumeAndRewardPercentages::get(&next_level).0; + if amount >= required { + return next_level.increase::(amount); + } + self + } + } +} + +#[derive(Clone, Copy, Default, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct FeeDistribution { + /// Percentage of the fee that goes to the referrer. + pub referrer: Permill, + /// Percentage of the fee that goes back to the trader. + pub trader: Permill, + /// Percentage of the fee that goes to specific account given by `ExternalAccount` config parameter as reward.r + pub external: Permill, +} + +#[derive(Clone, Debug, PartialEq, Encode, Decode, TypeInfo)] +pub struct AssetAmount { + asset_id: AssetId, + amount: Balance, +} + +impl AssetAmount { + pub fn new(asset_id: AssetId, amount: Balance) -> Self { + Self { asset_id, amount } + } +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use crate::traits::Convert; + use frame_support::pallet_prelude::*; + use frame_support::sp_runtime::ArithmeticError; + use frame_support::traits::fungibles::{Inspect, Transfer}; + use frame_support::PalletId; + use hydra_dx_math::ema::EmaPrice; + use sp_runtime::traits::Zero; + + #[pallet::pallet] + #[pallet::generate_store(pub(crate) trait Store)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Origin that can set asset reward percentages. + type AuthorityOrigin: EnsureOrigin; + + /// Asset type + type AssetId: frame_support::traits::tokens::AssetId + MaybeSerializeDeserialize; + + /// Support for transfers. + type Currency: Transfer; + + /// Support for asset conversion. + type Convert: Convert; + + /// Price provider to use for shares calculation. + type PriceProvider: PriceProvider; + + /// ID of an asset that is used to distribute rewards in. + #[pallet::constant] + type RewardAsset: Get; + + /// Pallet id. Determines account which holds accumulated rewards in various assets. + #[pallet::constant] + type PalletId: Get; + + /// Registration fee details. + /// (ID of an asset which fee is to be paid in, Amount, Beneficiary account) + #[pallet::constant] + type RegistrationFee: Get<(Self::AssetId, Balance, Self::AccountId)>; + + /// Maximum referral code length. + #[pallet::constant] + type CodeLength: Get; + + /// Volume and Global reward percentages for all assets if not specified explicitly for the asset. + type LevelVolumeAndRewardPercentages: GetByKey; + + /// External account that receives some percentage of the fee. Usually something like staking. + type ExternalAccount: Get>; + + /// Seed amount that was sent to the reward pot. + #[pallet::constant] + type SeedNativeAmount: Get; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper: BenchmarkHelper; + } + + /// Referral codes + /// Maps an account to a referral code. + #[pallet::storage] + #[pallet::getter(fn referral_account)] + pub(super) type ReferralCodes = + StorageMap<_, Blake2_128Concat, ReferralCode, T::AccountId>; + + /// Referral accounts + #[pallet::storage] + #[pallet::getter(fn referral_code)] + pub(super) type ReferralAccounts = + StorageMap<_, Blake2_128Concat, T::AccountId, ReferralCode>; + + /// Linked accounts. + /// Maps an account to a referral account. + #[pallet::storage] + #[pallet::getter(fn linked_referral_account)] + pub(super) type LinkedAccounts = StorageMap<_, Blake2_128Concat, T::AccountId, T::AccountId>; + + /// Shares per account. + #[pallet::storage] + #[pallet::getter(fn account_shares)] + pub(super) type Shares = StorageMap<_, Blake2_128Concat, T::AccountId, Balance, ValueQuery>; + + /// Total share issuance. + #[pallet::storage] + #[pallet::getter(fn total_shares)] + pub(super) type TotalShares = StorageValue<_, Balance, ValueQuery>; + + /// Referer level and total accumulated rewards over time. + /// Maps referrer account to (Level, Balance). Level indicates current rewards and Balance is used to unlock next level. + /// Dev note: we use OptionQuery here because this helps to easily determine that an account if referrer account. + #[pallet::storage] + #[pallet::getter(fn referrer_level)] + pub(super) type Referrer = StorageMap<_, Blake2_128Concat, T::AccountId, (Level, Balance), OptionQuery>; + + /// Asset fee distribution rewards information. + /// Maps (asset_id, level) to asset reward percentages. + #[pallet::storage] + #[pallet::getter(fn asset_rewards)] + pub(super) type AssetRewards = + StorageDoubleMap<_, Blake2_128Concat, T::AssetId, Blake2_128Concat, Level, FeeDistribution, OptionQuery>; + + /// Information about assets that are currently in the rewards pot. + /// Used to easily determine list of assets that need to be converted. + #[pallet::storage] + #[pallet::getter(fn pending_conversions)] + pub(super) type PendingConversions = StorageMap<_, Blake2_128Concat, T::AssetId, ()>; + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// Referral code has been registered. + CodeRegistered { + code: ReferralCode, + account: T::AccountId, + }, + /// Referral code has been linked to an account. + CodeLinked { + account: T::AccountId, + code: ReferralCode, + referral_account: T::AccountId, + }, + /// Asset has been converted to RewardAsset. + Converted { + from: AssetAmount, + to: AssetAmount, + }, + /// Rewards claimed. + Claimed { who: T::AccountId, rewards: Balance }, + /// New asset rewards has been set. + AssetRewardsUpdated { + asset_id: T::AssetId, + level: Level, + rewards: FeeDistribution, + }, + /// Referrer reached new level. + LevelUp { who: T::AccountId, level: Level }, + } + + #[pallet::error] + #[cfg_attr(test, derive(PartialEq, Eq))] + pub enum Error { + /// Referral code is too long. + TooLong, + /// Referral code is too short. + TooShort, + /// Referral code contains invalid character. Only alphanumeric characters are allowed. + InvalidCharacter, + /// Referral code already exists. + AlreadyExists, + /// Provided referral code is invalid. Either does not exist or is too long. + InvalidCode, + /// Account is already linked to another referral account. + AlreadyLinked, + /// Nothing in the referral pot account for the asset. + ZeroAmount, + /// Linking an account to the same referral account is not allowed. + LinkNotAllowed, + /// Calculated rewards are more than the fee amount. This can happen if percentages are incorrectly set. + IncorrectRewardCalculation, + /// Given referrer and trader percentages exceeds 100% percent. + IncorrectRewardPercentage, + /// The account has already a code registered. + AlreadyRegistered, + /// Price for given asset pair not found. + PriceNotFound, + /// Minimum trading amount for conversion has not been reached. + ConversionMinTradingAmountNotReached, + /// Zero amount received from conversion. + ConversionZeroAmountReceived, + } + + #[pallet::call] + impl Pallet { + /// Register new referral code. + /// + /// `origin` pays the registration fee. + /// `code` is assigned to the given `account`. + /// + /// Length of the `code` must be at least `MIN_CODE_LENGTH`. + /// Maximum length is limited to `T::CodeLength`. + /// `code` must contain only alfa-numeric characters and all characters will be converted to upper case. + /// + /// Parameters: + /// - `code`: Code to register. Must follow the restrictions. + /// + /// Emits `CodeRegistered` event when successful. + #[pallet::call_index(0)] + #[pallet::weight(::WeightInfo::register_code())] + pub fn register_code(origin: OriginFor, code: ReferralCode) -> DispatchResult { + let who = ensure_signed(origin)?; + ensure!( + ReferralAccounts::::get(&who).is_none(), + Error::::AlreadyRegistered + ); + + ensure!(code.len() >= MIN_CODE_LENGTH, Error::::TooShort); + + ensure!( + code.clone() + .into_inner() + .iter() + .all(|c| char::is_alphanumeric(*c as char)), + Error::::InvalidCharacter + ); + + let code = Self::normalize_code(code); + + ReferralCodes::::mutate(code.clone(), |v| -> DispatchResult { + ensure!(v.is_none(), Error::::AlreadyExists); + + let (fee_asset, fee_amount, beneficiary) = T::RegistrationFee::get(); + T::Currency::transfer(fee_asset, &who, &beneficiary, fee_amount, true)?; + + *v = Some(who.clone()); + Referrer::::insert(&who, (Level::default(), Balance::zero())); + ReferralAccounts::::insert(&who, code.clone()); + Self::deposit_event(Event::CodeRegistered { code, account: who }); + Ok(()) + }) + } + + /// Link a code to an account. + /// + /// `Code` must be valid registered code. Otherwise `InvalidCode` is returned. + /// + /// Signer account is linked to the referral account of the code. + /// + /// Parameters: + /// - `code`: Code to use to link the signer account to. + /// + /// Emits `CodeLinked` event when successful. + #[pallet::call_index(1)] + #[pallet::weight(::WeightInfo::link_code())] + pub fn link_code(origin: OriginFor, code: ReferralCode) -> DispatchResult { + let who = ensure_signed(origin)?; + let code = Self::normalize_code(code); + let ref_account = Self::referral_account(&code).ok_or(Error::::InvalidCode)?; + + LinkedAccounts::::mutate(who.clone(), |v| -> DispatchResult { + ensure!(v.is_none(), Error::::AlreadyLinked); + + ensure!(who != ref_account, Error::::LinkNotAllowed); + + *v = Some(ref_account.clone()); + Self::deposit_event(Event::CodeLinked { + account: who, + code, + referral_account: ref_account, + }); + Ok(()) + })?; + Ok(()) + } + + /// Convert accrued asset amount to reward currency. + /// + /// Parameters: + /// - `asset_id`: Id of an asset to convert to RewardAsset. + /// + /// Emits `Converted` event when successful. + #[pallet::call_index(2)] + #[pallet::weight(::WeightInfo::convert())] + pub fn convert(origin: OriginFor, asset_id: T::AssetId) -> DispatchResult { + ensure_signed(origin)?; + + let asset_balance = T::Currency::balance(asset_id, &Self::pot_account_id()); + ensure!(asset_balance > 0, Error::::ZeroAmount); + + let total_reward_asset = + T::Convert::convert(Self::pot_account_id(), asset_id, T::RewardAsset::get(), asset_balance)?; + + PendingConversions::::remove(asset_id); + + Self::deposit_event(Event::Converted { + from: AssetAmount::new(asset_id, asset_balance), + to: AssetAmount::new(T::RewardAsset::get(), total_reward_asset), + }); + + Ok(()) + } + + /// Claim accumulated rewards + /// + /// IF there is any asset in the reward pot, all is converted to RewardCurrency first. + /// + /// Reward amount is calculated based on the shares of the signer account. + /// + /// if the signer account is referrer account, total accumulated rewards is updated as well as referrer level if reached. + /// + /// Emits `Claimed` event when successful. + #[pallet::call_index(3)] + #[pallet::weight( { + let c = PendingConversions::::iter().count() as u64; + let convert_weight = (::WeightInfo::convert()).saturating_mul(c); + let w = ::WeightInfo::claim_rewards(); + let one_read = T::DbWeight::get().reads(1_u64); + w.saturating_add(convert_weight).saturating_add(one_read) + })] + pub fn claim_rewards(origin: OriginFor) -> DispatchResult { + let who = ensure_signed(origin)?; + for (asset_id, _) in PendingConversions::::iter() { + let asset_balance = T::Currency::balance(asset_id, &Self::pot_account_id()); + let r = T::Convert::convert(Self::pot_account_id(), asset_id, T::RewardAsset::get(), asset_balance); + if let Err(error) = r { + if error == Error::::ConversionMinTradingAmountNotReached.into() + || error == Error::::ConversionZeroAmountReceived.into() + { + // We allow these errors to continue claiming as the current amount of asset that needed to be converted + // has very low impact on the rewards. + } else { + return Err(error); + } + } else { + PendingConversions::::remove(asset_id); + } + } + let shares = Shares::::take(&who); + if shares == Balance::zero() { + return Ok(()); + } + + let reward_reserve = T::Currency::balance(T::RewardAsset::get(), &Self::pot_account_id()); + let reward_reserve = reward_reserve.saturating_sub(T::SeedNativeAmount::get()); + let share_issuance = TotalShares::::get(); + + let rewards = || -> Option { + let shares_hp = U256::from(shares); + let reward_reserve_hp = U256::from(reward_reserve); + let share_issuance_hp = U256::from(share_issuance); + let r = shares_hp + .checked_mul(reward_reserve_hp)? + .checked_div(share_issuance_hp)?; + Balance::try_from(r).ok() + }() + .ok_or(ArithmeticError::Overflow)?; + + // Make sure that we can transfer all the rewards if all shares withdrawn. + let keep_pot_alive = shares != share_issuance; + + T::Currency::transfer( + T::RewardAsset::get(), + &Self::pot_account_id(), + &who, + rewards, + keep_pot_alive, + )?; + TotalShares::::mutate(|v| { + *v = v.saturating_sub(shares); + }); + Referrer::::mutate(who.clone(), |v| { + if let Some((level, total)) = v { + *total = total.saturating_add(rewards); + let new_level = level.increase::(*total); + if *level != new_level { + *level = new_level; + Self::deposit_event(Event::LevelUp { + who: who.clone(), + level: new_level, + }); + } + } + }); + + Self::deposit_event(Event::Claimed { who, rewards }); + Ok(()) + } + + /// Set asset reward percentages + /// + /// Parameters: + /// - `asset_id`: asset id + /// - `level`: level + /// - `rewards`: reward fee percentages + /// + /// Emits `AssetRewardsUpdated` event when successful. + #[pallet::call_index(4)] + #[pallet::weight(::WeightInfo::set_reward_percentage())] + pub fn set_reward_percentage( + origin: OriginFor, + asset_id: T::AssetId, + level: Level, + rewards: FeeDistribution, + ) -> DispatchResult { + T::AuthorityOrigin::ensure_origin(origin)?; + + //ensure that total percentage does not exceed 100% + ensure!( + rewards + .referrer + .checked_add(&rewards.trader) + .ok_or(Error::::IncorrectRewardPercentage)? + .checked_add(&rewards.external) + .is_some(), + Error::::IncorrectRewardPercentage + ); + + AssetRewards::::mutate(asset_id, level, |v| { + *v = Some(rewards); + }); + Self::deposit_event(Event::AssetRewardsUpdated { + asset_id, + level, + rewards, + }); + Ok(()) + } + } + + #[pallet::hooks] + impl Hooks for Pallet { + fn on_idle(_n: T::BlockNumber, remaining_weight: Weight) -> Weight { + let convert_weight = T::WeightInfo::convert(); + if convert_weight.is_zero() { + return Weight::zero(); + } + let one_read = T::DbWeight::get().reads(1u64); + let max_converts = remaining_weight.saturating_sub(one_read).ref_time() / convert_weight.ref_time(); + + for asset_id in PendingConversions::::iter_keys().take(max_converts as usize) { + let asset_balance = T::Currency::balance(asset_id, &Self::pot_account_id()); + let r = T::Convert::convert(Self::pot_account_id(), asset_id, T::RewardAsset::get(), asset_balance); + if r.is_ok() { + PendingConversions::::remove(asset_id); + } + } + convert_weight.saturating_mul(max_converts).saturating_add(one_read) + } + } +} + +impl Pallet { + pub fn pot_account_id() -> T::AccountId { + T::PalletId::get().into_account_truncating() + } + + pub(crate) fn normalize_code(code: ReferralCode) -> ReferralCode { + let r = code.into_inner().iter().map(|v| v.to_ascii_uppercase()).collect(); + ReferralCode::::truncate_from(r) + } + + /// Process trader fee + /// `source`: account to take the fee from + /// `trader`: account that does the trade + /// + /// Returns unused amount on success. + #[transactional] + pub fn process_trade_fee( + source: T::AccountId, + trader: T::AccountId, + asset_id: T::AssetId, + amount: Balance, + ) -> Result { + let Some(price) = T::PriceProvider::get_price(T::RewardAsset::get(), asset_id) else { + // no price, no fun. + return Ok(Balance::zero()); + }; + + let (level, ref_account) = if let Some(acc) = Self::linked_referral_account(&trader) { + if let Some((level, _)) = Self::referrer_level(&acc) { + // Should not really happen, the ref entry should be always there. + (level, Some(acc)) + } else { + defensive!("Referrer details not found"); + return Ok(Balance::zero()); + } + } else { + (Level::None, None) + }; + + // What is asset fee for this level? if not explicitly set, use global parameter. + let rewards = + Self::asset_rewards(asset_id, level).unwrap_or_else(|| T::LevelVolumeAndRewardPercentages::get(&level).1); + + // Rewards + let external_account = T::ExternalAccount::get(); + let referrer_reward = if ref_account.is_some() { + rewards.referrer.mul_floor(amount) + } else { + 0 + }; + let trader_reward = rewards.trader.mul_floor(amount); + let external_reward = if external_account.is_some() { + rewards.external.mul_floor(amount) + } else { + 0 + }; + let total_taken = referrer_reward + .saturating_add(trader_reward) + .saturating_add(external_reward); + ensure!(total_taken <= amount, Error::::IncorrectRewardCalculation); + T::Currency::transfer(asset_id, &source, &Self::pot_account_id(), total_taken, true)?; + + let referrer_shares = if ref_account.is_some() { + multiply_by_rational_with_rounding(referrer_reward, price.n, price.d, Rounding::Down) + .ok_or(ArithmeticError::Overflow)? + } else { + 0 + }; + let trader_shares = multiply_by_rational_with_rounding(trader_reward, price.n, price.d, Rounding::Down) + .ok_or(ArithmeticError::Overflow)?; + let external_shares = if external_account.is_some() { + multiply_by_rational_with_rounding(external_reward, price.n, price.d, Rounding::Down) + .ok_or(ArithmeticError::Overflow)? + } else { + 0 + }; + + TotalShares::::mutate(|v| { + *v = v.saturating_add( + referrer_shares + .saturating_add(trader_shares) + .saturating_add(external_shares), + ); + }); + if let Some(acc) = ref_account { + Shares::::mutate(acc, |v| { + *v = v.saturating_add(referrer_shares); + }); + } + Shares::::mutate(trader, |v| { + *v = v.saturating_add(trader_shares); + }); + if let Some(acc) = external_account { + Shares::::mutate(acc, |v| { + *v = v.saturating_add(external_shares); + }); + } + if asset_id != T::RewardAsset::get() { + PendingConversions::::insert(asset_id, ()); + } + Ok(total_taken) + } +} diff --git a/pallets/referrals/src/tests.rs b/pallets/referrals/src/tests.rs new file mode 100644 index 000000000..0f720f91f --- /dev/null +++ b/pallets/referrals/src/tests.rs @@ -0,0 +1,464 @@ +// This file is part of HydraDX. + +// Copyright (C) 2020-2023 Intergalactic, Limited (GIB). +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod claim; +mod convert; +mod flow; +mod link; +mod mock_amm; +mod register; +mod tiers; +mod trade_fee; + +use crate as pallet_referrals; +use crate::*; + +use std::cell::RefCell; +use std::collections::HashMap; + +use frame_support::{ + construct_runtime, parameter_types, + sp_runtime::traits::Zero, + sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + }, + traits::{ConstU32, ConstU64, Everything, GenesisBuild}, + PalletId, +}; +use sp_core::H256; + +use crate::tests::mock_amm::{Hooks, TradeResult}; +use crate::traits::Convert; +use frame_support::{assert_noop, assert_ok}; +use frame_system::EnsureRoot; +use hydra_dx_math::ema::EmaPrice; +use orml_traits::MultiCurrency; +use orml_traits::{parameter_type_with_key, MultiCurrencyExtended}; +use sp_runtime::helpers_128bit::multiply_by_rational_with_rounding; +use sp_runtime::DispatchError; +use sp_runtime::Rounding; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +pub(crate) type AccountId = u64; +pub(crate) type AssetId = u32; + +pub(crate) const ONE: Balance = 1_000_000_000_000; + +pub const HDX: AssetId = 0; +pub const DAI: AssetId = 2; +pub const DOT: AssetId = 5; + +pub const ALICE: AccountId = 1; +pub const BOB: AccountId = 2; +pub const CHARLIE: AccountId = 3; +pub const TREASURY: AccountId = 400; + +pub(crate) const INITIAL_ALICE_BALANCE: Balance = 1_000 * ONE; + +thread_local! { + pub static CONVERSION_RATE: RefCell> = RefCell::new(HashMap::default()); + pub static TIER_VOLUME: RefCell>> = RefCell::new(HashMap::default()); + pub static TIER_REWARDS: RefCell> = RefCell::new(HashMap::default()); + pub static SEED_AMOUNT: RefCell = RefCell::new(Balance::zero()); + pub static EXTERNAL_ACCOUNT: RefCell> = RefCell::new(None); +} + +construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Referrals: pallet_referrals, + Tokens: orml_tokens, + MockAmm: mock_amm, + } +); + +parameter_types! { + pub const RefarralPalletId: PalletId = PalletId(*b"test_ref"); + pub const CodeLength: u32 = 7; + pub const RegistrationFee: (AssetId,Balance, AccountId) = (HDX, 222 * 1_000_000_000_000, TREASURY) ; + pub const RewardAsset: AssetId = HDX; +} + +pub struct LevelVolumeAndRewards; + +impl GetByKey for LevelVolumeAndRewards { + fn get(level: &Level) -> (Balance, FeeDistribution) { + let c = TIER_VOLUME.with(|v| v.borrow().get(level).copied()); + + let volume = if let Some(l) = c { + l.unwrap() + } else { + // if not explicitly set, we dont care about this in the test + 0 + }; + let rewards = TIER_REWARDS + .with(|v| v.borrow().get(level).copied()) + .unwrap_or_default(); + + (volume, rewards) + } +} + +pub struct SeedAmount; + +impl Get for SeedAmount { + fn get() -> Balance { + SEED_AMOUNT.with(|v| *v.borrow()) + } +} + +pub struct ExtAccount; + +impl Get> for ExtAccount { + fn get() -> Option { + EXTERNAL_ACCOUNT.with(|v| *v.borrow()) + } +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type AuthorityOrigin = EnsureRoot; + type AssetId = AssetId; + type Currency = Tokens; + type Convert = AssetConvert; + type PriceProvider = ConversionPrice; + type RewardAsset = RewardAsset; + type PalletId = RefarralPalletId; + type RegistrationFee = RegistrationFee; + type CodeLength = CodeLength; + type LevelVolumeAndRewardPercentages = LevelVolumeAndRewards; + type ExternalAccount = ExtAccount; + type SeedNativeAmount = SeedAmount; + type WeightInfo = (); + + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = Benchmarking; +} + +impl frame_system::Config for Test { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +parameter_type_with_key! { + pub ExistentialDeposits: |_asset_id: AssetId| -> Balance { + 0 + }; +} + +impl orml_tokens::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type Amount = i128; + type CurrencyId = AssetId; + type WeightInfo = (); + type ExistentialDeposits = ExistentialDeposits; + type CurrencyHooks = (); + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = (); + type DustRemovalWhitelist = Everything; +} + +impl mock_amm::pallet::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AssetId = AssetId; + type TradeHooks = AmmTrader; +} + +pub struct ExtBuilder { + endowed_accounts: Vec<(AccountId, AssetId, Balance)>, + shares: Vec<(AccountId, Balance)>, + tiers: Vec<(AssetId, Level, FeeDistribution)>, + assets: Vec, +} + +impl Default for ExtBuilder { + fn default() -> Self { + CONVERSION_RATE.with(|v| { + v.borrow_mut().clear(); + }); + SEED_AMOUNT.with(|v| { + let mut c = v.borrow_mut(); + *c = 0u128; + }); + TIER_VOLUME.with(|v| { + v.borrow_mut().clear(); + }); + TIER_REWARDS.with(|v| { + v.borrow_mut().clear(); + }); + EXTERNAL_ACCOUNT.with(|v| { + let mut c = v.borrow_mut(); + *c = None; + }); + + Self { + endowed_accounts: vec![(ALICE, HDX, INITIAL_ALICE_BALANCE)], + shares: vec![], + tiers: vec![], + assets: vec![], + } + } +} + +impl ExtBuilder { + pub fn with_endowed_accounts(mut self, accounts: Vec<(AccountId, AssetId, Balance)>) -> Self { + self.endowed_accounts.extend(accounts); + self + } + + pub fn with_shares(mut self, shares: Vec<(AccountId, Balance)>) -> Self { + self.shares.extend(shares); + self + } + + pub fn with_assets(mut self, shares: Vec) -> Self { + self.assets.extend(shares); + self + } + pub fn with_tiers(mut self, shares: Vec<(AssetId, Level, FeeDistribution)>) -> Self { + self.tiers.extend(shares); + self + } + pub fn with_conversion_price(self, pair: (AssetId, AssetId), price: EmaPrice) -> Self { + CONVERSION_RATE.with(|v| { + let mut m = v.borrow_mut(); + m.insert(pair, price); + m.insert((pair.1, pair.0), price.inverted()); + }); + self + } + pub fn with_seed_amount(self, amount: Balance) -> Self { + SEED_AMOUNT.with(|v| { + let mut m = v.borrow_mut(); + *m = amount; + }); + self + } + + pub fn with_tier_volumes(self, volumes: HashMap>) -> Self { + TIER_VOLUME.with(|v| { + v.swap(&RefCell::new(volumes)); + }); + self + } + + pub fn with_global_tier_rewards(self, rewards: HashMap) -> Self { + TIER_REWARDS.with(|v| { + v.swap(&RefCell::new(rewards)); + }); + self + } + + pub fn with_external_account(self, acc: AccountId) -> Self { + EXTERNAL_ACCOUNT.with(|v| { + let mut m = v.borrow_mut(); + *m = Some(acc); + }); + self + } + + #[cfg(feature = "runtime-benchmarks")] + pub fn with_default_volumes(self) -> Self { + let mut volumes = HashMap::new(); + volumes.insert(Level::Tier0, Some(0)); + volumes.insert(Level::Tier1, Some(10_000_000_000_000)); + volumes.insert(Level::Tier2, Some(11_000_000_000_000)); + volumes.insert(Level::Tier3, Some(12_000_000_000_000)); + volumes.insert(Level::Tier4, Some(13_000_000_000_000)); + TIER_VOLUME.with(|v| { + v.swap(&RefCell::new(volumes)); + }); + self + } + + pub fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + + orml_tokens::GenesisConfig:: { + balances: self + .endowed_accounts + .iter() + .flat_map(|(x, asset, amount)| vec![(*x, *asset, *amount)]) + .collect(), + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut r: sp_io::TestExternalities = t.into(); + + r.execute_with(|| { + for (acc, amount) in self.shares.iter() { + Shares::::insert(acc, amount); + TotalShares::::mutate(|v| { + *v = v.saturating_add(*amount); + }); + } + }); + + r.execute_with(|| { + for (asset, level, tier) in self.tiers.iter() { + AssetRewards::::insert(asset, level, tier); + } + }); + r.execute_with(|| { + for asset in self.assets.iter() { + PendingConversions::::insert(asset, ()); + } + }); + r.execute_with(|| { + let seed_amount = SEED_AMOUNT.with(|v| *v.borrow()); + Tokens::update_balance(HDX, &Referrals::pot_account_id(), seed_amount as i128).unwrap(); + }); + + r.execute_with(|| { + System::set_block_number(1); + }); + + r + } +} + +pub fn expect_events(e: Vec) { + e.into_iter().for_each(frame_system::Pallet::::assert_has_event); +} + +pub struct AssetConvert; + +impl Convert for AssetConvert { + type Error = DispatchError; + + fn convert( + who: AccountId, + asset_from: AssetId, + asset_to: AssetId, + amount: Balance, + ) -> Result { + let price = CONVERSION_RATE + .with(|v| v.borrow().get(&(asset_to, asset_from)).copied()) + .ok_or(Error::::InvalidCode)?; + let result = multiply_by_rational_with_rounding(amount, price.n, price.d, Rounding::Down).unwrap(); + Tokens::update_balance(asset_from, &who, -(amount as i128)).unwrap(); + Tokens::update_balance(asset_to, &who, result as i128).unwrap(); + Ok(result) + } +} + +#[macro_export] +macro_rules! assert_balance { + ( $x:expr, $y:expr, $z:expr) => {{ + assert_eq!(Tokens::free_balance($y, &$x), $z); + }}; +} + +pub struct AmmTrader; + +const TRADE_PERCENTAGE: Permill = Permill::from_percent(1); + +impl Hooks for AmmTrader { + fn simulate_trade( + _who: &AccountId, + asset_in: AssetId, + asset_out: AssetId, + amount: Balance, + ) -> Result, DispatchError> { + let price = CONVERSION_RATE + .with(|v| v.borrow().get(&(asset_out, asset_in)).copied()) + .expect("to have a price"); + let amount_out = multiply_by_rational_with_rounding(amount, price.n, price.d, Rounding::Down).unwrap(); + let fee_amount = TRADE_PERCENTAGE.mul_floor(amount_out); + Ok(TradeResult { + amount_in: amount, + amount_out, + fee: fee_amount, + fee_asset: asset_out, + }) + } + + fn on_trade_fee( + source: &AccountId, + trader: &AccountId, + fee_asset: AssetId, + fee: Balance, + ) -> Result<(), DispatchError> { + Referrals::process_trade_fee(*source, *trader, fee_asset, fee)?; + Ok(()) + } +} + +pub struct ConversionPrice; + +impl PriceProvider for ConversionPrice { + type Price = EmaPrice; + + fn get_price(asset_a: AssetId, asset_b: AssetId) -> Option { + if asset_a == asset_b { + return Some(EmaPrice::one()); + } + CONVERSION_RATE.with(|v| v.borrow().get(&(asset_a, asset_b)).copied()) + } +} + +#[cfg(feature = "runtime-benchmarks")] +use crate::traits::BenchmarkHelper; + +#[cfg(feature = "runtime-benchmarks")] +pub struct Benchmarking; + +#[cfg(feature = "runtime-benchmarks")] +impl BenchmarkHelper for Benchmarking { + fn prepare_convertible_asset_and_amount() -> (AssetId, Balance) { + let price = EmaPrice::new(1_000_000_000_000, 1_000_000_000_000); + CONVERSION_RATE.with(|v| { + let mut m = v.borrow_mut(); + m.insert((1234, HDX), price); + m.insert((HDX, 1234), price.inverted()); + }); + + (1234, 1_000_000_000_000) + } +} diff --git a/pallets/referrals/src/tests/claim.rs b/pallets/referrals/src/tests/claim.rs new file mode 100644 index 000000000..0a3470464 --- /dev/null +++ b/pallets/referrals/src/tests/claim.rs @@ -0,0 +1,207 @@ +use crate::tests::*; +use pretty_assertions::assert_eq; + +#[test] +fn claim_rewards_should_work_when_amount_is_zero() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(Referrals::claim_rewards(RuntimeOrigin::signed(BOB))); + }); +} + +#[test] +fn claim_rewards_should_convert_all_assets() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Pallet::::pot_account_id(), DAI, 3_000_000_000_000_000_000), + (Pallet::::pot_account_id(), DOT, 4_000_000_000_000), + ]) + .with_assets(vec![DAI, DOT]) + .with_conversion_price((HDX, DAI), EmaPrice::new(1_000_000_000_000, 1_000_000_000_000_000_000)) + .with_conversion_price((HDX, DOT), EmaPrice::new(1_000_000_000_000, 1_000_000_000_000)) + .build() + .execute_with(|| { + assert_ok!(Referrals::claim_rewards(RuntimeOrigin::signed(BOB))); + // Assert + let acc = Pallet::::pot_account_id(); + let reserve = Tokens::free_balance(HDX, &acc); + assert_eq!(reserve, 7_000_000_000_000); + let reserve = Tokens::free_balance(DOT, &acc); + assert_eq!(reserve, 0); + let reserve = Tokens::free_balance(DAI, &acc); + assert_eq!(reserve, 0); + }); +} + +#[test] +fn claim_rewards_should_remove_assets_from_the_list() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Pallet::::pot_account_id(), DAI, 3_000_000_000_000_000_000), + (Pallet::::pot_account_id(), DOT, 4_000_000_000_000), + ]) + .with_assets(vec![DAI, DOT]) + .with_conversion_price((HDX, DAI), EmaPrice::new(1_000_000_000_000, 1_000_000_000_000_000_000)) + .with_conversion_price((HDX, DOT), EmaPrice::new(1_000_000_000_000, 1_000_000_000_000)) + .build() + .execute_with(|| { + assert_ok!(Referrals::claim_rewards(RuntimeOrigin::signed(BOB))); + // Assert + let count = PendingConversions::::iter().count(); + assert_eq!(count, 0); + }); +} + +#[test] +fn claim_rewards_should_calculate_correct_portion_when_claimed() { + ExtBuilder::default() + .with_endowed_accounts(vec![(Pallet::::pot_account_id(), HDX, 20_000_000_000_000)]) + .with_shares(vec![(BOB, 5_000_000_000_000), (ALICE, 15_000_000_000_000)]) + .build() + .execute_with(|| { + assert_ok!(Referrals::claim_rewards(RuntimeOrigin::signed(BOB))); + // Assert + let reserve = Tokens::free_balance(HDX, &BOB); + assert_eq!(reserve, 5_000_000_000_000); + let reserve = Tokens::free_balance(HDX, &Pallet::::pot_account_id()); + assert_eq!(reserve, 15_000_000_000_000); + }); +} + +#[test] +fn claim_rewards_should_decrease_total_shares_issuance_when_claimed() { + ExtBuilder::default() + .with_endowed_accounts(vec![(Pallet::::pot_account_id(), HDX, 20_000_000_000_000)]) + .with_shares(vec![(BOB, 5_000_000_000_000), (ALICE, 15_000_000_000_000)]) + .build() + .execute_with(|| { + assert_ok!(Referrals::claim_rewards(RuntimeOrigin::signed(BOB))); + // Assert + let reserve = TotalShares::::get(); + assert_eq!(reserve, 15_000_000_000_000); + }); +} + +#[test] +fn claim_rewards_should_reset_account_shares_to_zero() { + ExtBuilder::default() + .with_endowed_accounts(vec![(Pallet::::pot_account_id(), HDX, 20_000_000_000_000)]) + .with_shares(vec![(BOB, 5_000_000_000_000), (ALICE, 15_000_000_000_000)]) + .build() + .execute_with(|| { + assert_ok!(Referrals::claim_rewards(RuntimeOrigin::signed(BOB))); + // Assert + let shares = Shares::::get(BOB); + assert_eq!(shares, 0); + }); +} + +#[test] +fn claim_rewards_should_emit_event_when_successful() { + ExtBuilder::default() + .with_endowed_accounts(vec![(Pallet::::pot_account_id(), HDX, 20_000_000_000_000)]) + .with_shares(vec![(BOB, 5_000_000_000_000), (ALICE, 15_000_000_000_000)]) + .build() + .execute_with(|| { + // Act + assert_ok!(Referrals::claim_rewards(RuntimeOrigin::signed(BOB))); + // Assert + expect_events(vec![Event::Claimed { + who: BOB, + rewards: 5_000_000_000_000, + } + .into()]); + }); +} + +#[test] +fn claim_rewards_update_total_accumulated_for_referrer_account() { + ExtBuilder::default() + .with_endowed_accounts(vec![(Pallet::::pot_account_id(), HDX, 20_000_000_000_000)]) + .with_shares(vec![(BOB, 5_000_000_000_000), (ALICE, 15_000_000_000_000)]) + .build() + .execute_with(|| { + // ARRANGE + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone(),)); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB), code)); + // Act + assert_ok!(Referrals::claim_rewards(RuntimeOrigin::signed(ALICE))); + // Assert + let (_, total) = Referrer::::get(ALICE).unwrap(); + assert_eq!(total, 15_000_000_000_000); + }); +} + +#[test] +fn claim_rewards_should_exclude_seed_amount() { + ExtBuilder::default() + .with_endowed_accounts(vec![(Pallet::::pot_account_id(), HDX, 20_000_000_000_000)]) + .with_shares(vec![(BOB, 5_000_000_000_000), (ALICE, 15_000_000_000_000)]) + .with_seed_amount(100_000_000_000_000) + .build() + .execute_with(|| { + // ARRANGE + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone(),)); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB), code)); + // Act + assert_ok!(Referrals::claim_rewards(RuntimeOrigin::signed(ALICE))); + // Assert + let (_, total) = Referrer::::get(ALICE).unwrap(); + assert_eq!(total, 15_000_000_000_000); + }); +} + +#[test] +fn claim_rewards_should_increase_referrer_level_when_limit_is_reached() { + let mut volumes = HashMap::new(); + volumes.insert(Level::Tier0, Some(0)); + volumes.insert(Level::Tier1, Some(10_000_000_000_000)); + volumes.insert(Level::Tier2, Some(20_000_000_000_000)); + + ExtBuilder::default() + .with_endowed_accounts(vec![(Pallet::::pot_account_id(), HDX, 20_000_000_000_000)]) + .with_shares(vec![(BOB, 5_000_000_000_000), (ALICE, 15_000_000_000_000)]) + .with_tier_volumes(volumes) + .build() + .execute_with(|| { + // ARRANGE + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone(),)); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB), code)); + // Act + assert_ok!(Referrals::claim_rewards(RuntimeOrigin::signed(ALICE))); + // Assert + let (level, total) = Referrer::::get(ALICE).unwrap(); + assert_eq!(level, Level::Tier1); + assert_eq!(total, 15_000_000_000_000); + }); +} + +#[test] +fn claim_rewards_should_increase_referrer_level_directly_to_top_tier_when_limit_is_reached() { + let mut volumes = HashMap::new(); + volumes.insert(Level::Tier0, Some(0)); + volumes.insert(Level::Tier1, Some(10_000_000_000_000)); + volumes.insert(Level::Tier2, Some(11_000_000_000_000)); + volumes.insert(Level::Tier3, Some(12_000_000_000_000)); + volumes.insert(Level::Tier4, Some(13_000_000_000_000)); + + ExtBuilder::default() + .with_endowed_accounts(vec![(Pallet::::pot_account_id(), HDX, 20_000_000_000_000)]) + .with_shares(vec![(BOB, 5_000_000_000_000), (ALICE, 15_000_000_000_000)]) + .with_tier_volumes(volumes) + .build() + .execute_with(|| { + // ARRANGE + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone(),)); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB), code)); + // Act + assert_ok!(Referrals::claim_rewards(RuntimeOrigin::signed(ALICE))); + // Assert + let (level, total) = Referrer::::get(ALICE).unwrap(); + assert_eq!(level, Level::Tier4); + assert_eq!(total, 15_000_000_000_000); + }); +} diff --git a/pallets/referrals/src/tests/convert.rs b/pallets/referrals/src/tests/convert.rs new file mode 100644 index 000000000..7f30f6600 --- /dev/null +++ b/pallets/referrals/src/tests/convert.rs @@ -0,0 +1,87 @@ +use crate::tests::*; +use frame_support::traits::Hooks; +use pretty_assertions::assert_eq; + +#[test] +fn convert_should_fail_when_amount_is_zero() { + ExtBuilder::default().build().execute_with(|| { + // Arrange + assert_noop!( + Referrals::convert(RuntimeOrigin::signed(ALICE), DAI), + Error::::ZeroAmount + ); + }); +} + +#[test] +fn convert_should_convert_all_asset_amount_when_successful() { + ExtBuilder::default() + .with_endowed_accounts(vec![(Pallet::::pot_account_id(), DAI, 1_000_000_000_000_000_000)]) + .with_conversion_price((HDX, DAI), EmaPrice::new(1_000_000_000_000, 1_000_000_000_000_000_000)) + .with_assets(vec![DAI]) + .build() + .execute_with(|| { + // Arrange + assert_ok!(Referrals::convert(RuntimeOrigin::signed(ALICE), DAI)); + // Assert + let balance = Tokens::free_balance(DAI, &Pallet::::pot_account_id()); + assert_eq!(balance, 0); + let balance = Tokens::free_balance(HDX, &Pallet::::pot_account_id()); + assert_eq!(balance, 1_000_000_000_000); + }); +} + +#[test] +fn convert_should_remove_asset_from_the_asset_list() { + ExtBuilder::default() + .with_endowed_accounts(vec![(Pallet::::pot_account_id(), DAI, 1_000_000_000_000_000_000)]) + .with_conversion_price((HDX, DAI), EmaPrice::new(1_000_000_000_000, 1_000_000_000_000_000_000)) + .with_assets(vec![DAI]) + .build() + .execute_with(|| { + // Arrange + assert_ok!(Referrals::convert(RuntimeOrigin::signed(ALICE), DAI)); + // Assert + let entry = PendingConversions::::get(DAI); + assert_eq!(entry, None) + }); +} + +#[test] +fn convert_should_emit_event_when_successful() { + ExtBuilder::default() + .with_endowed_accounts(vec![(Pallet::::pot_account_id(), DAI, 1_000_000_000_000_000_000)]) + .with_conversion_price((HDX, DAI), EmaPrice::new(1_000_000_000_000, 1_000_000_000_000_000_000)) + .with_assets(vec![DAI]) + .build() + .execute_with(|| { + // Arrange + assert_ok!(Referrals::convert(RuntimeOrigin::signed(ALICE), DAI)); + // Assert + expect_events(vec![Event::Converted { + from: AssetAmount::new(DAI, 1_000_000_000_000_000_000), + to: AssetAmount::new(RewardAsset::get(), 1_000_000_000_000), + } + .into()]); + }); +} + +#[test] +fn on_idle_should_convert_all_asset_amount_when_successful() { + ExtBuilder::default() + .with_endowed_accounts(vec![(Pallet::::pot_account_id(), DAI, 1_000_000_000_000_000_000)]) + .with_conversion_price((HDX, DAI), EmaPrice::new(1_000_000_000_000, 1_000_000_000_000_000_000)) + .with_assets(vec![DAI]) + .build() + .execute_with(|| { + // Arrange + Referrals::on_idle(10, 1_000_000_000_000.into()); + // Assert + let balance = Tokens::free_balance(DAI, &Pallet::::pot_account_id()); + assert_eq!(balance, 0); + let balance = Tokens::free_balance(HDX, &Pallet::::pot_account_id()); + assert_eq!(balance, 1_000_000_000_000); + let entry = PendingConversions::::get(DAI); + assert_eq!(entry, None) + }); +} diff --git a/pallets/referrals/src/tests/flow.rs b/pallets/referrals/src/tests/flow.rs new file mode 100644 index 000000000..14f782efb --- /dev/null +++ b/pallets/referrals/src/tests/flow.rs @@ -0,0 +1,143 @@ +use crate::tests::*; +use pretty_assertions::assert_eq; + +#[test] +fn complete_referral_flow_should_work_as_expected() { + let mut volumes = HashMap::new(); + volumes.insert(Level::Tier0, Some(0)); + volumes.insert(Level::Tier1, Some(100_000_000)); + volumes.insert(Level::Tier2, Some(200_000_000)); + volumes.insert(Level::Tier3, Some(300_000_000)); + volumes.insert(Level::Tier4, Some(400_000_000)); + + let bob_initial_hdx = 10_000_000_000_000; + + ExtBuilder::default() + .with_endowed_accounts(vec![ + (BOB, DAI, 2_000_000_000_000_000_000), + (BOB, HDX, bob_initial_hdx), + (CHARLIE, DOT, 2_000_000_000_000), + ]) + .with_conversion_price((HDX, DAI), EmaPrice::new(1_000_000_000_000, 1_000_000_000_000_000_000)) + .with_conversion_price((HDX, DOT), EmaPrice::new(1_000_000_000_000, 500_000_000_000)) + .with_tiers(vec![ + ( + DAI, + Level::Tier0, + FeeDistribution { + referrer: Permill::from_float(0.005), + trader: Permill::from_float(0.002), + external: Permill::from_float(0.002), + }, + ), + ( + DOT, + Level::Tier0, + FeeDistribution { + referrer: Permill::from_float(0.005), + trader: Permill::from_float(0.002), + external: Permill::from_float(0.002), + }, + ), + ( + DAI, + Level::Tier1, + FeeDistribution { + referrer: Permill::from_float(0.03), + trader: Permill::from_float(0.01), + external: Permill::from_float(0.002), + }, + ), + ( + DOT, + Level::Tier1, + FeeDistribution { + referrer: Permill::from_float(0.03), + trader: Permill::from_float(0.01), + external: Permill::from_float(0.002), + }, + ), + ( + HDX, + Level::Tier0, + FeeDistribution { + referrer: Permill::from_float(0.002), + trader: Permill::from_float(0.001), + external: Permill::from_float(0.002), + }, + ), + ( + HDX, + Level::Tier1, + FeeDistribution { + referrer: Permill::from_float(0.03), + trader: Permill::from_float(0.01), + external: Permill::from_float(0.002), + }, + ), + ]) + .with_tier_volumes(volumes) + .build() + .execute_with(|| { + // ARRANGE + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone())); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB), code.clone())); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(CHARLIE), code,)); + // TRADES + assert_ok!(MockAmm::trade(RuntimeOrigin::signed(BOB), HDX, DAI, 1_000_000_000_000)); + assert_ok!(MockAmm::trade( + RuntimeOrigin::signed(BOB), + DAI, + HDX, + 1_000_000_000_000_000_000 + )); + assert_ok!(MockAmm::trade( + RuntimeOrigin::signed(CHARLIE), + HDX, + DOT, + 1_000_000_000_000 + )); + + // Assert shares + let alice_shares = Shares::::get(ALICE); + assert_eq!(alice_shares, 120_000_000); + let bob_shares = Shares::::get(BOB); + assert_eq!(bob_shares, 30_000_000); + let charlie_shares = Shares::::get(CHARLIE); + assert_eq!(charlie_shares, 20_000_000); + let total_shares = TotalShares::::get(); + assert_eq!(total_shares, alice_shares + bob_shares + charlie_shares); + + // CLAIMS + assert_ok!(Referrals::claim_rewards(RuntimeOrigin::signed(CHARLIE),)); + // Assert charlie rewards + let shares = Shares::::get(CHARLIE); + assert_eq!(shares, 0); + let total_shares = TotalShares::::get(); + assert_eq!(total_shares, alice_shares + bob_shares); + let charlie_balance = Tokens::free_balance(HDX, &CHARLIE); + assert_eq!(charlie_balance, 20000000); + + assert_ok!(Referrals::claim_rewards(RuntimeOrigin::signed(BOB),)); + // Assert BOB rewards + let shares = Shares::::get(BOB); + assert_eq!(shares, 0); + let total_shares = TotalShares::::get(); + assert_eq!(total_shares, alice_shares); + let bob_balance = Tokens::free_balance(HDX, &BOB); + assert_eq!(bob_balance, 10_000_000_000_000); + + assert_ok!(Referrals::claim_rewards(RuntimeOrigin::signed(ALICE),)); + // Assert ALICE rewards + let shares = Shares::::get(ALICE); + assert_eq!(shares, 0); + let total_shares = TotalShares::::get(); + assert_eq!(total_shares, 0); + let alice_balance = Tokens::free_balance(HDX, &ALICE); + assert_eq!(alice_balance, 778_000_120_000_000); + let (level, total) = Referrer::::get(ALICE).unwrap(); + assert_eq!(level, Level::Tier1); + assert_eq!(total, 120_000_000); + }); +} diff --git a/pallets/referrals/src/tests/link.rs b/pallets/referrals/src/tests/link.rs new file mode 100644 index 000000000..4e6195821 --- /dev/null +++ b/pallets/referrals/src/tests/link.rs @@ -0,0 +1,106 @@ +use crate::tests::*; +use pretty_assertions::assert_eq; + +#[test] +fn link_code_should_work_when_code_is_valid() { + ExtBuilder::default().build().execute_with(|| { + // Arrange + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone())); + // ACT + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB), code)); + }); +} + +#[test] +fn link_code_should_fail_when_code_does_not_exist() { + ExtBuilder::default().build().execute_with(|| { + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_noop!( + Referrals::link_code(RuntimeOrigin::signed(ALICE), code), + Error::::InvalidCode + ); + }); +} + +#[test] +fn link_code_should_link_correctly_when_code_is_valid() { + ExtBuilder::default().build().execute_with(|| { + // ARRANGE + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone(),)); + + // ACT + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB), code)); + + // ASSERT + let entry = Pallet::::linked_referral_account::(BOB); + assert_eq!(entry, Some(ALICE)); + }); +} + +#[test] +fn link_code_should_fail_when_linking_to_same_acccount() { + ExtBuilder::default().build().execute_with(|| { + // ARRANGE + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone(),)); + + // ACT + assert_noop!( + Referrals::link_code(RuntimeOrigin::signed(ALICE), code), + Error::::LinkNotAllowed + ); + }); +} + +#[test] +fn link_code_should_link_correctly_when_code_is_lowercase() { + ExtBuilder::default().build().execute_with(|| { + // ARRANGE + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code,)); + + // ACT + let code: ReferralCode<::CodeLength> = b"balls69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB), code)); + + // ASSERT + let entry = Pallet::::linked_referral_account::(BOB); + assert_eq!(entry, Some(ALICE)); + }); +} + +#[test] +fn link_code_should_fail_when_account_is_already_linked() { + ExtBuilder::default().build().execute_with(|| { + // ARRANGE + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone(),)); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB), code.clone())); + + // ACT + assert_noop!( + Referrals::link_code(RuntimeOrigin::signed(BOB), code), + Error::::AlreadyLinked + ); + }); +} + +#[test] +fn link_code_should_emit_event_when_successful() { + ExtBuilder::default().build().execute_with(|| { + //ARRANGE + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone())); + // ACT + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB), code.clone())); + // ASSERT + expect_events(vec![Event::CodeLinked { + account: BOB, + code, + referral_account: ALICE, + } + .into()]); + }); +} diff --git a/pallets/referrals/src/tests/mock_amm.rs b/pallets/referrals/src/tests/mock_amm.rs new file mode 100644 index 000000000..82148ff26 --- /dev/null +++ b/pallets/referrals/src/tests/mock_amm.rs @@ -0,0 +1,68 @@ +use frame_support::pallet_prelude::*; +pub use pallet::*; + +type Balance = u128; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_system::{ensure_signed, pallet_prelude::OriginFor}; + + #[pallet::pallet] + #[pallet::generate_store(pub (crate) trait Store)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + type AssetId: frame_support::traits::tokens::AssetId + MaybeSerializeDeserialize; + + type TradeHooks: Hooks; + } + + #[pallet::event] + pub enum Event {} + + #[pallet::error] + pub enum Error {} + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight(Weight::zero())] + pub fn trade( + origin: OriginFor, + asset_in: T::AssetId, + asset_out: T::AssetId, + amount: Balance, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let result = T::TradeHooks::simulate_trade(&who, asset_in, asset_out, amount)?; + T::TradeHooks::on_trade_fee(&who, &who, result.fee_asset, result.fee)?; + Ok(()) + } + } +} + +pub struct TradeResult { + pub amount_in: Balance, + pub amount_out: Balance, + pub fee: Balance, + pub fee_asset: AssetId, +} + +pub trait Hooks { + fn simulate_trade( + who: &AccountId, + asset_in: AssetId, + asset_out: AssetId, + amount: Balance, + ) -> Result, DispatchError>; + fn on_trade_fee( + source: &AccountId, + trader: &AccountId, + fee_asset: AssetId, + fee: Balance, + ) -> Result<(), DispatchError>; +} diff --git a/pallets/referrals/src/tests/register.rs b/pallets/referrals/src/tests/register.rs new file mode 100644 index 000000000..02864f294 --- /dev/null +++ b/pallets/referrals/src/tests/register.rs @@ -0,0 +1,158 @@ +use crate::tests::*; +use pretty_assertions::assert_eq; +use sp_runtime::traits::Zero; + +#[test] +fn register_code_should_work_when_code_is_max_length() { + ExtBuilder::default().build().execute_with(|| { + let code: ReferralCode<::CodeLength> = vec![b'x'; ::CodeLength::get() as usize] + .try_into() + .unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code,)); + }); +} + +#[test] +fn register_code_should_work_when_code_is_min_length() { + ExtBuilder::default().build().execute_with(|| { + let code: ReferralCode<::CodeLength> = vec![b'x'; crate::MIN_CODE_LENGTH].try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code,)); + }); +} + +#[test] +fn register_code_should_fail_when_code_is_too_short() { + ExtBuilder::default().build().execute_with(|| { + for len in 0..MIN_CODE_LENGTH { + let code: ReferralCode<::CodeLength> = vec![b'x'; len].try_into().unwrap(); + assert_noop!( + Referrals::register_code(RuntimeOrigin::signed(ALICE), code), + Error::::TooShort + ); + } + }); +} + +#[test] +fn register_code_should_fail_when_code_already_exists() { + ExtBuilder::default().build().execute_with(|| { + // Arrange + let code: ReferralCode<::CodeLength> = vec![b'x'; ::CodeLength::get() as usize] + .try_into() + .unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone(),)); + // Act + assert_noop!( + Referrals::register_code(RuntimeOrigin::signed(BOB), code), + Error::::AlreadyExists + ); + }); +} + +#[test] +fn register_code_should_fail_when_code_is_lowercase_and_already_exists() { + ExtBuilder::default().build().execute_with(|| { + // Arrange + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code,)); + // Act + let code: ReferralCode<::CodeLength> = b"balls69".to_vec().try_into().unwrap(); + assert_noop!( + Referrals::register_code(RuntimeOrigin::signed(BOB), code), + Error::::AlreadyExists + ); + }); +} + +#[test] +fn register_code_should_fail_when_code_contains_invalid_char() { + ExtBuilder::default().build().execute_with(|| { + let code: ReferralCode<::CodeLength> = b"ABCD?".to_vec().try_into().unwrap(); + assert_noop!( + Referrals::register_code(RuntimeOrigin::signed(ALICE), code), + Error::::InvalidCharacter + ); + }); +} + +#[test] +fn register_code_should_store_account_mapping_to_code_correctly() { + ExtBuilder::default().build().execute_with(|| { + // Arrange + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + // Act + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone())); + // Assert + let entry = Pallet::::referral_account::>(code); + assert_eq!(entry, Some(ALICE)); + }); +} + +#[test] +fn register_code_should_convert_to_upper_case_when_code_is_lower_case() { + ExtBuilder::default().build().execute_with(|| { + // Arrange + let code: ReferralCode<::CodeLength> = b"balls69".to_vec().try_into().unwrap(); + // Act + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone())); + // Assert + let entry = Pallet::::referral_account::>(code.clone()); + assert_eq!(entry, None); + let normalized = Pallet::::normalize_code(code); + let entry = Pallet::::referral_account::>(normalized); + assert_eq!(entry, Some(ALICE)); + }); +} + +#[test] +fn register_code_should_emit_event_when_successful() { + ExtBuilder::default().build().execute_with(|| { + // Arrange + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + // Act + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone())); + // Assert + expect_events(vec![Event::CodeRegistered { code, account: ALICE }.into()]); + }); +} + +#[test] +fn signer_should_pay_the_registration_fee() { + ExtBuilder::default().build().execute_with(|| { + // Arrange + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + // Act + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code)); + // Assert + let (fee_asset, amount, beneficiary) = RegistrationFee::get(); + assert_balance!(ALICE, fee_asset, INITIAL_ALICE_BALANCE - amount); + assert_balance!(beneficiary, fee_asset, amount); + }); +} + +#[test] +fn singer_should_set_default_level_for_referrer() { + ExtBuilder::default().build().execute_with(|| { + // Arrange + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + // Act + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code)); + // Assert + let entry = Pallet::::referrer_level(ALICE); + assert_eq!(entry, Some((Level::default(), Balance::zero()))); + }); +} + +#[test] +fn register_code_should_fail_when_account_has_already_code_registered() { + ExtBuilder::default().build().execute_with(|| { + // Arrange + let code: ReferralCode<::CodeLength> = b"FIRST".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code)); + let code: ReferralCode<::CodeLength> = b"SECOND".to_vec().try_into().unwrap(); + assert_noop!( + Referrals::register_code(RuntimeOrigin::signed(ALICE), code), + Error::::AlreadyRegistered + ); + }); +} diff --git a/pallets/referrals/src/tests/tiers.rs b/pallets/referrals/src/tests/tiers.rs new file mode 100644 index 000000000..3c2ba5e00 --- /dev/null +++ b/pallets/referrals/src/tests/tiers.rs @@ -0,0 +1,92 @@ +use crate::tests::*; +use pretty_assertions::assert_eq; +use sp_runtime::DispatchError::BadOrigin; + +#[test] +fn setting_asset_tier_should_fail_when_not_correct_origin() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + Referrals::set_reward_percentage( + RuntimeOrigin::signed(BOB), + DAI, + Level::Tier0, + FeeDistribution { + referrer: Permill::from_percent(1), + trader: Permill::from_percent(2), + external: Permill::from_percent(2), + } + ), + BadOrigin + ); + }); +} + +#[test] +fn setting_asset_tier_should_correctly_update_storage() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(Referrals::set_reward_percentage( + RuntimeOrigin::root(), + DAI, + Level::Tier0, + FeeDistribution { + referrer: Permill::from_percent(1), + trader: Permill::from_percent(2), + external: Permill::from_percent(3), + } + )); + let d = AssetRewards::::get(DAI, Level::Tier0); + assert_eq!( + d, + Some(FeeDistribution { + referrer: Permill::from_percent(1), + trader: Permill::from_percent(2), + external: Permill::from_percent(3), + }) + ) + }); +} + +#[test] +fn setting_asset_tier_should_fail_when_total_percentage_exceeds_hundred_percent() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + Referrals::set_reward_percentage( + RuntimeOrigin::root(), + DAI, + Level::Tier0, + FeeDistribution { + referrer: Permill::from_percent(60), + trader: Permill::from_percent(40), + external: Permill::from_percent(10), + } + ), + Error::::IncorrectRewardPercentage + ); + }); +} + +#[test] +fn setting_asset_tier_should_emit_event() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(Referrals::set_reward_percentage( + RuntimeOrigin::root(), + DAI, + Level::Tier0, + FeeDistribution { + referrer: Permill::from_percent(1), + trader: Permill::from_percent(2), + external: Permill::from_percent(3), + } + )); + expect_events(vec![Event::AssetRewardsUpdated { + asset_id: DAI, + level: Level::Tier0, + rewards: FeeDistribution { + referrer: Permill::from_percent(1), + trader: Permill::from_percent(2), + external: Permill::from_percent(3), + }, + } + .into()]); + }); +} diff --git a/pallets/referrals/src/tests/trade_fee.rs b/pallets/referrals/src/tests/trade_fee.rs new file mode 100644 index 000000000..941b6baf2 --- /dev/null +++ b/pallets/referrals/src/tests/trade_fee.rs @@ -0,0 +1,350 @@ +use crate::tests::*; +use pretty_assertions::assert_eq; + +#[test] +fn process_trade_fee_should_increased_referrer_shares() { + ExtBuilder::default() + .with_endowed_accounts(vec![(BOB, DAI, 2_000_000_000_000_000_000)]) + .with_conversion_price((HDX, DAI), EmaPrice::new(1_000_000_000_000, 1_000_000_000_000_000_000)) + .with_tiers(vec![( + DAI, + Level::Tier0, + FeeDistribution { + referrer: Permill::from_percent(50), + trader: Permill::zero(), + external: Permill::zero(), + }, + )]) + .build() + .execute_with(|| { + // ARRANGE + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone(),)); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB), code)); + // Act + assert_ok!(MockAmm::trade(RuntimeOrigin::signed(BOB), HDX, DAI, 1_000_000_000_000,)); + // Assert + let shares = Shares::::get(ALICE); + assert_eq!(shares, 5_000_000_000); + }); +} + +#[test] +fn process_trade_fee_should_increased_trader_shares() { + ExtBuilder::default() + .with_endowed_accounts(vec![(BOB, DAI, 2_000_000_000_000_000_000)]) + .with_conversion_price((HDX, DAI), EmaPrice::new(1_000_000_000_000, 1_000_000_000_000_000_000)) + .with_tiers(vec![( + DAI, + Level::Tier0, + FeeDistribution { + referrer: Permill::from_percent(50), + trader: Permill::from_percent(20), + external: Permill::zero(), + }, + )]) + .build() + .execute_with(|| { + // ARRANGE + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone(),)); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB), code)); + // Act + assert_ok!(MockAmm::trade(RuntimeOrigin::signed(BOB), HDX, DAI, 1_000_000_000_000,)); + // Assert + let shares = Shares::::get(BOB); + assert_eq!(shares, 2_000_000_000); + }); +} + +#[test] +fn process_trade_fee_should_increased_total_share_issuance() { + ExtBuilder::default() + .with_endowed_accounts(vec![(BOB, DAI, 2_000_000_000_000_000_000)]) + .with_conversion_price((HDX, DAI), EmaPrice::new(1_000_000_000_000, 1_000_000_000_000_000_000)) + .with_tiers(vec![( + DAI, + Level::Tier0, + FeeDistribution { + referrer: Permill::from_percent(50), + trader: Permill::from_percent(20), + external: Permill::zero(), + }, + )]) + .build() + .execute_with(|| { + // ARRANGE + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone(),)); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB), code)); + // Act + assert_ok!(MockAmm::trade(RuntimeOrigin::signed(BOB), HDX, DAI, 1_000_000_000_000,)); + // Assert + let shares = TotalShares::::get(); + assert_eq!(shares, 2_000_000_000 + 5_000_000_000); + }); +} + +#[test] +fn process_trade_fee_should_fail_when_taken_amount_is_greater_than_fee_amount() { + ExtBuilder::default() + .with_endowed_accounts(vec![(BOB, DAI, 2_000_000_000_000_000_000)]) + .with_conversion_price((HDX, DAI), EmaPrice::new(1_000_000_000_000, 1_000_000_000_000_000_000)) + .with_tiers(vec![( + DAI, + Level::Tier0, + FeeDistribution { + referrer: Permill::from_percent(50), + trader: Permill::from_percent(70), + external: Permill::zero(), + }, + )]) + .build() + .execute_with(|| { + // ARRANGE + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone(),)); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB), code)); + // Act + assert_noop!( + MockAmm::trade(RuntimeOrigin::signed(BOB), HDX, DAI, 1_000_000_000_000,), + Error::::IncorrectRewardCalculation + ); + }); +} + +#[test] +fn process_trade_should_not_increase_shares_when_trader_does_not_have_linked_account() { + ExtBuilder::default() + .with_conversion_price((HDX, DAI), EmaPrice::new(1_000_000_000_000, 1_000_000_000_000_000_000)) + .with_shares(vec![(BOB, 1_000_000_000_000)]) + .build() + .execute_with(|| { + // ARRANGE + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code,)); + // Assert + assert_ok!(MockAmm::trade( + RuntimeOrigin::signed(ALICE), + HDX, + DAI, + 1_000_000_000_000, + )); + let shares = Shares::::get(ALICE); + assert_eq!(shares, 0); + }); +} + +#[test] +fn process_trade_fee_should_add_asset_to_asset_list() { + ExtBuilder::default() + .with_endowed_accounts(vec![(BOB, DAI, 2_000_000_000_000_000_000)]) + .with_conversion_price((HDX, DAI), EmaPrice::new(1_000_000_000_000, 1_000_000_000_000_000_000)) + .with_tiers(vec![( + DAI, + Level::Tier0, + FeeDistribution { + referrer: Permill::from_percent(50), + trader: Permill::from_percent(20), + external: Permill::zero(), + }, + )]) + .build() + .execute_with(|| { + // ARRANGE + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone(),)); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB), code)); + // Act + assert_ok!(MockAmm::trade(RuntimeOrigin::signed(BOB), HDX, DAI, 1_000_000_000_000,)); + // Assert + let asset = PendingConversions::::get(DAI); + assert_eq!(asset, Some(())); + }); +} + +#[test] +fn process_trade_fee_should_not_add_reward_asset_to_asset_list() { + ExtBuilder::default() + .with_endowed_accounts(vec![(BOB, HDX, 2_000_000_000_000)]) + .with_conversion_price((HDX, DAI), EmaPrice::new(1_000_000_000_000, 1_000_000_000_000_000_000)) + .with_tiers(vec![( + DAI, + Level::Tier0, + FeeDistribution { + referrer: Permill::from_percent(50), + trader: Permill::from_percent(20), + external: Permill::zero(), + }, + )]) + .build() + .execute_with(|| { + // ARRANGE + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone(),)); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB), code)); + // Act + assert_ok!(MockAmm::trade(RuntimeOrigin::signed(BOB), DAI, HDX, 1_000_000_000_000,)); + // Assert + let asset = PendingConversions::::get(HDX); + assert_eq!(asset, None); + }); +} + +#[test] +fn process_trade_fee_should_increase_external_account_shares_when_trader_has_no_code_linked() { + let mut none_rewards = HashMap::new(); + none_rewards.insert( + Level::None, + FeeDistribution { + referrer: Default::default(), + trader: Default::default(), + external: Permill::from_percent(50), + }, + ); + + ExtBuilder::default() + .with_endowed_accounts(vec![(BOB, DAI, 2_000_000_000_000_000_000)]) + .with_conversion_price((HDX, DAI), EmaPrice::new(1_000_000_000_000, 1_000_000_000_000_000_000)) + .with_global_tier_rewards(none_rewards) + .with_external_account(12345) + .build() + .execute_with(|| { + // Act + assert_ok!(MockAmm::trade(RuntimeOrigin::signed(BOB), HDX, DAI, 1_000_000_000_000)); + // Assert + let shares = Shares::::get(12345); + assert_eq!(shares, 5_000_000_000); + let shares = TotalShares::::get(); + assert_eq!(shares, 5_000_000_000); + }); +} + +#[test] +fn process_trade_fee_should_transfer_fee_to_pot_when_no_code_linked() { + let mut none_rewards = HashMap::new(); + none_rewards.insert( + Level::None, + FeeDistribution { + referrer: Default::default(), + trader: Default::default(), + external: Permill::from_percent(50), + }, + ); + + ExtBuilder::default() + .with_endowed_accounts(vec![(BOB, DAI, 2_000_000_000_000_000_000)]) + .with_conversion_price((HDX, DAI), EmaPrice::new(1_000_000_000_000, 1_000_000_000_000_000_000)) + .with_global_tier_rewards(none_rewards) + .with_external_account(12345) + .build() + .execute_with(|| { + // Act + assert_ok!(MockAmm::trade(RuntimeOrigin::signed(BOB), HDX, DAI, 1_000_000_000_000)); + // Assert + let reserve = Tokens::free_balance(DAI, &Referrals::pot_account_id()); + assert_eq!(reserve, 5_000_000_000_000_000); + }); +} + +#[test] +fn process_trade_fee_should_reward_all_parties_based_on_global_config_when_asset_not_set_explicitly() { + let mut global_rewards = HashMap::new(); + global_rewards.insert( + Level::None, + FeeDistribution { + referrer: Default::default(), + trader: Default::default(), + external: Permill::from_percent(50), + }, + ); + global_rewards.insert( + Level::Tier0, + FeeDistribution { + referrer: Permill::from_percent(5), + trader: Permill::from_percent(5), + external: Permill::from_percent(40), + }, + ); + ExtBuilder::default() + .with_endowed_accounts(vec![(BOB, HDX, 2_000_000_000_000)]) + .with_conversion_price((HDX, DAI), EmaPrice::new(1_000_000_000_000, 1_000_000_000_000_000_000)) + .with_global_tier_rewards(global_rewards) + .with_external_account(12345) + .build() + .execute_with(|| { + // ARRANGE + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone(),)); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB), code)); + // Act + assert_ok!(MockAmm::trade( + RuntimeOrigin::signed(BOB), + DAI, + HDX, + 1_000_000_000_000_000_000 + )); + // Assert + let referrer_shares = Shares::::get(ALICE); + assert_eq!(referrer_shares, 500_000_000); + let trader_shares = Shares::::get(BOB); + assert_eq!(trader_shares, 500_000_000); + let external_shares = Shares::::get(12345); + assert_eq!(external_shares, 4_000_000_000); + let shares = TotalShares::::get(); + assert_eq!(shares, 5_000_000_000); + }); +} + +#[test] +fn process_trade_fee_should_use_configured_asset_instead_of_global_when_set() { + let mut global_rewards = HashMap::new(); + global_rewards.insert( + Level::None, + FeeDistribution { + referrer: Default::default(), + trader: Default::default(), + external: Permill::from_percent(50), + }, + ); + global_rewards.insert( + Level::Tier0, + FeeDistribution { + referrer: Permill::from_percent(5), + trader: Permill::from_percent(5), + external: Permill::from_percent(40), + }, + ); + ExtBuilder::default() + .with_endowed_accounts(vec![(BOB, DAI, 2_000_000_000_000_000_000)]) + .with_conversion_price((HDX, DAI), EmaPrice::new(1_000_000_000_000, 1_000_000_000_000_000_000)) + .with_tiers(vec![( + DAI, + Level::Tier0, + FeeDistribution { + referrer: Permill::from_percent(10), + trader: Permill::from_percent(5), + external: Permill::from_percent(30), + }, + )]) + .with_global_tier_rewards(global_rewards) + .with_external_account(12345) + .build() + .execute_with(|| { + // ARRANGE + let code: ReferralCode<::CodeLength> = b"BALLS69".to_vec().try_into().unwrap(); + assert_ok!(Referrals::register_code(RuntimeOrigin::signed(ALICE), code.clone(),)); + assert_ok!(Referrals::link_code(RuntimeOrigin::signed(BOB), code)); + // Act + assert_ok!(MockAmm::trade(RuntimeOrigin::signed(BOB), HDX, DAI, 1_000_000_000_000)); + // Assert + let referrer_shares = Shares::::get(ALICE); + assert_eq!(referrer_shares, 1_000_000_000); + let trader_shares = Shares::::get(BOB); + assert_eq!(trader_shares, 500_000_000); + let external_shares = Shares::::get(12345); + assert_eq!(external_shares, 3_000_000_000); + let shares = TotalShares::::get(); + assert_eq!(shares, 3_000_000_000 + 1_000_000_000 + 500_000_000); + }); +} diff --git a/pallets/referrals/src/traits.rs b/pallets/referrals/src/traits.rs new file mode 100644 index 000000000..300cc6403 --- /dev/null +++ b/pallets/referrals/src/traits.rs @@ -0,0 +1,13 @@ +pub trait Convert { + type Error; + + fn convert(who: AccountId, asset_from: AssetId, asset_to: AssetId, amount: Balance) + -> Result; +} + +#[cfg(feature = "runtime-benchmarks")] +pub trait BenchmarkHelper { + // Should prepare everything that provides price for selected asset + // Amount returned is minted into pot account in benchmarks. + fn prepare_convertible_asset_and_amount() -> (AssetId, Balance); +} diff --git a/pallets/referrals/src/weights.rs b/pallets/referrals/src/weights.rs new file mode 100644 index 000000000..3cf7f4557 --- /dev/null +++ b/pallets/referrals/src/weights.rs @@ -0,0 +1,243 @@ +// This file is part of HydraDX. + +// Copyright (C) 2020-2023 Intergalactic, Limited (GIB). +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for pallet_referrals +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-12-12, STEPS: 5, REPEAT: 20, LOW RANGE: [], HIGH RANGE: [] +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// target/release/hydradx +// benchmark +// pallet +// --pallet=pallet_referrals +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --chain=dev +// --extrinsic=* +// --steps=5 +// --repeat=20 +// --output +// referrals.rs +// --template +// .maintain/pallet-weight-template-no-back.hbs + +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(clippy::unnecessary_cast)] + +use frame_support::{ + traits::Get, + weights::{constants::RocksDbWeight, Weight}, +}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_bonds. +pub trait WeightInfo { + fn register_code() -> Weight; + fn link_code() -> Weight; + fn convert() -> Weight; + fn claim_rewards() -> Weight; + fn set_reward_percentage() -> Weight; +} + +/// Weights for pallet_referrals using the hydraDX node and recommended hardware. +pub struct HydraWeight(PhantomData); + +impl WeightInfo for HydraWeight { + // Storage: Referrals ReferralCodes (r:1 w:1) + // Proof: Referrals ReferralCodes (max_values: None, max_size: Some(56), added: 2531, mode: MaxEncodedLen) + // Storage: System Account (r:2 w:2) + // Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + // Storage: Referrals Referrer (r:0 w:1) + // Proof: Referrals Referrer (max_values: None, max_size: Some(65), added: 2540, mode: MaxEncodedLen) + fn register_code() -> Weight { + // Minimum execution time: 61_000 nanoseconds. + Weight::from_ref_time(62_000_000 as u64) + .saturating_add(T::DbWeight::get().reads(3 as u64)) + .saturating_add(T::DbWeight::get().writes(4 as u64)) + } + // Storage: Referrals ReferralCodes (r:1 w:0) + // Proof: Referrals ReferralCodes (max_values: None, max_size: Some(56), added: 2531, mode: MaxEncodedLen) + // Storage: Referrals LinkedAccounts (r:1 w:1) + // Proof: Referrals LinkedAccounts (max_values: None, max_size: Some(80), added: 2555, mode: MaxEncodedLen) + fn link_code() -> Weight { + // Minimum execution time: 26_000 nanoseconds. + Weight::from_ref_time(27_000_000 as u64) + .saturating_add(T::DbWeight::get().reads(2 as u64)) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + } + // Storage: Tokens Accounts (r:2 w:2) + // Proof: Tokens Accounts (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) + // Storage: System Account (r:2 w:2) + // Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + // Storage: Omnipool Assets (r:2 w:2) + // Proof: Omnipool Assets (max_values: None, max_size: Some(85), added: 2560, mode: MaxEncodedLen) + // Storage: Omnipool HubAssetImbalance (r:1 w:1) + // Proof: Omnipool HubAssetImbalance (max_values: Some(1), max_size: Some(17), added: 512, mode: MaxEncodedLen) + // Storage: DynamicFees AssetFee (r:1 w:0) + // Proof: DynamicFees AssetFee (max_values: None, max_size: Some(24), added: 2499, mode: MaxEncodedLen) + // Storage: EmaOracle Oracles (r:1 w:0) + // Proof: EmaOracle Oracles (max_values: None, max_size: Some(177), added: 2652, mode: MaxEncodedLen) + // Storage: AssetRegistry Assets (r:1 w:0) + // Proof: AssetRegistry Assets (max_values: None, max_size: Some(87), added: 2562, mode: MaxEncodedLen) + // Storage: EmaOracle Accumulator (r:1 w:1) + // Proof: EmaOracle Accumulator (max_values: Some(1), max_size: Some(5921), added: 6416, mode: MaxEncodedLen) + // Storage: CircuitBreaker AllowedTradeVolumeLimitPerAsset (r:2 w:2) + // Proof: CircuitBreaker AllowedTradeVolumeLimitPerAsset (max_values: None, max_size: Some(68), added: 2543, mode: MaxEncodedLen) + // Storage: CircuitBreaker TradeVolumeLimitPerAsset (r:2 w:0) + // Proof: CircuitBreaker TradeVolumeLimitPerAsset (max_values: None, max_size: Some(28), added: 2503, mode: MaxEncodedLen) + // Storage: CircuitBreaker LiquidityAddLimitPerAsset (r:1 w:0) + // Proof: CircuitBreaker LiquidityAddLimitPerAsset (max_values: None, max_size: Some(29), added: 2504, mode: MaxEncodedLen) + // Storage: CircuitBreaker AllowedAddLiquidityAmountPerAsset (r:1 w:1) + // Proof: CircuitBreaker AllowedAddLiquidityAmountPerAsset (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen) + // Storage: CircuitBreaker LiquidityRemoveLimitPerAsset (r:1 w:0) + // Proof: CircuitBreaker LiquidityRemoveLimitPerAsset (max_values: None, max_size: Some(29), added: 2504, mode: MaxEncodedLen) + // Storage: CircuitBreaker AllowedRemoveLiquidityAmountPerAsset (r:1 w:1) + // Proof: CircuitBreaker AllowedRemoveLiquidityAmountPerAsset (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen) + // Storage: Referrals LinkedAccounts (r:1 w:0) + // Proof: Referrals LinkedAccounts (max_values: None, max_size: Some(80), added: 2555, mode: MaxEncodedLen) + // Storage: Staking Staking (r:1 w:0) + // Proof: Staking Staking (max_values: Some(1), max_size: Some(48), added: 543, mode: MaxEncodedLen) + // Storage: MultiTransactionPayment AccountCurrencyMap (r:0 w:1) + // Proof: MultiTransactionPayment AccountCurrencyMap (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen) + // Storage: Referrals Assets (r:0 w:1) + // Proof: Referrals Assets (max_values: None, max_size: Some(20), added: 2495, mode: MaxEncodedLen) + fn convert() -> Weight { + // Minimum execution time: 304_000 nanoseconds. + Weight::from_ref_time(307_000_000 as u64) + .saturating_add(T::DbWeight::get().reads(21 as u64)) + .saturating_add(T::DbWeight::get().writes(14 as u64)) + } + // Storage: Referrals Assets (r:1 w:0) + // Proof: Referrals Assets (max_values: None, max_size: Some(20), added: 2495, mode: MaxEncodedLen) + // Storage: Referrals Shares (r:1 w:1) + // Proof: Referrals Shares (max_values: None, max_size: Some(64), added: 2539, mode: MaxEncodedLen) + // Storage: System Account (r:2 w:2) + // Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + // Storage: Referrals TotalShares (r:1 w:1) + // Proof: Referrals TotalShares (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + // Storage: Referrals Referrer (r:1 w:1) + // Proof: Referrals Referrer (max_values: None, max_size: Some(65), added: 2540, mode: MaxEncodedLen) + fn claim_rewards() -> Weight { + // Minimum execution time: 73_000 nanoseconds. + Weight::from_ref_time(74_000_000 as u64) + .saturating_add(T::DbWeight::get().reads(6 as u64)) + .saturating_add(T::DbWeight::get().writes(5 as u64)) + } + // Storage: Referrals AssetTier (r:1 w:1) + // Proof: Referrals AssetTier (max_values: None, max_size: Some(45), added: 2520, mode: MaxEncodedLen) + fn set_reward_percentage() -> Weight { + // Minimum execution time: 19_000 nanoseconds. + Weight::from_ref_time(19_000_000 as u64) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + } +} + +impl WeightInfo for () { + // Storage: Referrals ReferralCodes (r:1 w:1) + // Proof: Referrals ReferralCodes (max_values: None, max_size: Some(56), added: 2531, mode: MaxEncodedLen) + // Storage: System Account (r:2 w:2) + // Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + // Storage: Referrals Referrer (r:0 w:1) + // Proof: Referrals Referrer (max_values: None, max_size: Some(65), added: 2540, mode: MaxEncodedLen) + fn register_code() -> Weight { + // Minimum execution time: 61_000 nanoseconds. + Weight::from_ref_time(62_000_000 as u64) + .saturating_add(RocksDbWeight::get().reads(3 as u64)) + .saturating_add(RocksDbWeight::get().writes(4 as u64)) + } + // Storage: Referrals ReferralCodes (r:1 w:0) + // Proof: Referrals ReferralCodes (max_values: None, max_size: Some(56), added: 2531, mode: MaxEncodedLen) + // Storage: Referrals LinkedAccounts (r:1 w:1) + // Proof: Referrals LinkedAccounts (max_values: None, max_size: Some(80), added: 2555, mode: MaxEncodedLen) + fn link_code() -> Weight { + // Minimum execution time: 26_000 nanoseconds. + Weight::from_ref_time(27_000_000 as u64) + .saturating_add(RocksDbWeight::get().reads(2 as u64)) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) + } + // Storage: Tokens Accounts (r:2 w:2) + // Proof: Tokens Accounts (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) + // Storage: System Account (r:2 w:2) + // Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + // Storage: Omnipool Assets (r:2 w:2) + // Proof: Omnipool Assets (max_values: None, max_size: Some(85), added: 2560, mode: MaxEncodedLen) + // Storage: Omnipool HubAssetImbalance (r:1 w:1) + // Proof: Omnipool HubAssetImbalance (max_values: Some(1), max_size: Some(17), added: 512, mode: MaxEncodedLen) + // Storage: DynamicFees AssetFee (r:1 w:0) + // Proof: DynamicFees AssetFee (max_values: None, max_size: Some(24), added: 2499, mode: MaxEncodedLen) + // Storage: EmaOracle Oracles (r:1 w:0) + // Proof: EmaOracle Oracles (max_values: None, max_size: Some(177), added: 2652, mode: MaxEncodedLen) + // Storage: AssetRegistry Assets (r:1 w:0) + // Proof: AssetRegistry Assets (max_values: None, max_size: Some(87), added: 2562, mode: MaxEncodedLen) + // Storage: EmaOracle Accumulator (r:1 w:1) + // Proof: EmaOracle Accumulator (max_values: Some(1), max_size: Some(5921), added: 6416, mode: MaxEncodedLen) + // Storage: CircuitBreaker AllowedTradeVolumeLimitPerAsset (r:2 w:2) + // Proof: CircuitBreaker AllowedTradeVolumeLimitPerAsset (max_values: None, max_size: Some(68), added: 2543, mode: MaxEncodedLen) + // Storage: CircuitBreaker TradeVolumeLimitPerAsset (r:2 w:0) + // Proof: CircuitBreaker TradeVolumeLimitPerAsset (max_values: None, max_size: Some(28), added: 2503, mode: MaxEncodedLen) + // Storage: CircuitBreaker LiquidityAddLimitPerAsset (r:1 w:0) + // Proof: CircuitBreaker LiquidityAddLimitPerAsset (max_values: None, max_size: Some(29), added: 2504, mode: MaxEncodedLen) + // Storage: CircuitBreaker AllowedAddLiquidityAmountPerAsset (r:1 w:1) + // Proof: CircuitBreaker AllowedAddLiquidityAmountPerAsset (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen) + // Storage: CircuitBreaker LiquidityRemoveLimitPerAsset (r:1 w:0) + // Proof: CircuitBreaker LiquidityRemoveLimitPerAsset (max_values: None, max_size: Some(29), added: 2504, mode: MaxEncodedLen) + // Storage: CircuitBreaker AllowedRemoveLiquidityAmountPerAsset (r:1 w:1) + // Proof: CircuitBreaker AllowedRemoveLiquidityAmountPerAsset (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen) + // Storage: Referrals LinkedAccounts (r:1 w:0) + // Proof: Referrals LinkedAccounts (max_values: None, max_size: Some(80), added: 2555, mode: MaxEncodedLen) + // Storage: Staking Staking (r:1 w:0) + // Proof: Staking Staking (max_values: Some(1), max_size: Some(48), added: 543, mode: MaxEncodedLen) + // Storage: MultiTransactionPayment AccountCurrencyMap (r:0 w:1) + // Proof: MultiTransactionPayment AccountCurrencyMap (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen) + // Storage: Referrals Assets (r:0 w:1) + // Proof: Referrals Assets (max_values: None, max_size: Some(20), added: 2495, mode: MaxEncodedLen) + fn convert() -> Weight { + // Minimum execution time: 304_000 nanoseconds. + Weight::from_ref_time(307_000_000 as u64) + .saturating_add(RocksDbWeight::get().reads(21 as u64)) + .saturating_add(RocksDbWeight::get().writes(14 as u64)) + } + // Storage: Referrals Assets (r:1 w:0) + // Proof: Referrals Assets (max_values: None, max_size: Some(20), added: 2495, mode: MaxEncodedLen) + // Storage: Referrals Shares (r:1 w:1) + // Proof: Referrals Shares (max_values: None, max_size: Some(64), added: 2539, mode: MaxEncodedLen) + // Storage: System Account (r:2 w:2) + // Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + // Storage: Referrals TotalShares (r:1 w:1) + // Proof: Referrals TotalShares (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + // Storage: Referrals Referrer (r:1 w:1) + // Proof: Referrals Referrer (max_values: None, max_size: Some(65), added: 2540, mode: MaxEncodedLen) + fn claim_rewards() -> Weight { + // Minimum execution time: 73_000 nanoseconds. + Weight::from_ref_time(74_000_000 as u64) + .saturating_add(RocksDbWeight::get().reads(6 as u64)) + .saturating_add(RocksDbWeight::get().writes(5 as u64)) + } + // Storage: Referrals AssetTier (r:1 w:1) + // Proof: Referrals AssetTier (max_values: None, max_size: Some(45), added: 2520, mode: MaxEncodedLen) + fn set_reward_percentage() -> Weight { + // Minimum execution time: 19_000 nanoseconds. + Weight::from_ref_time(19_000_000 as u64) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) + } +} diff --git a/pallets/staking/Cargo.toml b/pallets/staking/Cargo.toml index f48b997ff..992c459e0 100644 --- a/pallets/staking/Cargo.toml +++ b/pallets/staking/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-staking" -version = "2.1.0" +version = "2.1.1" authors = ['GalacticCouncil'] edition = "2021" license = "Apache-2.0" diff --git a/pallets/staking/src/lib.rs b/pallets/staking/src/lib.rs index 6ea350a30..fe46b2287 100644 --- a/pallets/staking/src/lib.rs +++ b/pallets/staking/src/lib.rs @@ -938,9 +938,9 @@ impl Pallet { ) -> Result { if asset == T::NativeAssetId::get() && Self::is_initialized() { T::Currency::transfer(asset, &source, &Self::pot_account_id(), amount)?; - Ok(Balance::zero()) - } else { Ok(amount) + } else { + Ok(Balance::zero()) } } diff --git a/runtime/adapters/Cargo.toml b/runtime/adapters/Cargo.toml index 42c619673..dd6811326 100644 --- a/runtime/adapters/Cargo.toml +++ b/runtime/adapters/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-adapters" -version = "0.6.8" +version = "0.6.9" description = "Structs and other generic types for building runtimes." authors = ["GalacticCouncil"] edition = "2021" @@ -28,6 +28,7 @@ pallet-staking = { workspace = true } pallet-route-executor = { workspace = true } pallet-currencies = { workspace = true } pallet-stableswap = { workspace = true } +pallet-referrals = { workspace = true } # Substrate dependencies frame-support = { workspace = true } diff --git a/runtime/adapters/src/lib.rs b/runtime/adapters/src/lib.rs index 72ddb07ae..31e8a10df 100644 --- a/runtime/adapters/src/lib.rs +++ b/runtime/adapters/src/lib.rs @@ -62,6 +62,7 @@ use xcm_executor::{ }; pub mod inspect; +pub mod price; pub mod xcm_exchange; pub mod xcm_execute_filter; @@ -323,18 +324,21 @@ where } /// Passes on trade and liquidity data from the omnipool to the oracle. -pub struct OmnipoolHookAdapter(PhantomData<(Origin, Lrna, Runtime)>); +pub struct OmnipoolHookAdapter(PhantomData<(Origin, NativeAsset, Lrna, Runtime)>); -impl OmnipoolHooks - for OmnipoolHookAdapter +impl OmnipoolHooks + for OmnipoolHookAdapter where Lrna: Get, + NativeAsset: Get, Runtime: pallet_ema_oracle::Config + pallet_circuit_breaker::Config + frame_system::Config - + pallet_staking::Config, + + pallet_staking::Config + + pallet_referrals::Config, ::AccountId: From, ::AssetId: From, + ::AssetId: From, { type Error = DispatchError; @@ -461,8 +465,29 @@ where w1.saturating_add(w2).saturating_add(w3) } - fn on_trade_fee(fee_account: AccountId, asset: AssetId, amount: Balance) -> Result { - pallet_staking::Pallet::::process_trade_fee(fee_account.into(), asset.into(), amount) + fn on_trade_fee( + fee_account: AccountId, + trader: AccountId, + asset: AssetId, + amount: Balance, + ) -> Result { + let referrals_used = if asset == NativeAsset::get() { + Balance::zero() + } else { + pallet_referrals::Pallet::::process_trade_fee( + fee_account.clone().into(), + trader.into(), + asset.into(), + amount, + )? + }; + + let staking_used = pallet_staking::Pallet::::process_trade_fee( + fee_account.into(), + asset.into(), + amount.saturating_sub(referrals_used), + )?; + Ok(staking_used.saturating_add(referrals_used)) } } diff --git a/runtime/adapters/src/price.rs b/runtime/adapters/src/price.rs new file mode 100644 index 000000000..33c9191d0 --- /dev/null +++ b/runtime/adapters/src/price.rs @@ -0,0 +1,21 @@ +use hydradx_traits::price::PriceProvider; +use hydradx_traits::router::{AssetPair, RouteProvider}; +use hydradx_traits::{OraclePeriod, PriceOracle}; +use sp_core::Get; +use sp_std::marker::PhantomData; + +pub struct OraclePriceProviderUsingRoute(PhantomData<(RP, OP, P)>); + +impl PriceProvider for OraclePriceProviderUsingRoute +where + RP: RouteProvider, + OP: PriceOracle, + P: Get, +{ + type Price = OP::Price; + + fn get_price(asset_a: AssetId, asset_b: AssetId) -> Option { + let route = RP::get_route(AssetPair::new(asset_a, asset_b)); + OP::price(&route, P::get()) + } +} diff --git a/runtime/hydradx/Cargo.toml b/runtime/hydradx/Cargo.toml index ad6abbe45..9adf22497 100644 --- a/runtime/hydradx/Cargo.toml +++ b/runtime/hydradx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-runtime" -version = "195.0.0" +version = "196.0.0" authors = ["GalacticCouncil"] edition = "2021" license = "Apache 2.0" @@ -35,6 +35,7 @@ pallet-stableswap = { workspace = true } pallet-bonds = { workspace = true } pallet-lbp = { workspace = true } pallet-xyk = { workspace = true } +pallet-referrals = { workspace = true } # pallets pallet-balances = { workspace = true } @@ -188,6 +189,7 @@ runtime-benchmarks = [ "pallet-stableswap/runtime-benchmarks", "pallet-lbp/runtime-benchmarks", "pallet-xyk/runtime-benchmarks", + "pallet-referrals/runtime-benchmarks", ] std = [ "codec/std", @@ -280,6 +282,7 @@ std = [ "pallet-evm-chain-id/std", "pallet-evm-precompile-dispatch/std", "pallet-xyk/std", + "pallet-referrals/std", ] try-runtime= [ "frame-try-runtime", @@ -346,4 +349,5 @@ try-runtime= [ "pallet-evm/try-runtime", "pallet-evm-chain-id/try-runtime", "pallet-xyk/try-runtime", + "pallet-referrals/try-runtime", ] diff --git a/runtime/hydradx/src/assets.rs b/runtime/hydradx/src/assets.rs index e50166310..861fba964 100644 --- a/runtime/hydradx/src/assets.rs +++ b/runtime/hydradx/src/assets.rs @@ -54,13 +54,14 @@ use frame_support::{ use frame_system::{EnsureRoot, EnsureSigned, RawOrigin}; use hydradx_traits::router::{inverse_route, Trade}; use orml_traits::currency::MutationHooks; -use orml_traits::GetByKey; +use orml_traits::{GetByKey, MultiCurrency}; use pallet_dynamic_fees::types::FeeParams; use pallet_lbp::weights::WeightInfo as LbpWeights; use pallet_route_executor::{weights::WeightInfo as RouterWeights, AmmTradeWeights, MAX_NUMBER_OF_TRADES}; use pallet_staking::types::Action; use pallet_staking::SigmoidPercentage; use pallet_xyk::weights::WeightInfo as XykWeights; +use sp_runtime::{DispatchError, FixedPointNumber}; use sp_std::num::NonZeroU16; parameter_types! { @@ -247,7 +248,7 @@ impl pallet_omnipool::Config for Runtime { type NFTCollectionId = OmnipoolCollectionId; type NFTHandler = Uniques; type WeightInfo = weights::omnipool::HydraWeight; - type OmnipoolHooks = OmnipoolHookAdapter; + type OmnipoolHooks = OmnipoolHookAdapter; type PriceBarrier = ( EnsurePriceWithin< AccountId, @@ -414,8 +415,9 @@ where } } +use hydradx_traits::pools::SpotPriceProvider; #[cfg(feature = "runtime-benchmarks")] -use hydradx_traits::{pools::SpotPriceProvider, PriceOracle}; +use hydradx_traits::PriceOracle; #[cfg(feature = "runtime-benchmarks")] use hydra_dx_math::ema::EmaPrice; @@ -571,18 +573,22 @@ impl AmmTradeWeights> for RouterWeightInfo { let amm_weight = match trade.pool { PoolType::Omnipool => weights::omnipool::HydraWeight::::router_execution_sell(c, e) - .saturating_add( as OmnipoolHooks< - RuntimeOrigin, - AccountId, - AssetId, - Balance, - >>::on_trade_weight()) - .saturating_add( as OmnipoolHooks< - RuntimeOrigin, - AccountId, - AssetId, - Balance, - >>::on_liquidity_changed_weight()), + .saturating_add( + as OmnipoolHooks< + RuntimeOrigin, + AccountId, + AssetId, + Balance, + >>::on_trade_weight(), + ) + .saturating_add( + as OmnipoolHooks< + RuntimeOrigin, + AccountId, + AssetId, + Balance, + >>::on_liquidity_changed_weight(), + ), PoolType::LBP => weights::lbp::HydraWeight::::router_execution_sell(c, e), PoolType::Stableswap(_) => weights::stableswap::HydraWeight::::router_execution_sell(c, e), PoolType::XYK => weights::xyk::HydraWeight::::router_execution_sell(c, e) @@ -605,18 +611,22 @@ impl AmmTradeWeights> for RouterWeightInfo { let amm_weight = match trade.pool { PoolType::Omnipool => weights::omnipool::HydraWeight::::router_execution_buy(c, e) - .saturating_add( as OmnipoolHooks< - RuntimeOrigin, - AccountId, - AssetId, - Balance, - >>::on_trade_weight()) - .saturating_add( as OmnipoolHooks< - RuntimeOrigin, - AccountId, - AssetId, - Balance, - >>::on_liquidity_changed_weight()), + .saturating_add( + as OmnipoolHooks< + RuntimeOrigin, + AccountId, + AssetId, + Balance, + >>::on_trade_weight(), + ) + .saturating_add( + as OmnipoolHooks< + RuntimeOrigin, + AccountId, + AssetId, + Balance, + >>::on_liquidity_changed_weight(), + ), PoolType::LBP => weights::lbp::HydraWeight::::router_execution_buy(c, e), PoolType::Stableswap(_) => weights::stableswap::HydraWeight::::router_execution_buy(c, e), PoolType::XYK => weights::xyk::HydraWeight::::router_execution_buy(c, e) @@ -816,6 +826,13 @@ where use pallet_currencies::fungibles::FungibleCurrencies; +#[cfg(not(feature = "runtime-benchmarks"))] +use hydradx_adapters::price::OraclePriceProviderUsingRoute; + +#[cfg(feature = "runtime-benchmarks")] +use hydradx_traits::price::PriceProvider; +use pallet_referrals::traits::Convert; +use pallet_referrals::{FeeDistribution, Level}; #[cfg(feature = "runtime-benchmarks")] use pallet_stableswap::BenchmarkHelper; #[cfg(feature = "runtime-benchmarks")] @@ -1004,3 +1021,201 @@ impl pallet_xyk::Config for Runtime { type NonDustableWhitelistHandler = Duster; type OracleSource = XYKOracleSourceIdentifier; } + +parameter_types! { + pub const ReferralsPalletId: PalletId = PalletId(*b"referral"); + pub RegistrationFee: (AssetId,Balance, AccountId)= (NativeAssetId::get(), 222_000_000_000_000, TreasuryAccount::get()); + pub const MaxCodeLength: u32 = 7; + pub const ReferralsOraclePeriod: OraclePeriod = OraclePeriod::TenMinutes; + pub const ReferralsSeedAmount: Balance = 10_000_000_000_000; + pub ReferralsExternalRewardAccount: Option = Some(StakingPalletId::get().into_account_truncating()); +} + +impl pallet_referrals::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type AuthorityOrigin = EnsureRoot; + type AssetId = AssetId; + type Currency = FungibleCurrencies; + type Convert = ConvertViaOmnipool; + #[cfg(not(feature = "runtime-benchmarks"))] + type PriceProvider = + OraclePriceProviderUsingRoute, ReferralsOraclePeriod>; + #[cfg(feature = "runtime-benchmarks")] + type PriceProvider = ReferralsDummyPriceProvider; + type RewardAsset = NativeAssetId; + type PalletId = ReferralsPalletId; + type RegistrationFee = RegistrationFee; + type CodeLength = MaxCodeLength; + type LevelVolumeAndRewardPercentages = ReferralsLevelVolumeAndRewards; + type ExternalAccount = ReferralsExternalRewardAccount; + type SeedNativeAmount = ReferralsSeedAmount; + type WeightInfo = weights::referrals::HydraWeight; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = ReferralsBenchmarkHelper; +} + +pub struct ConvertViaOmnipool(PhantomData); +impl Convert for ConvertViaOmnipool +where + SP: SpotPriceProvider, +{ + type Error = DispatchError; + + fn convert( + who: AccountId, + asset_from: AssetId, + asset_to: AssetId, + amount: Balance, + ) -> Result { + if amount < ::MinimumTradingLimit::get() { + return Err(pallet_referrals::Error::::ConversionMinTradingAmountNotReached.into()); + } + let price = SP::spot_price(asset_to, asset_from).ok_or(pallet_referrals::Error::::PriceNotFound)?; + let amount_to_receive = price.saturating_mul_int(amount); + let min_expected = amount_to_receive + .saturating_sub(Permill::from_percent(1).mul_floor(amount_to_receive)) + .max(1); + let balance = Currencies::free_balance(asset_to, &who); + let r = Omnipool::sell( + RuntimeOrigin::signed(who.clone()), + asset_from, + asset_to, + amount, + min_expected, + ); + if let Err(error) = r { + if error == pallet_omnipool::Error::::ZeroAmountOut.into() { + return Err(pallet_referrals::Error::::ConversionZeroAmountReceived.into()); + } + return Err(error); + } + let balance_after = Currencies::free_balance(asset_to, &who); + let received = balance_after.saturating_sub(balance); + Ok(received) + } +} + +pub struct ReferralsLevelVolumeAndRewards; + +impl GetByKey for ReferralsLevelVolumeAndRewards { + fn get(k: &Level) -> (Balance, FeeDistribution) { + let volume = match k { + Level::Tier0 | Level::None => 0, + Level::Tier1 => 305 * UNITS, + Level::Tier2 => 4_583 * UNITS, + Level::Tier3 => 61_111 * UNITS, + Level::Tier4 => 763_888 * UNITS, + }; + let rewards = match k { + Level::None => FeeDistribution { + referrer: Permill::zero(), + trader: Permill::zero(), + external: Permill::from_percent(50), + }, + Level::Tier0 => FeeDistribution { + referrer: Permill::from_percent(5), + trader: Permill::from_percent(10), + external: Permill::from_percent(35), + }, + Level::Tier1 => FeeDistribution { + referrer: Permill::from_percent(10), + trader: Permill::from_percent(11), + external: Permill::from_percent(29), + }, + Level::Tier2 => FeeDistribution { + referrer: Permill::from_percent(15), + trader: Permill::from_percent(12), + external: Permill::from_percent(23), + }, + Level::Tier3 => FeeDistribution { + referrer: Permill::from_percent(20), + trader: Permill::from_percent(13), + external: Permill::from_percent(17), + }, + Level::Tier4 => FeeDistribution { + referrer: Permill::from_percent(25), + trader: Permill::from_percent(15), + external: Permill::from_percent(10), + }, + }; + (volume, rewards) + } +} + +#[cfg(feature = "runtime-benchmarks")] +use pallet_referrals::BenchmarkHelper as RefBenchmarkHelper; + +#[cfg(feature = "runtime-benchmarks")] +pub struct ReferralsBenchmarkHelper; + +#[cfg(feature = "runtime-benchmarks")] +impl RefBenchmarkHelper for ReferralsBenchmarkHelper { + fn prepare_convertible_asset_and_amount() -> (AssetId, Balance) { + let asset_id: u32 = 1234u32; + let asset_name = asset_id.to_le_bytes().to_vec(); + let name: BoundedVec = asset_name.clone().try_into().unwrap(); + + AssetRegistry::register_asset( + name, + pallet_asset_registry::AssetType::::Token, + 1_000_000, + Some(asset_id), + None, + ) + .unwrap(); + AssetRegistry::set_metadata(RuntimeOrigin::root(), asset_id, asset_name, 18).unwrap(); + + let native_price = FixedU128::from_inner(1201500000000000); + let asset_price = FixedU128::from_inner(45_000_000_000); + + Currencies::update_balance( + RuntimeOrigin::root(), + Omnipool::protocol_account(), + NativeAssetId::get(), + 1_000_000_000_000_000_000, + ) + .unwrap(); + + Currencies::update_balance( + RuntimeOrigin::root(), + Omnipool::protocol_account(), + asset_id, + 1_000_000_000_000_000_000_000_000, + ) + .unwrap(); + + Omnipool::add_token( + RuntimeOrigin::root(), + NativeAssetId::get(), + native_price, + Permill::from_percent(10), + TreasuryAccount::get(), + ) + .unwrap(); + + Omnipool::add_token( + RuntimeOrigin::root(), + asset_id, + asset_price, + Permill::from_percent(10), + TreasuryAccount::get(), + ) + .unwrap(); + (1234, 1_000_000_000_000_000_000) + } +} + +#[cfg(feature = "runtime-benchmarks")] +pub struct ReferralsDummyPriceProvider; + +#[cfg(feature = "runtime-benchmarks")] +impl PriceProvider for ReferralsDummyPriceProvider { + type Price = EmaPrice; + + fn get_price(asset_a: AssetId, asset_b: AssetId) -> Option { + if asset_a == asset_b { + return Some(EmaPrice::one()); + } + Some(EmaPrice::new(1_000_000_000_000, 2_000_000_000_000_000_000)) + } +} diff --git a/runtime/hydradx/src/benchmarking/omnipool.rs b/runtime/hydradx/src/benchmarking/omnipool.rs index af8f3a7fe..921a5eba1 100644 --- a/runtime/hydradx/src/benchmarking/omnipool.rs +++ b/runtime/hydradx/src/benchmarking/omnipool.rs @@ -1,4 +1,6 @@ -use crate::{AccountId, AssetId, AssetRegistry, Balance, EmaOracle, Omnipool, Runtime, RuntimeOrigin, System}; +use crate::{ + AccountId, AssetId, AssetRegistry, Balance, EmaOracle, Omnipool, Referrals, Runtime, RuntimeOrigin, System, +}; use super::*; @@ -20,6 +22,7 @@ use hydradx_traits::{ use orml_benchmarking::runtime_benchmarks; use orml_traits::{MultiCurrency, MultiCurrencyExtended}; use pallet_omnipool::types::Tradability; +use pallet_referrals::ReferralCode; pub fn update_balance(currency_id: AssetId, who: &AccountId, balance: Balance) { assert_ok!( @@ -193,9 +196,10 @@ runtime_benchmarks! { let token_amount = 200_000_000_000_000_u128; update_balance(token_id, &acc, token_amount); + update_balance(0, &owner, 1_000_000_000_000_000_u128); // Add the token to the pool - Omnipool::add_token(RawOrigin::Root.into(), token_id, token_price, Permill::from_percent(100), owner)?; + Omnipool::add_token(RawOrigin::Root.into(), token_id, token_price, Permill::from_percent(100), owner.clone())?; // Create LP provider account with correct balance aand add some liquidity let lp_provider: AccountId = account("provider", 1, 1); @@ -218,6 +222,10 @@ runtime_benchmarks! { let amount_sell = 100_000_000_000_u128; let buy_min_amount = 10_000_000_000_u128; + // Register and link referral code to account for the weight too + let code = ReferralCode::<::CodeLength>::truncate_from(b"MYCODE".to_vec()); + Referrals::register_code(RawOrigin::Signed(owner).into(), code.clone())?; + Referrals::link_code(RawOrigin::Signed(seller.clone()).into(), code)?; }: { Omnipool::sell(RawOrigin::Signed(seller.clone()).into(), token_id, DAI, amount_sell, buy_min_amount)? } verify { assert!(::Currency::free_balance(DAI, &seller) >= buy_min_amount); @@ -236,9 +244,10 @@ runtime_benchmarks! { let token_amount = 200_000_000_000_000_u128; update_balance(token_id, &acc, token_amount); + update_balance(0, &owner, 1_000_000_000_000_000_u128); // Add the token to the pool - Omnipool::add_token(RawOrigin::Root.into(), token_id, token_price, Permill::from_percent(100), owner)?; + Omnipool::add_token(RawOrigin::Root.into(), token_id, token_price, Permill::from_percent(100), owner.clone())?; // Create LP provider account with correct balance aand add some liquidity let lp_provider: AccountId = account("provider", 1, 1); @@ -260,7 +269,10 @@ runtime_benchmarks! { let amount_buy = 1_000_000_000_000_u128; let sell_max_limit = 2_000_000_000_000_u128; - + // Register and link referral code to account for the weight too + let code = ReferralCode::<::CodeLength>::truncate_from(b"MYCODE".to_vec()); + Referrals::register_code(RawOrigin::Signed(owner).into(), code.clone())?; + Referrals::link_code(RawOrigin::Signed(seller.clone()).into(), code)?; }: { Omnipool::buy(RawOrigin::Signed(seller.clone()).into(), DAI, token_id, amount_buy, sell_max_limit)? } verify { assert!(::Currency::free_balance(DAI, &seller) >= Balance::zero()); diff --git a/runtime/hydradx/src/lib.rs b/runtime/hydradx/src/lib.rs index e30cc3940..798830fba 100644 --- a/runtime/hydradx/src/lib.rs +++ b/runtime/hydradx/src/lib.rs @@ -99,7 +99,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("hydradx"), impl_name: create_runtime_str!("hydradx"), authoring_version: 1, - spec_version: 195, + spec_version: 196, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -119,6 +119,7 @@ pub fn get_all_module_accounts() -> Vec { vec![ TreasuryPalletId::get().into_account_truncating(), VestingPalletId::get().into_account_truncating(), + ReferralsPalletId::get().into_account_truncating(), ] } @@ -167,6 +168,7 @@ construct_runtime!( Bonds: pallet_bonds = 71, LBP: pallet_lbp = 73, XYK: pallet_xyk = 74, + Referrals: pallet_referrals = 75, // ORML related modules Tokens: orml_tokens = 77, @@ -548,6 +550,7 @@ impl_runtime_apis! { list_benchmark!(list, extra, pallet_staking, Staking); list_benchmark!(list, extra, pallet_lbp, LBP); list_benchmark!(list, extra, pallet_xyk, XYK); + list_benchmark!(list, extra, pallet_referrals, Referrals); list_benchmark!(list, extra, cumulus_pallet_xcmp_queue, XcmpQueue); list_benchmark!(list, extra, pallet_transaction_pause, TransactionPause); @@ -618,6 +621,7 @@ impl_runtime_apis! { add_benchmark!(params, batches, pallet_lbp, LBP); add_benchmark!(params, batches, pallet_xyk, XYK); add_benchmark!(params, batches, pallet_stableswap, Stableswap); + add_benchmark!(params, batches, pallet_referrals, Referrals); add_benchmark!(params, batches, cumulus_pallet_xcmp_queue, XcmpQueue); add_benchmark!(params, batches, pallet_transaction_pause, TransactionPause); diff --git a/runtime/hydradx/src/weights/mod.rs b/runtime/hydradx/src/weights/mod.rs index 1b7827d67..9117b1c57 100644 --- a/runtime/hydradx/src/weights/mod.rs +++ b/runtime/hydradx/src/weights/mod.rs @@ -17,6 +17,7 @@ pub mod otc; pub mod payment; pub mod preimage; pub mod proxy; +pub mod referrals; pub mod registry; pub mod route_executor; pub mod scheduler; diff --git a/runtime/hydradx/src/weights/omnipool.rs b/runtime/hydradx/src/weights/omnipool.rs index c70eff905..f205a52ef 100644 --- a/runtime/hydradx/src/weights/omnipool.rs +++ b/runtime/hydradx/src/weights/omnipool.rs @@ -18,7 +18,7 @@ //! Autogenerated weights for pallet_omnipool //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-11-15, STEPS: 5, REPEAT: 20, LOW RANGE: [], HIGH RANGE: [] +//! DATE: 2023-12-13, STEPS: 5, REPEAT: 20, LOW RANGE: [], HIGH RANGE: [] //! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 // Executed Command: @@ -79,8 +79,8 @@ impl WeightInfo for HydraWeight { // Storage: Omnipool Positions (r:0 w:1) // Proof: Omnipool Positions (max_values: None, max_size: Some(100), added: 2575, mode: MaxEncodedLen) fn add_token() -> Weight { - // Minimum execution time: 147_434 nanoseconds. - Weight::from_ref_time(148_837_000 as u64) + // Minimum execution time: 146_335 nanoseconds. + Weight::from_ref_time(147_377_000 as u64) .saturating_add(T::DbWeight::get().reads(12 as u64)) .saturating_add(T::DbWeight::get().writes(10 as u64)) } @@ -121,8 +121,8 @@ impl WeightInfo for HydraWeight { // Storage: Omnipool Positions (r:0 w:1) // Proof: Omnipool Positions (max_values: None, max_size: Some(100), added: 2575, mode: MaxEncodedLen) fn add_liquidity() -> Weight { - // Minimum execution time: 224_128 nanoseconds. - Weight::from_ref_time(226_321_000 as u64) + // Minimum execution time: 219_924 nanoseconds. + Weight::from_ref_time(221_299_000 as u64) .saturating_add(T::DbWeight::get().reads(20 as u64)) .saturating_add(T::DbWeight::get().writes(14 as u64)) } @@ -165,12 +165,12 @@ impl WeightInfo for HydraWeight { // Storage: Uniques ItemPriceOf (r:0 w:1) // Proof: Uniques ItemPriceOf (max_values: None, max_size: Some(113), added: 2588, mode: MaxEncodedLen) fn remove_liquidity() -> Weight { - // Minimum execution time: 298_601 nanoseconds. - Weight::from_ref_time(300_078_000 as u64) + // Minimum execution time: 293_052 nanoseconds. + Weight::from_ref_time(295_034_000 as u64) .saturating_add(T::DbWeight::get().reads(23 as u64)) .saturating_add(T::DbWeight::get().writes(16 as u64)) } - // Storage: Tokens Accounts (r:4 w:4) + // Storage: Tokens Accounts (r:5 w:5) // Proof: Tokens Accounts (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) // Storage: Omnipool Assets (r:3 w:3) // Proof: Omnipool Assets (max_values: None, max_size: Some(85), added: 2560, mode: MaxEncodedLen) @@ -180,9 +180,9 @@ impl WeightInfo for HydraWeight { // Proof: DynamicFees AssetFee (max_values: None, max_size: Some(24), added: 2499, mode: MaxEncodedLen) // Storage: AssetRegistry Assets (r:2 w:0) // Proof: AssetRegistry Assets (max_values: None, max_size: Some(87), added: 2562, mode: MaxEncodedLen) - // Storage: System Account (r:2 w:1) + // Storage: System Account (r:3 w:2) // Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - // Storage: MultiTransactionPayment AccountCurrencyMap (r:1 w:1) + // Storage: MultiTransactionPayment AccountCurrencyMap (r:2 w:2) // Proof: MultiTransactionPayment AccountCurrencyMap (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen) // Storage: MultiTransactionPayment AcceptedCurrencies (r:1 w:0) // Proof: MultiTransactionPayment AcceptedCurrencies (max_values: None, max_size: Some(28), added: 2503, mode: MaxEncodedLen) @@ -198,15 +198,27 @@ impl WeightInfo for HydraWeight { // Proof: CircuitBreaker LiquidityRemoveLimitPerAsset (max_values: None, max_size: Some(29), added: 2504, mode: MaxEncodedLen) // Storage: CircuitBreaker AllowedRemoveLiquidityAmountPerAsset (r:1 w:0) // Proof: CircuitBreaker AllowedRemoveLiquidityAmountPerAsset (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen) + // Storage: Referrals LinkedAccounts (r:1 w:0) + // Proof: Referrals LinkedAccounts (max_values: None, max_size: Some(80), added: 2555, mode: MaxEncodedLen) + // Storage: Referrals Referrer (r:1 w:0) + // Proof: Referrals Referrer (max_values: None, max_size: Some(65), added: 2540, mode: MaxEncodedLen) + // Storage: Referrals AssetTier (r:1 w:0) + // Proof: Referrals AssetTier (max_values: None, max_size: Some(45), added: 2520, mode: MaxEncodedLen) + // Storage: Referrals TotalShares (r:1 w:1) + // Proof: Referrals TotalShares (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + // Storage: Referrals Shares (r:2 w:2) + // Proof: Referrals Shares (max_values: None, max_size: Some(64), added: 2539, mode: MaxEncodedLen) + // Storage: Referrals Assets (r:0 w:1) + // Proof: Referrals Assets (max_values: None, max_size: Some(20), added: 2495, mode: MaxEncodedLen) fn sell() -> Weight { - // Minimum execution time: 252_248 nanoseconds. - Weight::from_ref_time(255_333_000 as u64) - .saturating_add(T::DbWeight::get().reads(22 as u64)) - .saturating_add(T::DbWeight::get().writes(14 as u64)) + // Minimum execution time: 318_696 nanoseconds. + Weight::from_ref_time(320_028_000 as u64) + .saturating_add(T::DbWeight::get().reads(31 as u64)) + .saturating_add(T::DbWeight::get().writes(21 as u64)) } // Storage: Omnipool Assets (r:3 w:3) // Proof: Omnipool Assets (max_values: None, max_size: Some(85), added: 2560, mode: MaxEncodedLen) - // Storage: Tokens Accounts (r:4 w:4) + // Storage: Tokens Accounts (r:5 w:5) // Proof: Tokens Accounts (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) // Storage: Omnipool HubAssetImbalance (r:1 w:1) // Proof: Omnipool HubAssetImbalance (max_values: Some(1), max_size: Some(17), added: 512, mode: MaxEncodedLen) @@ -216,11 +228,11 @@ impl WeightInfo for HydraWeight { // Proof: EmaOracle Oracles (max_values: None, max_size: Some(177), added: 2652, mode: MaxEncodedLen) // Storage: AssetRegistry Assets (r:2 w:0) // Proof: AssetRegistry Assets (max_values: None, max_size: Some(87), added: 2562, mode: MaxEncodedLen) - // Storage: System Account (r:2 w:1) + // Storage: System Account (r:3 w:2) // Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - // Storage: MultiTransactionPayment AccountCurrencyMap (r:1 w:1) + // Storage: MultiTransactionPayment AccountCurrencyMap (r:2 w:1) // Proof: MultiTransactionPayment AccountCurrencyMap (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen) - // Storage: MultiTransactionPayment AcceptedCurrencies (r:1 w:0) + // Storage: MultiTransactionPayment AcceptedCurrencies (r:2 w:0) // Proof: MultiTransactionPayment AcceptedCurrencies (max_values: None, max_size: Some(28), added: 2503, mode: MaxEncodedLen) // Storage: EmaOracle Accumulator (r:1 w:1) // Proof: EmaOracle Accumulator (max_values: Some(1), max_size: Some(5921), added: 6416, mode: MaxEncodedLen) @@ -234,17 +246,29 @@ impl WeightInfo for HydraWeight { // Proof: CircuitBreaker LiquidityRemoveLimitPerAsset (max_values: None, max_size: Some(29), added: 2504, mode: MaxEncodedLen) // Storage: CircuitBreaker AllowedRemoveLiquidityAmountPerAsset (r:1 w:0) // Proof: CircuitBreaker AllowedRemoveLiquidityAmountPerAsset (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen) + // Storage: Referrals LinkedAccounts (r:1 w:0) + // Proof: Referrals LinkedAccounts (max_values: None, max_size: Some(80), added: 2555, mode: MaxEncodedLen) + // Storage: Referrals Referrer (r:1 w:0) + // Proof: Referrals Referrer (max_values: None, max_size: Some(65), added: 2540, mode: MaxEncodedLen) + // Storage: Referrals AssetTier (r:1 w:0) + // Proof: Referrals AssetTier (max_values: None, max_size: Some(45), added: 2520, mode: MaxEncodedLen) + // Storage: Referrals TotalShares (r:1 w:1) + // Proof: Referrals TotalShares (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + // Storage: Referrals Shares (r:2 w:2) + // Proof: Referrals Shares (max_values: None, max_size: Some(64), added: 2539, mode: MaxEncodedLen) + // Storage: Referrals Assets (r:0 w:1) + // Proof: Referrals Assets (max_values: None, max_size: Some(20), added: 2495, mode: MaxEncodedLen) fn buy() -> Weight { - // Minimum execution time: 285_129 nanoseconds. - Weight::from_ref_time(291_349_000 as u64) - .saturating_add(T::DbWeight::get().reads(24 as u64)) - .saturating_add(T::DbWeight::get().writes(15 as u64)) + // Minimum execution time: 344_823 nanoseconds. + Weight::from_ref_time(347_210_000 as u64) + .saturating_add(T::DbWeight::get().reads(34 as u64)) + .saturating_add(T::DbWeight::get().writes(21 as u64)) } // Storage: Omnipool Assets (r:1 w:1) // Proof: Omnipool Assets (max_values: None, max_size: Some(85), added: 2560, mode: MaxEncodedLen) fn set_asset_tradable_state() -> Weight { - // Minimum execution time: 36_408 nanoseconds. - Weight::from_ref_time(36_896_000 as u64) + // Minimum execution time: 35_160 nanoseconds. + Weight::from_ref_time(35_515_000 as u64) .saturating_add(T::DbWeight::get().reads(1 as u64)) .saturating_add(T::DbWeight::get().writes(1 as u64)) } @@ -261,8 +285,8 @@ impl WeightInfo for HydraWeight { // Storage: MultiTransactionPayment AcceptedCurrencies (r:1 w:0) // Proof: MultiTransactionPayment AcceptedCurrencies (max_values: None, max_size: Some(28), added: 2503, mode: MaxEncodedLen) fn refund_refused_asset() -> Weight { - // Minimum execution time: 111_080 nanoseconds. - Weight::from_ref_time(114_389_000 as u64) + // Minimum execution time: 108_179 nanoseconds. + Weight::from_ref_time(109_484_000 as u64) .saturating_add(T::DbWeight::get().reads(8 as u64)) .saturating_add(T::DbWeight::get().writes(5 as u64)) } @@ -279,16 +303,16 @@ impl WeightInfo for HydraWeight { // Storage: Uniques ItemPriceOf (r:0 w:1) // Proof: Uniques ItemPriceOf (max_values: None, max_size: Some(113), added: 2588, mode: MaxEncodedLen) fn sacrifice_position() -> Weight { - // Minimum execution time: 78_568 nanoseconds. - Weight::from_ref_time(79_330_000 as u64) + // Minimum execution time: 77_117 nanoseconds. + Weight::from_ref_time(78_173_000 as u64) .saturating_add(T::DbWeight::get().reads(4 as u64)) .saturating_add(T::DbWeight::get().writes(6 as u64)) } // Storage: Omnipool Assets (r:1 w:1) // Proof: Omnipool Assets (max_values: None, max_size: Some(85), added: 2560, mode: MaxEncodedLen) fn set_asset_weight_cap() -> Weight { - // Minimum execution time: 35_559 nanoseconds. - Weight::from_ref_time(36_075_000 as u64) + // Minimum execution time: 34_863 nanoseconds. + Weight::from_ref_time(35_340_000 as u64) .saturating_add(T::DbWeight::get().reads(1 as u64)) .saturating_add(T::DbWeight::get().writes(1 as u64)) } @@ -311,8 +335,8 @@ impl WeightInfo for HydraWeight { // Storage: EmaOracle Accumulator (r:1 w:1) // Proof: EmaOracle Accumulator (max_values: Some(1), max_size: Some(5921), added: 6416, mode: MaxEncodedLen) fn withdraw_protocol_liquidity() -> Weight { - // Minimum execution time: 168_287 nanoseconds. - Weight::from_ref_time(171_781_000 as u64) + // Minimum execution time: 163_392 nanoseconds. + Weight::from_ref_time(164_478_000 as u64) .saturating_add(T::DbWeight::get().reads(13 as u64)) .saturating_add(T::DbWeight::get().writes(8 as u64)) } @@ -333,8 +357,8 @@ impl WeightInfo for HydraWeight { // Storage: MultiTransactionPayment AcceptedCurrencies (r:1 w:0) // Proof: MultiTransactionPayment AcceptedCurrencies (max_values: None, max_size: Some(28), added: 2503, mode: MaxEncodedLen) fn remove_token() -> Weight { - // Minimum execution time: 162_935 nanoseconds. - Weight::from_ref_time(167_702_000 as u64) + // Minimum execution time: 161_410 nanoseconds. + Weight::from_ref_time(162_579_000 as u64) .saturating_add(T::DbWeight::get().reads(14 as u64)) .saturating_add(T::DbWeight::get().writes(8 as u64)) } @@ -366,16 +390,18 @@ impl WeightInfo for HydraWeight { // Proof: CircuitBreaker LiquidityRemoveLimitPerAsset (max_values: None, max_size: Some(29), added: 2504, mode: MaxEncodedLen) // Storage: CircuitBreaker AllowedRemoveLiquidityAmountPerAsset (r:1 w:0) // Proof: CircuitBreaker AllowedRemoveLiquidityAmountPerAsset (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen) + // Storage: Referrals LinkedAccounts (r:1 w:0) + // Proof: Referrals LinkedAccounts (max_values: None, max_size: Some(80), added: 2555, mode: MaxEncodedLen) /// The range of component `c` is `[0, 1]`. /// The range of component `e` is `[0, 1]`. fn router_execution_sell(c: u32, e: u32) -> Weight { - // Minimum execution time: 47_302 nanoseconds. - Weight::from_ref_time(38_172_125 as u64) // Standard Error: 136_198 - .saturating_add(Weight::from_ref_time(10_307_875 as u64).saturating_mul(c as u64)) - // Standard Error: 136_198 - .saturating_add(Weight::from_ref_time(216_455_950 as u64).saturating_mul(e as u64)) + // Minimum execution time: 46_488 nanoseconds. + Weight::from_ref_time(36_800_250 as u64) // Standard Error: 121_871 + .saturating_add(Weight::from_ref_time(10_721_750 as u64).saturating_mul(c as u64)) + // Standard Error: 121_871 + .saturating_add(Weight::from_ref_time(220_720_900 as u64).saturating_mul(e as u64)) .saturating_add(T::DbWeight::get().reads(6 as u64)) - .saturating_add(T::DbWeight::get().reads((16 as u64).saturating_mul(e as u64))) + .saturating_add(T::DbWeight::get().reads((17 as u64).saturating_mul(e as u64))) .saturating_add(T::DbWeight::get().writes((14 as u64).saturating_mul(e as u64))) } // Storage: Omnipool Assets (r:3 w:3) @@ -408,15 +434,17 @@ impl WeightInfo for HydraWeight { // Proof: CircuitBreaker LiquidityRemoveLimitPerAsset (max_values: None, max_size: Some(29), added: 2504, mode: MaxEncodedLen) // Storage: CircuitBreaker AllowedRemoveLiquidityAmountPerAsset (r:1 w:0) // Proof: CircuitBreaker AllowedRemoveLiquidityAmountPerAsset (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen) + // Storage: Referrals LinkedAccounts (r:1 w:0) + // Proof: Referrals LinkedAccounts (max_values: None, max_size: Some(80), added: 2555, mode: MaxEncodedLen) /// The range of component `c` is `[1, 2]`. /// The range of component `e` is `[0, 1]`. fn router_execution_buy(c: u32, e: u32) -> Weight { - // Minimum execution time: 281_574 nanoseconds. - Weight::from_ref_time(269_724_625 as u64) // Standard Error: 346_676 - .saturating_add(Weight::from_ref_time(13_487_775 as u64).saturating_mul(c as u64)) - // Standard Error: 346_676 - .saturating_add(Weight::from_ref_time(558_675 as u64).saturating_mul(e as u64)) - .saturating_add(T::DbWeight::get().reads(24 as u64)) + // Minimum execution time: 284_953 nanoseconds. + Weight::from_ref_time(267_973_525 as u64) // Standard Error: 501_278 + .saturating_add(Weight::from_ref_time(14_832_175 as u64).saturating_mul(c as u64)) + // Standard Error: 501_278 + .saturating_add(Weight::from_ref_time(5_424_675 as u64).saturating_mul(e as u64)) + .saturating_add(T::DbWeight::get().reads(25 as u64)) .saturating_add(T::DbWeight::get().writes(15 as u64)) } } diff --git a/runtime/hydradx/src/weights/referrals.rs b/runtime/hydradx/src/weights/referrals.rs new file mode 100644 index 000000000..e166658ec --- /dev/null +++ b/runtime/hydradx/src/weights/referrals.rs @@ -0,0 +1,145 @@ +// This file is part of HydraDX. + +// Copyright (C) 2020-2023 Intergalactic, Limited (GIB). +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for pallet_referrals +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-12-13, STEPS: 5, REPEAT: 20, LOW RANGE: [], HIGH RANGE: [] +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// target/release/hydradx +// benchmark +// pallet +// --pallet=pallet-referrals +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --chain=dev +// --extrinsic=* +// --steps=5 +// --repeat=20 +// --output +// referrals.rs +// --template +// .maintain/pallet-weight-template-no-back.hbs + +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(clippy::unnecessary_cast)] + +use frame_support::{ + traits::Get, + weights::{constants::RocksDbWeight, Weight}, +}; +use sp_std::marker::PhantomData; + +use pallet_referrals::weights::WeightInfo; + +/// Weights for pallet_referrals using the hydraDX node and recommended hardware. +pub struct HydraWeight(PhantomData); + +impl WeightInfo for HydraWeight { + // Storage: Referrals ReferralCodes (r:1 w:1) + // Proof: Referrals ReferralCodes (max_values: None, max_size: Some(56), added: 2531, mode: MaxEncodedLen) + // Storage: System Account (r:2 w:2) + // Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + // Storage: Referrals Referrer (r:0 w:1) + // Proof: Referrals Referrer (max_values: None, max_size: Some(65), added: 2540, mode: MaxEncodedLen) + fn register_code() -> Weight { + // Minimum execution time: 42_186 nanoseconds. + Weight::from_ref_time(42_832_000 as u64) + .saturating_add(T::DbWeight::get().reads(3 as u64)) + .saturating_add(T::DbWeight::get().writes(4 as u64)) + } + // Storage: Referrals ReferralCodes (r:1 w:0) + // Proof: Referrals ReferralCodes (max_values: None, max_size: Some(56), added: 2531, mode: MaxEncodedLen) + // Storage: Referrals LinkedAccounts (r:1 w:1) + // Proof: Referrals LinkedAccounts (max_values: None, max_size: Some(80), added: 2555, mode: MaxEncodedLen) + fn link_code() -> Weight { + // Minimum execution time: 21_324 nanoseconds. + Weight::from_ref_time(21_932_000 as u64) + .saturating_add(T::DbWeight::get().reads(2 as u64)) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + } + // Storage: Tokens Accounts (r:2 w:2) + // Proof: Tokens Accounts (max_values: None, max_size: Some(108), added: 2583, mode: MaxEncodedLen) + // Storage: System Account (r:2 w:2) + // Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + // Storage: Omnipool Assets (r:2 w:2) + // Proof: Omnipool Assets (max_values: None, max_size: Some(85), added: 2560, mode: MaxEncodedLen) + // Storage: Omnipool HubAssetImbalance (r:1 w:1) + // Proof: Omnipool HubAssetImbalance (max_values: Some(1), max_size: Some(17), added: 512, mode: MaxEncodedLen) + // Storage: DynamicFees AssetFee (r:1 w:0) + // Proof: DynamicFees AssetFee (max_values: None, max_size: Some(24), added: 2499, mode: MaxEncodedLen) + // Storage: EmaOracle Oracles (r:1 w:0) + // Proof: EmaOracle Oracles (max_values: None, max_size: Some(177), added: 2652, mode: MaxEncodedLen) + // Storage: AssetRegistry Assets (r:1 w:0) + // Proof: AssetRegistry Assets (max_values: None, max_size: Some(87), added: 2562, mode: MaxEncodedLen) + // Storage: EmaOracle Accumulator (r:1 w:1) + // Proof: EmaOracle Accumulator (max_values: Some(1), max_size: Some(5921), added: 6416, mode: MaxEncodedLen) + // Storage: CircuitBreaker AllowedTradeVolumeLimitPerAsset (r:2 w:2) + // Proof: CircuitBreaker AllowedTradeVolumeLimitPerAsset (max_values: None, max_size: Some(68), added: 2543, mode: MaxEncodedLen) + // Storage: CircuitBreaker TradeVolumeLimitPerAsset (r:2 w:0) + // Proof: CircuitBreaker TradeVolumeLimitPerAsset (max_values: None, max_size: Some(28), added: 2503, mode: MaxEncodedLen) + // Storage: CircuitBreaker LiquidityAddLimitPerAsset (r:1 w:0) + // Proof: CircuitBreaker LiquidityAddLimitPerAsset (max_values: None, max_size: Some(29), added: 2504, mode: MaxEncodedLen) + // Storage: CircuitBreaker AllowedAddLiquidityAmountPerAsset (r:1 w:1) + // Proof: CircuitBreaker AllowedAddLiquidityAmountPerAsset (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen) + // Storage: CircuitBreaker LiquidityRemoveLimitPerAsset (r:1 w:0) + // Proof: CircuitBreaker LiquidityRemoveLimitPerAsset (max_values: None, max_size: Some(29), added: 2504, mode: MaxEncodedLen) + // Storage: CircuitBreaker AllowedRemoveLiquidityAmountPerAsset (r:1 w:1) + // Proof: CircuitBreaker AllowedRemoveLiquidityAmountPerAsset (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen) + // Storage: Referrals LinkedAccounts (r:1 w:0) + // Proof: Referrals LinkedAccounts (max_values: None, max_size: Some(80), added: 2555, mode: MaxEncodedLen) + // Storage: Staking Staking (r:1 w:0) + // Proof: Staking Staking (max_values: Some(1), max_size: Some(48), added: 543, mode: MaxEncodedLen) + // Storage: MultiTransactionPayment AccountCurrencyMap (r:0 w:1) + // Proof: MultiTransactionPayment AccountCurrencyMap (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen) + // Storage: Referrals Assets (r:0 w:1) + // Proof: Referrals Assets (max_values: None, max_size: Some(20), added: 2495, mode: MaxEncodedLen) + fn convert() -> Weight { + // Minimum execution time: 249_840 nanoseconds. + Weight::from_ref_time(251_054_000 as u64) + .saturating_add(T::DbWeight::get().reads(21 as u64)) + .saturating_add(T::DbWeight::get().writes(14 as u64)) + } + // Storage: Referrals Assets (r:1 w:0) + // Proof: Referrals Assets (max_values: None, max_size: Some(20), added: 2495, mode: MaxEncodedLen) + // Storage: Referrals Shares (r:1 w:1) + // Proof: Referrals Shares (max_values: None, max_size: Some(64), added: 2539, mode: MaxEncodedLen) + // Storage: System Account (r:2 w:2) + // Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + // Storage: Referrals TotalShares (r:1 w:1) + // Proof: Referrals TotalShares (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + // Storage: Referrals Referrer (r:1 w:1) + // Proof: Referrals Referrer (max_values: None, max_size: Some(65), added: 2540, mode: MaxEncodedLen) + fn claim_rewards() -> Weight { + // Minimum execution time: 62_058 nanoseconds. + Weight::from_ref_time(62_527_000 as u64) + .saturating_add(T::DbWeight::get().reads(6 as u64)) + .saturating_add(T::DbWeight::get().writes(5 as u64)) + } + // Storage: Referrals AssetTier (r:1 w:1) + // Proof: Referrals AssetTier (max_values: None, max_size: Some(45), added: 2520, mode: MaxEncodedLen) + fn set_reward_percentage() -> Weight { + // Minimum execution time: 15_537 nanoseconds. + Weight::from_ref_time(16_059_000 as u64) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + } +} diff --git a/scripts/benchmark.all.sh b/scripts/benchmark.all.sh index dc446f8e1..060268a5f 100644 --- a/scripts/benchmark.all.sh +++ b/scripts/benchmark.all.sh @@ -32,6 +32,7 @@ pallets=("frame-system:system" "pallet-route-executor:route_executor" "pallet-stableswap:stableswap" "pallet-staking:staking" +"pallet-referrals:referrals" ) command="cargo run --bin hydradx --release --features=runtime-benchmarks -- benchmark pallet --pallet=[pallet] --execution=wasm --wasm-execution=compiled --heap-pages=4096 --chain=dev --extrinsic='*' --steps=5 --repeat=20 --output [output].rs --template .maintain/pallet-weight-template-no-back.hbs" diff --git a/traits/Cargo.toml b/traits/Cargo.toml index fded7238d..efacdd11e 100644 --- a/traits/Cargo.toml +++ b/traits/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-traits" -version = "2.8.1" +version = "2.8.2" description = "Shared traits" authors = ["GalacticCouncil"] edition = "2021" diff --git a/traits/src/lib.rs b/traits/src/lib.rs index 3386fba63..fa91200de 100644 --- a/traits/src/lib.rs +++ b/traits/src/lib.rs @@ -26,6 +26,8 @@ pub mod router; pub use registry::*; pub mod oracle; +pub mod price; + pub use oracle::*; use codec::{Decode, Encode}; diff --git a/traits/src/price.rs b/traits/src/price.rs new file mode 100644 index 000000000..1dbbb6807 --- /dev/null +++ b/traits/src/price.rs @@ -0,0 +1,5 @@ +pub trait PriceProvider { + type Price; + + fn get_price(asset_a: AssetId, asset_b: AssetId) -> Option; +}