diff --git a/evm-tests/src/contracts/alpha.ts b/evm-tests/src/contracts/alpha.ts index a87702eb80..ae24298048 100644 --- a/evm-tests/src/contracts/alpha.ts +++ b/evm-tests/src/contracts/alpha.ts @@ -315,5 +315,18 @@ export const IAlphaABI = [ ], "stateMutability": "view", "type": "function" + }, + { + "inputs": [], + "name": "getCKBurn", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" } ] \ No newline at end of file diff --git a/evm-tests/test/alpha.precompile.test.ts b/evm-tests/test/alpha.precompile.test.ts index 31b31da135..1ca3c755af 100644 --- a/evm-tests/test/alpha.precompile.test.ts +++ b/evm-tests/test/alpha.precompile.test.ts @@ -8,6 +8,7 @@ import { PublicClient } from "viem"; import { PolkadotSigner, TypedApi } from "polkadot-api"; import { toViemAddress, convertPublicKeyToSs58 } from "../src/address-utils" import { IAlphaABI, IALPHA_ADDRESS } from "../src/contracts/alpha" +import { u64 } from "@polkadot-api/substrate-bindings"; describe("Test Alpha Precompile", () => { // init substrate part @@ -209,6 +210,24 @@ describe("Test Alpha Precompile", () => { assert.ok(typeof alphaIssuance === 'bigint', "Alpha issuance should be a bigint"); assert.ok(alphaIssuance >= BigInt(0), "Alpha issuance should be non-negative"); }); + + it("getCKBurn returns valid CK burn rate", async () => { + const ckBurn = await publicClient.readContract({ + abi: IAlphaABI, + address: toViemAddress(IALPHA_ADDRESS), + functionName: "getCKBurn", + args: [] + }) + + const ckBurnOnChain = await api.query.SubtensorModule.CKBurn.getValue() + + assert.strictEqual(ckBurn, ckBurnOnChain, "CK burn should match on chain"); + assert.ok(ckBurn !== undefined, "CK burn should be defined"); + const ckBurnPercentage = BigInt(ckBurn) * BigInt(100) / BigInt(2 ** 64 - 1) + assert.ok(ckBurnPercentage >= BigInt(0), "CK burn percentage should be non-negative"); + assert.ok(ckBurnPercentage <= BigInt(100), "CK burn percentage should be less than or equal to 100"); + assert.ok(typeof ckBurn === 'bigint', "CK burn should be a bigint"); + }); }); describe("Global Functions", () => { diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index abc5e7a443..5d8216caa1 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -1591,6 +1591,20 @@ pub mod pallet { pallet_subtensor::Pallet::::set_owner_immune_neuron_limit(netuid, immune_neurons)?; Ok(()) } + + /// Sets the childkey burn for a subnet. + /// It is only callable by the root account. + /// The extrinsic will call the Subtensor pallet to set the childkey burn. + #[pallet::call_index(73)] + #[pallet::weight(Weight::from_parts(15_650_000, 0) + .saturating_add(::DbWeight::get().reads(1_u64)) + .saturating_add(::DbWeight::get().writes(1_u64)))] + pub fn sudo_set_ck_burn(origin: OriginFor, burn: u64) -> DispatchResult { + ensure_root(origin)?; + pallet_subtensor::Pallet::::set_ck_burn(burn); + log::debug!("CKBurnSet( burn: {burn:?} ) "); + Ok(()) + } } } diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index b14dd9e826..0ecdf1fdc8 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -744,10 +744,11 @@ impl Pallet { // Calculate the hotkey's share of the validator emission based on its childkey take let validating_emission: U96F32 = U96F32::saturating_from_num(dividends); let mut remaining_emission: U96F32 = validating_emission; - let childkey_take_proportion: U96F32 = + let burn_take_proportion: U96F32 = Self::get_ck_burn(); + let child_take_proportion: U96F32 = U96F32::saturating_from_num(Self::get_childkey_take(hotkey, netuid)) .safe_div(U96F32::saturating_from_num(u16::MAX)); - log::debug!("Childkey take proportion: {childkey_take_proportion:?} for hotkey {hotkey:?}"); + log::debug!("Childkey take proportion: {child_take_proportion:?} for hotkey {hotkey:?}"); // NOTE: Only the validation emission should be split amongst parents. // Grab the owner of the childkey. @@ -755,7 +756,7 @@ impl Pallet { // Initialize variables to track emission distribution let mut to_parents: u64 = 0; - let mut total_child_emission_take: U96F32 = U96F32::saturating_from_num(0); + let mut total_child_take: U96F32 = U96F32::saturating_from_num(0); // Initialize variables to calculate total stakes from parents let mut total_contribution: U96F32 = U96F32::saturating_from_num(0); @@ -819,23 +820,26 @@ impl Pallet { remaining_emission = remaining_emission.saturating_sub(parent_emission); // Get the childkey take for this parent. - let child_emission_take: U96F32 = if parent_owner == childkey_owner { - // The parent is from the same coldkey, so we don't remove any childkey take. - U96F32::saturating_from_num(0) - } else { - childkey_take_proportion - .saturating_mul(U96F32::saturating_from_num(parent_emission)) + let mut burn_take: U96F32 = U96F32::saturating_from_num(0); + let mut child_take: U96F32 = U96F32::saturating_from_num(0); + if parent_owner != childkey_owner { + // The parent is from a different coldkey, we burn some proportion + burn_take = burn_take_proportion.saturating_mul(parent_emission); + child_take = child_take_proportion.saturating_mul(parent_emission); + parent_emission = parent_emission.saturating_sub(burn_take); + parent_emission = parent_emission.saturating_sub(child_take); + total_child_take = total_child_take.saturating_add(child_take); + + Self::burn_subnet_alpha( + netuid, + AlphaCurrency::from(burn_take.saturating_to_num::()), + ); }; + log::debug!("burn_takee: {burn_take:?} for hotkey {hotkey:?}"); + log::debug!("child_take: {child_take:?} for hotkey {hotkey:?}"); + log::debug!("parent_emission: {parent_emission:?} for hotkey {hotkey:?}"); + log::debug!("total_child_take: {total_child_take:?} for hotkey {hotkey:?}"); - // Remove the childkey take from the parent's emission. - parent_emission = parent_emission.saturating_sub(child_emission_take); - - // Add the childkey take to the total childkey take tracker. - total_child_emission_take = - total_child_emission_take.saturating_add(child_emission_take); - - log::debug!("Child emission take: {child_emission_take:?} for hotkey {hotkey:?}"); - log::debug!("Parent emission: {parent_emission:?} for hotkey {hotkey:?}"); log::debug!("remaining emission: {remaining_emission:?}"); // Add the parent's emission to the distribution list @@ -853,7 +857,7 @@ impl Pallet { // Calculate the final emission for the hotkey itself. // This includes the take left from the parents and the self contribution. let child_emission = remaining_emission - .saturating_add(total_child_emission_take) + .saturating_add(total_child_take) .saturating_to_num::() .into(); diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 4c2eaf0cc7..ce4e41e357 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -867,6 +867,12 @@ pub mod pallet { 50400 } + #[pallet::type_value] + /// Default value for ck burn, 18%. + pub fn DefaultCKBurn() -> u64 { + u64::MAX / 100 * 18 + } + #[pallet::storage] pub type MinActivityCutoff = StorageValue<_, u16, ValueQuery, DefaultMinActivityCutoff>; @@ -920,6 +926,9 @@ pub mod pallet { /// --- ITEM --> Global weight pub type TaoWeight = StorageValue<_, u64, ValueQuery, DefaultTaoWeight>; #[pallet::storage] + /// --- ITEM --> CK burn + pub type CKBurn = StorageValue<_, u64, ValueQuery, DefaultCKBurn>; + #[pallet::storage] /// --- ITEM ( default_delegate_take ) pub type MaxDelegateTake = StorageValue<_, u16, ValueQuery, DefaultDelegateTake>; #[pallet::storage] diff --git a/pallets/subtensor/src/staking/helpers.rs b/pallets/subtensor/src/staking/helpers.rs index 3f07115b7b..c89a762f91 100644 --- a/pallets/subtensor/src/staking/helpers.rs +++ b/pallets/subtensor/src/staking/helpers.rs @@ -321,4 +321,10 @@ impl Pallet { pub fn is_user_liquidity_enabled(netuid: NetUid) -> bool { T::SwapInterface::is_user_liquidity_enabled(netuid) } + + pub fn burn_subnet_alpha(netuid: NetUid, amount: AlphaCurrency) { + SubnetAlphaOut::::mutate(netuid, |total| { + *total = total.saturating_sub(amount); + }); + } } diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index a44eb67afd..528289ec0e 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -96,6 +96,11 @@ impl Pallet { // This ensures the result is always between 0 and 1 weight_fixed.safe_div(U96F32::saturating_from_num(u64::MAX)) } + pub fn get_ck_burn() -> U96F32 { + let stored_weight = CKBurn::::get(); + let weight_fixed = U96F32::saturating_from_num(stored_weight); + weight_fixed.safe_div(U96F32::saturating_from_num(u64::MAX)) + } /// Sets the global global weight in storage. /// @@ -117,6 +122,11 @@ impl Pallet { // Update the TaoWeight storage with the new weight value TaoWeight::::set(weight); } + // Set the amount burned on non owned CK + pub fn set_ck_burn(weight: u64) { + // Update the ck burn value. + CKBurn::::set(weight); + } /// Calculates the weighted combination of alpha and global tao for a single hotkey onet a subnet. /// diff --git a/pallets/subtensor/src/tests/children.rs b/pallets/subtensor/src/tests/children.rs index 2ffc21ea1d..a0505fa9f3 100644 --- a/pallets/subtensor/src/tests/children.rs +++ b/pallets/subtensor/src/tests/children.rs @@ -2861,6 +2861,7 @@ fn test_childkey_take_drain() { // Add network, register hotkeys, and setup network parameters add_network(netuid, subnet_tempo, 0); + SubtensorModule::set_ck_burn(0); mock::setup_reserves(netuid, (stake * 10_000).into(), (stake * 10_000).into()); register_ok_neuron(netuid, child_hotkey, child_coldkey, 0); register_ok_neuron(netuid, parent_hotkey, parent_coldkey, 1); @@ -2980,6 +2981,7 @@ fn test_parent_child_chain_emission() { let subnet_owner_coldkey = U256::from(1001); let subnet_owner_hotkey = U256::from(1002); let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + SubtensorModule::set_ck_burn(0); Tempo::::insert(netuid, 1); // Setup large LPs to prevent slippage @@ -3192,6 +3194,7 @@ fn test_parent_child_chain_epoch() { new_test_ext(1).execute_with(|| { let netuid = NetUid::from(1); add_network(netuid, 1, 0); + SubtensorModule::set_ck_burn(0); // Set owner cut to 0 SubtensorModule::set_subnet_owner_cut(0_u16); @@ -3336,6 +3339,7 @@ fn test_dividend_distribution_with_children() { new_test_ext(1).execute_with(|| { let netuid = NetUid::from(1); add_network(netuid, 1, 0); + SubtensorModule::set_ck_burn(0); mock::setup_reserves( netuid, 1_000_000_000_000_000.into(), @@ -3570,6 +3574,7 @@ fn test_dividend_distribution_with_children() { fn test_dynamic_parent_child_relationships() { new_test_ext(1).execute_with(|| { let netuid = NetUid::from(1); + SubtensorModule::set_ck_burn(0); add_network_disable_commit_reveal(netuid, 1, 0); // Define hotkeys and coldkeys diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index 4781ad2dbd..9cf2ed402a 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -1063,6 +1063,7 @@ fn test_drain_alpha_childkey_parentkey() { new_test_ext(1).execute_with(|| { let netuid = NetUid::from(1); add_network(netuid, 1, 0); + SubtensorModule::set_ck_burn(0); let parent = U256::from(1); let child = U256::from(2); let coldkey = U256::from(3); @@ -1238,6 +1239,7 @@ fn test_get_root_children_drain() { let alpha = NetUid::from(1); add_network(NetUid::ROOT, 1, 0); add_network(alpha, 1, 0); + SubtensorModule::set_ck_burn(0); // Set TAO weight to 1. SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1. // Create keys. @@ -1399,6 +1401,7 @@ fn test_get_root_children_drain_half_proportion() { let alpha = NetUid::from(1); add_network(NetUid::ROOT, 1, 0); add_network(alpha, 1, 0); + SubtensorModule::set_ck_burn(0); // Set TAO weight to 1. SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1. // Create keys. @@ -1576,6 +1579,7 @@ fn test_get_root_children_drain_with_half_take() { add_network(alpha, 1, 0); // Set TAO weight to 1. SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1. + SubtensorModule::set_ck_burn(0); // Create keys. let cold_alice = U256::from(0); let cold_bob = U256::from(1); @@ -2750,6 +2754,75 @@ fn test_coinbase_v3_liquidity_update() { }); } +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_drain_alpha_childkey_parentkey_with_burn --exact --show-output --nocapture +#[test] +fn test_drain_alpha_childkey_parentkey_with_burn() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + add_network(netuid, 1, 0); + let parent = U256::from(1); + let child = U256::from(2); + let coldkey = U256::from(3); + let stake_before = AlphaCurrency::from(1_000_000_000); + register_ok_neuron(netuid, child, coldkey, 0); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &parent, + &coldkey, + netuid, + stake_before, + ); + mock_set_children_no_epochs(netuid, &parent, &[(u64::MAX, child)]); + + // Childkey take is 10% + ChildkeyTake::::insert(child, netuid, u16::MAX / 10); + + let burn_rate = SubtensorModule::get_ck_burn(); + let parent_stake_before = SubtensorModule::get_stake_for_hotkey_on_subnet(&parent, netuid); + let child_stake_before = SubtensorModule::get_stake_for_hotkey_on_subnet(&child, netuid); + + let pending_alpha = AlphaCurrency::from(1_000_000_000); + SubtensorModule::drain_pending_emission( + netuid, + pending_alpha, + TaoCurrency::ZERO, + AlphaCurrency::ZERO, + AlphaCurrency::ZERO, + ); + let parent_stake_after = SubtensorModule::get_stake_for_hotkey_on_subnet(&parent, netuid); + let child_stake_after = SubtensorModule::get_stake_for_hotkey_on_subnet(&child, netuid); + + let expected_ck_burn = I96F32::from_num(pending_alpha) + * I96F32::from_num(9.0 / 10.0) + * I96F32::from_num(burn_rate); + + let expected_total = I96F32::from_num(pending_alpha) - expected_ck_burn; + let parent_ratio = (I96F32::from_num(pending_alpha) * I96F32::from_num(9.0 / 10.0) + - expected_ck_burn) + / expected_total; + let child_ratio = (I96F32::from_num(pending_alpha) / I96F32::from_num(10)) / expected_total; + + let expected = + I96F32::from_num(stake_before) + I96F32::from_num(pending_alpha) * parent_ratio; + log::info!( + "expected: {:?}, parent_stake_after: {:?}", + expected.to_num::(), + parent_stake_after + ); + + close( + expected.to_num::(), + parent_stake_after.into(), + 3_000_000, + ); + let expected = I96F32::from_num(u64::from(pending_alpha)) * child_ratio; + close( + expected.to_num::(), + child_stake_after.into(), + 3_000_000, + ); + }); +} + #[test] fn test_incentive_is_autostaked_to_owner_destination() { new_test_ext(1).execute_with(|| { diff --git a/precompiles/src/alpha.rs b/precompiles/src/alpha.rs index 42261674d9..29f7cab568 100644 --- a/precompiles/src/alpha.rs +++ b/precompiles/src/alpha.rs @@ -90,6 +90,13 @@ where Ok(U256::from(tao_weight)) } + #[precompile::public("getCKBurn()")] + #[precompile::view] + fn get_ck_burn(_handle: &mut impl PrecompileHandle) -> EvmResult { + let ck_burn = pallet_subtensor::CKBurn::::get(); + Ok(U256::from(ck_burn)) + } + #[precompile::public("simSwapTaoForAlpha(uint16,uint64)")] #[precompile::view] fn sim_swap_tao_for_alpha( diff --git a/precompiles/src/solidity/alpha.abi b/precompiles/src/solidity/alpha.abi index 06975d5e61..14d6eb66dc 100644 --- a/precompiles/src/solidity/alpha.abi +++ b/precompiles/src/solidity/alpha.abi @@ -313,5 +313,18 @@ ], "stateMutability": "view", "type": "function" + }, + { + "inputs": [], + "name": "getCKBurn", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" } ] \ No newline at end of file diff --git a/precompiles/src/solidity/alpha.sol b/precompiles/src/solidity/alpha.sol index e66dcf00d9..c99252ff48 100644 --- a/precompiles/src/solidity/alpha.sol +++ b/precompiles/src/solidity/alpha.sol @@ -94,4 +94,8 @@ interface IAlpha { /// @dev Returns the sum of alpha prices for all subnets. /// @return The sum of alpha prices. function getSumAlphaPrice() external view returns (uint256); + + /// @dev Returns the CK burn rate. + /// @return The CK burn rate. + function getCKBurn() external view returns (uint256); } diff --git a/precompiles/src/solidity/subnet.abi b/precompiles/src/solidity/subnet.abi index 805c0057d9..f2d97e1f90 100644 --- a/precompiles/src/solidity/subnet.abi +++ b/precompiles/src/solidity/subnet.abi @@ -1046,5 +1046,8 @@ "outputs": [], "stateMutability": "payable", "type": "function" + }, + { + "inputs" } ] diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index f0dce12ca0..55dd9fb115 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -220,7 +220,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 308, + spec_version: 309, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1,