// The goal of staking precompile is to allow interaction between EVM users and smart contracts and // subtensor staking functionality, namely add_stake, and remove_stake extrinsicsk, as well as the // staking state. // // Additional requirement is to preserve compatibility with Ethereum indexers, which requires // no balance transfers from EVM accounts without a corresponding transaction that can be // parsed by an indexer. // // Implementation of add_stake: // - User transfers balance that will be staked to the precompile address with a payable // method addStake. This method also takes hotkey public key (bytes32) of the hotkey // that the stake should be assigned to. // - Precompile transfers the balance back to the signing address, and then invokes // do_add_stake from subtensor pallet with signing origin that mmatches to HashedAddressMapping // of the message sender, which will effectively withdraw and stake balance from the message // sender. // - Precompile checks the result of do_add_stake and, in case of a failure, reverts the transaction, // and leaves the balance on the message sender account. // // Implementation of remove_stake: // - User involkes removeStake method and specifies hotkey public key (bytes32) of the hotkey // to remove stake from, and the amount to unstake. // - Precompile calls do_remove_stake method of the subtensor pallet with the signing origin of message // sender, which effectively unstakes the specified amount and credits it to the message sender // - Precompile checks the result of do_remove_stake and, in case of a failure, reverts the transaction. // // Without an approve/allowance system, when an EOA transfers stake to a contract it is impossible for the // contract to know who sent funds and how much. For that reason, the precompile provides an `approve` // function for the sender to approve a spender (the contract) to call `transferStakeFrom`. // The allowance is specific to a pair of `(spender, netuid)`, but doesn't specify the `hotkey` which is instead // provided only in `transferStakeFrom`. use alloc::collections::BTreeSet; use alloc::vec::Vec; use core::marker::PhantomData; use frame_support::Blake2_128Concat; use frame_support::dispatch::{DispatchInfo, GetDispatchInfo, PostDispatchInfo}; use frame_support::pallet_prelude::{StorageDoubleMap, ValueQuery}; use frame_support::traits::{ConstU32, Get, IsSubType, StorageInstance}; use frame_system::RawOrigin; use pallet_evm::{ AddressMapping, BalanceConverter, EvmBalance, ExitError, PrecompileFailure, PrecompileHandle, SubstrateBalance, }; use pallet_subtensor_proxy as pallet_proxy; use precompile_utils::EvmResult; use precompile_utils::prelude::{Address, BoundedVec, revert}; use sp_core::{H160, H256, U256}; use sp_runtime::traits::{AsSystemOriginSigner, Dispatchable, StaticLookup, UniqueSaturatedInto}; use sp_std::vec; use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaBalance, NetUid, ProxyType, Token}; use crate::{PrecompileExt, PrecompileHandleExt}; // `get_stake_for_hotkey_and_coldkey_on_subnet` reads the transitional V1/V2 // share storage. In the V2 fallback case it performs two reads for the initial // share lookup, then five more for the value, share, and denominator. const STAKE_INFO_READS_PER_HOTKEY: u64 = 7; // Conservative charge for decoding and validating each 32-byte hotkey. const STAKE_INFO_INPUT_GAS_PER_HOTKEY: u64 = 64; const MAX_STAKE_INFO_HOTKEYS: usize = 64; const MAX_CONVICTION_HOTKEYS: usize = 64; // Individual state reads the lock row, mode, owner hotkey, global rates, and current block. const COLDKEY_LOCK_READS: u64 = 6; // Aggregate state reads the owner hotkey, global rates, current block, and up to four buckets. const HOTKEY_LOCK_READS: u64 = 8; /// Prefix for the Allowances map in Substrate storage. pub struct AllowancesPrefix; impl StorageInstance for AllowancesPrefix { const STORAGE_PREFIX: &'static str = "Allowances"; fn pallet_prefix() -> &'static str { "EvmPrecompileStaking" } } pub type AllowancesStorage = StorageDoubleMap< AllowancesPrefix, // For each approver (EVM address as only EVM-natives need the precompile) Blake2_128Concat, H160, // For each (spender, netuid, counter) triple — the counter tag invalidates // entries written under a previous registration of the same netuid. Blake2_128Concat, (H160, u16, u64), // Allowed amount U256, ValueQuery, >; // Old StakingPrecompile had ETH-precision in values, which was not alligned with Substrate API. So // it's kinda deprecated, but exists for backward compatibility. Eventually, we should remove it // to stop supporting both precompiles. // // All the future extensions should happen in StakingPrecompileV2. pub struct StakingPrecompileV2(PhantomData); impl PrecompileExt for StakingPrecompileV2 where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + pallet_subtensor::Config + pallet_proxy::Config + pallet_shield::Config + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, R::AccountId: From<[u8; 32]> + Into<[u8; 32]>, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> + From> + GetDispatchInfo + Dispatchable + IsSubType> + IsSubType> + IsSubType> + IsSubType>, ::AddressMapping: AddressMapping, <::Lookup as StaticLookup>::Source: From, { const INDEX: u64 = 2053; } #[precompile_utils::precompile] impl StakingPrecompileV2 where R: frame_system::Config + pallet_balances::Config + pallet_evm::Config + pallet_subtensor::Config + pallet_proxy::Config + pallet_shield::Config + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, R::AccountId: From<[u8; 32]> + Into<[u8; 32]>, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> + From> + GetDispatchInfo + Dispatchable + IsSubType> + IsSubType> + IsSubType> + IsSubType>, ::AddressMapping: AddressMapping, <::Lookup as StaticLookup>::Source: From, { #[precompile::public("addStake(bytes32,uint256,uint256)")] #[precompile::payable] fn add_stake( handle: &mut impl PrecompileHandle, address: H256, amount_rao: U256, netuid: U256, ) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let amount_staked: u64 = amount_rao.unique_saturated_into(); let hotkey = R::AccountId::from(address.0); let netuid = try_u16_from_u256(netuid)?; let call = pallet_subtensor::Call::::add_stake { hotkey, netuid: netuid.into(), amount_staked: amount_staked.into(), }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } #[precompile::public("removeStake(bytes32,uint256,uint256)")] #[precompile::payable] fn remove_stake( handle: &mut impl PrecompileHandle, address: H256, amount_alpha: U256, netuid: U256, ) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let hotkey = R::AccountId::from(address.0); let netuid = try_u16_from_u256(netuid)?; let amount_unstaked: u64 = amount_alpha.unique_saturated_into(); let call = pallet_subtensor::Call::::remove_stake { hotkey, netuid: netuid.into(), amount_unstaked: amount_unstaked.into(), }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } fn call_remove_stake_full_limit( handle: &mut impl PrecompileHandle, hotkey: H256, netuid: U256, limit_price: Option, ) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let hotkey = R::AccountId::from(hotkey.0); let netuid = try_u16_from_u256(netuid)?; let call = pallet_subtensor::Call::::remove_stake_full_limit { hotkey, netuid: netuid.into(), limit_price: limit_price.map(Into::into), }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } #[precompile::public("removeStakeFull(bytes32,uint256)")] #[precompile::payable] fn remove_stake_full( handle: &mut impl PrecompileHandle, hotkey: H256, netuid: U256, ) -> EvmResult<()> { Self::call_remove_stake_full_limit(handle, hotkey, netuid, None) } #[precompile::public("removeStakeFullLimit(bytes32,uint256,uint256)")] #[precompile::payable] fn remove_stake_full_limit( handle: &mut impl PrecompileHandle, hotkey: H256, netuid: U256, limit_price: U256, ) -> EvmResult<()> { let limit_price = try_u64_from_u256(limit_price)?; Self::call_remove_stake_full_limit(handle, hotkey, netuid, Some(limit_price)) } #[precompile::public("moveStake(bytes32,bytes32,uint256,uint256,uint256)")] #[precompile::payable] fn move_stake( handle: &mut impl PrecompileHandle, origin_hotkey: H256, destination_hotkey: H256, origin_netuid: U256, destination_netuid: U256, amount_alpha: U256, ) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let origin_hotkey = R::AccountId::from(origin_hotkey.0); let destination_hotkey = R::AccountId::from(destination_hotkey.0); let origin_netuid = try_u16_from_u256(origin_netuid)?; let destination_netuid = try_u16_from_u256(destination_netuid)?; let alpha_amount: u64 = amount_alpha.unique_saturated_into(); let call = pallet_subtensor::Call::::move_stake { origin_hotkey, destination_hotkey, origin_netuid: origin_netuid.into(), destination_netuid: destination_netuid.into(), alpha_amount: alpha_amount.into(), }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } #[precompile::public("transferStake(bytes32,bytes32,uint256,uint256,uint256)")] #[precompile::payable] fn transfer_stake( handle: &mut impl PrecompileHandle, destination_coldkey: H256, hotkey: H256, origin_netuid: U256, destination_netuid: U256, amount_alpha: U256, ) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let destination_coldkey = R::AccountId::from(destination_coldkey.0); let hotkey = R::AccountId::from(hotkey.0); let origin_netuid = try_u16_from_u256(origin_netuid)?; let destination_netuid = try_u16_from_u256(destination_netuid)?; let alpha_amount: u64 = amount_alpha.unique_saturated_into(); let call = pallet_subtensor::Call::::transfer_stake { destination_coldkey, hotkey, origin_netuid: origin_netuid.into(), destination_netuid: destination_netuid.into(), alpha_amount: alpha_amount.into(), }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } #[precompile::public("burnAlpha(bytes32,uint256,uint256)")] #[precompile::payable] fn burn_alpha( handle: &mut impl PrecompileHandle, hotkey: H256, amount: U256, netuid: U256, ) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let hotkey = R::AccountId::from(hotkey.0); let netuid = try_u16_from_u256(netuid)?; let amount: u64 = amount.unique_saturated_into(); let call = pallet_subtensor::Call::::burn_alpha { hotkey, amount: amount.into(), netuid: netuid.into(), }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } #[precompile::public("getTotalColdkeyStake(bytes32)")] #[precompile::view] fn get_total_coldkey_stake( handle: &mut impl PrecompileHandle, coldkey: H256, ) -> EvmResult { // StakingHotkeys + per-hotkey stake reads handle.record_db_reads::(2)?; let coldkey = R::AccountId::from(coldkey.0); let stake = pallet_subtensor::Pallet::::get_total_stake_for_coldkey(&coldkey); Ok(stake.to_u64().into()) } #[precompile::public("getTotalHotkeyStake(bytes32)")] #[precompile::view] fn get_total_hotkey_stake(handle: &mut impl PrecompileHandle, hotkey: H256) -> EvmResult { // Per-subnet stake + alpha price reads handle.record_db_reads::(2)?; let hotkey = R::AccountId::from(hotkey.0); let stake = pallet_subtensor::Pallet::::get_total_stake_for_hotkey(&hotkey); Ok(stake.to_u64().into()) } #[precompile::public("getStake(bytes32,bytes32,uint256)")] #[precompile::view] fn get_stake( handle: &mut impl PrecompileHandle, hotkey: H256, coldkey: H256, netuid: U256, ) -> EvmResult { // Worst-case V2 fallback reads for the alpha share pool. handle.record_db_reads::(STAKE_INFO_READS_PER_HOTKEY)?; let hotkey = R::AccountId::from(hotkey.0); let coldkey = R::AccountId::from(coldkey.0); let netuid = try_u16_from_u256(netuid)?; let stake = pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( &hotkey, &coldkey, netuid.into(), ); Ok(u64::from(stake).into()) } #[precompile::public("getStakeInfoForColdkeyAndNetuid(bytes32,uint256,bytes32[])")] #[precompile::view] fn get_stake_info_for_coldkey_and_netuid( handle: &mut impl PrecompileHandle, coldkey: H256, netuid: U256, hotkeys: BoundedVec>, ) -> EvmResult> { let hotkey_count: u64 = hotkeys.len().unique_saturated_into(); handle.record_cost(hotkey_count.saturating_mul(STAKE_INFO_INPUT_GAS_PER_HOTKEY))?; let hotkeys: Vec = hotkeys.into(); let coldkey = R::AccountId::from(coldkey.0); let netuid = NetUid::from(try_u16_from_u256(netuid)?); let mut seen = BTreeSet::new(); for hotkey in &hotkeys { if !seen.insert(hotkey) { return Err(revert("duplicate stake info hotkey")); } } // Charge the conservative V2 fallback cost for the complete bounded // batch before performing any stake reads. handle.record_db_reads::(hotkey_count.saturating_mul(STAKE_INFO_READS_PER_HOTKEY))?; Ok(hotkeys .into_iter() .filter_map(|hotkey| { let hotkey_account = R::AccountId::from(hotkey.0); let stake = pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( &hotkey_account, &coldkey, netuid, ) .to_u64(); if stake == 0 { return None; } Some((hotkey, stake.into())) }) .collect()) } #[precompile::public("getAlphaStakedValidators(bytes32,uint256)")] #[precompile::view] fn get_alpha_staked_validators( handle: &mut impl PrecompileHandle, hotkey: H256, netuid: U256, ) -> EvmResult> { let hotkey = R::AccountId::from(hotkey.0); let mut coldkeys: Vec = vec![]; let netuid = NetUid::from(try_u16_from_u256(netuid)?); for (coldkey, netuid_in_alpha, _) in pallet_subtensor::Pallet::::alpha_iter_single_prefix(&hotkey) { handle.record_db_reads::(1)?; if netuid == netuid_in_alpha { let key: [u8; 32] = coldkey.into(); coldkeys.push(key.into()); } } Ok(coldkeys) } #[precompile::public("getTotalAlphaStaked(bytes32,uint256)")] #[precompile::view] fn get_total_alpha_staked( handle: &mut impl PrecompileHandle, hotkey: H256, netuid: U256, ) -> EvmResult { handle.record_db_reads::(2)?; let hotkey = R::AccountId::from(hotkey.0); let netuid = try_u16_from_u256(netuid)?; let stake = pallet_subtensor::Pallet::::get_stake_for_hotkey_on_subnet(&hotkey, netuid.into()); Ok(u64::from(stake).into()) } #[precompile::public("getNominatorMinRequiredStake()")] #[precompile::view] fn get_nominator_min_required_stake(handle: &mut impl PrecompileHandle) -> EvmResult { // NominatorMinRequiredStake + DefaultMinStake reads handle.record_db_reads::(2)?; let stake = pallet_subtensor::Pallet::::get_nominator_min_required_stake(); Ok(stake.into()) } #[precompile::public("getDefaultMinStake()")] #[precompile::view] fn get_default_min_stake(handle: &mut impl PrecompileHandle) -> EvmResult { handle.record_db_reads::(1)?; Ok(pallet_subtensor::DefaultMinStake::::get() .to_u64() .into()) } /// Lock existing subnet alpha and begin building conviction for `hotkey`. #[precompile::public("lockStake(bytes32,uint256,uint256)")] #[precompile::payable] fn lock_stake( handle: &mut impl PrecompileHandle, hotkey: H256, amount_alpha: U256, netuid: U256, ) -> EvmResult<()> { let call = pallet_subtensor::Call::::lock_stake { hotkey: R::AccountId::from(hotkey.0), netuid: NetUid::from(try_u16_from_u256(netuid)?), amount: try_u64_from_u256(amount_alpha)?.into(), }; handle.try_dispatch_runtime_call::( call, RawOrigin::Signed(handle.caller_account_id::()), ) } /// Re-point the caller's lock on a subnet to another hotkey. #[precompile::public("moveLock(bytes32,uint256)")] #[precompile::payable] fn move_lock( handle: &mut impl PrecompileHandle, destination_hotkey: H256, netuid: U256, ) -> EvmResult<()> { let call = pallet_subtensor::Call::::move_lock { destination_hotkey: R::AccountId::from(destination_hotkey.0), netuid: NetUid::from(try_u16_from_u256(netuid)?), }; handle.try_dispatch_runtime_call::( call, RawOrigin::Signed(handle.caller_account_id::()), ) } /// Select perpetual or normally decaying behavior for the caller's lock. #[precompile::public("setPerpetualLock(uint256,bool)")] #[precompile::payable] fn set_perpetual_lock( handle: &mut impl PrecompileHandle, netuid: U256, enabled: bool, ) -> EvmResult<()> { let call = pallet_subtensor::Call::::set_perpetual_lock { netuid: NetUid::from(try_u16_from_u256(netuid)?), enabled, }; handle.try_dispatch_runtime_call::( call, RawOrigin::Signed(handle.caller_account_id::()), ) } /// Set whether the caller rejects incoming stake that carries a lock. #[precompile::public("setRejectLockedAlpha(bool)")] #[precompile::payable] fn set_reject_locked_alpha(handle: &mut impl PrecompileHandle, enabled: bool) -> EvmResult<()> { let call = pallet_subtensor::Call::::set_reject_locked_alpha { enabled }; handle.try_dispatch_runtime_call::( call, RawOrigin::Signed(handle.caller_account_id::()), ) } /// Return one coldkey's lock, rolled forward to the current block. /// /// Conviction is returned as exact unsigned Q64.64 bits. /// `exists` is false if the rolled lock has crossed the cleanup threshold, /// even when its stale storage row has not yet been removed. #[precompile::public("getColdkeyLock(bytes32,uint256)")] #[precompile::view] fn get_coldkey_lock( handle: &mut impl PrecompileHandle, coldkey: H256, netuid: U256, ) -> EvmResult<(bool, H256, U256, u128, bool)> { handle.record_db_reads::(COLDKEY_LOCK_READS)?; let coldkey = R::AccountId::from(coldkey.0); let netuid = NetUid::from(try_u16_from_u256(netuid)?); let perpetual = pallet_subtensor::DecayingLock::::get(&coldkey, netuid) == Some(false); let Some((hotkey, lock)) = pallet_subtensor::Lock::::iter_prefix((&coldkey, netuid)).next() else { return Ok((false, H256::zero(), U256::zero(), 0, perpetual)); }; let now = pallet_subtensor::Pallet::::get_current_block_as_u64(); let owner_lock = hotkey == pallet_subtensor::SubnetOwnerHotkey::::get(netuid); let (lock, _) = pallet_subtensor::staking::lock::ConvictionModel::roll_forward_lock( lock, now, pallet_subtensor::UnlockRate::::get(), pallet_subtensor::MaturityRate::::get(), owner_lock, perpetual, ); let exists = !lock.is_zero(); let hotkey: [u8; 32] = hotkey.into(); Ok(( exists, hotkey.into(), lock.locked_mass.to_u64().into(), lock.conviction.to_bits(), perpetual, )) } /// Return the rolled aggregate lock and conviction for a hotkey. /// /// This combines perpetual and decaying general buckets and, when the /// hotkey is the subnet owner, both owner buckets. #[precompile::public("getHotkeyLock(bytes32,uint256)")] #[precompile::view] fn get_hotkey_lock( handle: &mut impl PrecompileHandle, hotkey: H256, netuid: U256, ) -> EvmResult<(bool, U256, u128)> { handle.record_db_reads::(HOTKEY_LOCK_READS)?; let hotkey = R::AccountId::from(hotkey.0); let netuid = NetUid::from(try_u16_from_u256(netuid)?); let now = pallet_subtensor::Pallet::::get_current_block_as_u64(); let unlock_rate = pallet_subtensor::UnlockRate::::get(); let maturity_rate = pallet_subtensor::MaturityRate::::get(); let is_owner = hotkey == pallet_subtensor::SubnetOwnerHotkey::::get(netuid); let mut locked = AlphaBalance::ZERO; let mut conviction = U64F64::from_bits(0); let mut add_bucket = |maybe_lock: Option, owner_lock: bool, perpetual_lock: bool| { if let Some(lock) = maybe_lock { let (lock, _) = pallet_subtensor::staking::lock::ConvictionModel::roll_forward_lock( lock, now, unlock_rate, maturity_rate, owner_lock, perpetual_lock, ); locked = locked.saturating_add(lock.locked_mass); conviction = conviction.saturating_add(lock.conviction); } }; add_bucket( pallet_subtensor::HotkeyLock::::get(netuid, &hotkey), false, true, ); add_bucket( pallet_subtensor::DecayingHotkeyLock::::get(netuid, &hotkey), false, false, ); if is_owner { add_bucket(pallet_subtensor::OwnerLock::::get(netuid), true, true); add_bucket( pallet_subtensor::DecayingOwnerLock::::get(netuid), true, false, ); } let exists = locked > AlphaBalance::ZERO || conviction > U64F64::from_bits(0); Ok((exists, locked.to_u64().into(), conviction.to_bits())) } /// Return exact rolled conviction for up to 64 distinct candidate hotkeys. /// /// This bounded form avoids the runtime's unbounded all-hotkey scan. The /// returned values align one-for-one with `hotkeys`. #[precompile::public("getHotkeyConvictions(uint256,bytes32[])")] #[precompile::view] fn get_hotkey_convictions( handle: &mut impl PrecompileHandle, netuid: U256, hotkeys: BoundedVec>, ) -> EvmResult> { let hotkey_count: u64 = hotkeys.len().unique_saturated_into(); handle.record_cost(hotkey_count.saturating_mul(STAKE_INFO_INPUT_GAS_PER_HOTKEY))?; let hotkeys: Vec = hotkeys.into(); let netuid = NetUid::from(try_u16_from_u256(netuid)?); let mut seen = BTreeSet::new(); for hotkey in &hotkeys { if !seen.insert(hotkey) { return Err(revert("duplicate conviction hotkey")); } } handle.record_db_reads::(hotkey_count.saturating_mul(HOTKEY_LOCK_READS))?; Ok(hotkeys .into_iter() .map(|hotkey| { let hotkey = R::AccountId::from(hotkey.0); pallet_subtensor::Pallet::::hotkey_conviction(&hotkey, netuid).to_bits() }) .collect()) } /// Return the global lock decay and conviction maturity timescales. #[precompile::public("getLockRates()")] #[precompile::view] fn get_lock_rates(handle: &mut impl PrecompileHandle) -> EvmResult<(u64, u64)> { handle.record_db_reads::(2)?; Ok(( pallet_subtensor::UnlockRate::::get(), pallet_subtensor::MaturityRate::::get(), )) } /// Return whether a coldkey rejects incoming locked alpha. #[precompile::public("getRejectLockedAlpha(bytes32)")] #[precompile::view] fn get_reject_locked_alpha( handle: &mut impl PrecompileHandle, coldkey: H256, ) -> EvmResult { handle.record_db_reads::(1)?; let coldkey = R::AccountId::from(coldkey.0); Ok(pallet_subtensor::Pallet::::account_rejects_locked_alpha( &coldkey, )) } #[precompile::public("addProxy(bytes32)")] #[precompile::payable] fn add_proxy(handle: &mut impl PrecompileHandle, delegate: H256) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let delegate = R::AccountId::from(delegate.0); let delegate = ::Lookup::unlookup(delegate); let call = pallet_proxy::Call::::add_proxy { delegate, proxy_type: ProxyType::Staking, delay: 0u32.into(), }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } #[precompile::public("removeProxy(bytes32)")] #[precompile::payable] fn remove_proxy(handle: &mut impl PrecompileHandle, delegate: H256) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let delegate = R::AccountId::from(delegate.0); let delegate = ::Lookup::unlookup(delegate); let call = pallet_proxy::Call::::remove_proxy { delegate, proxy_type: ProxyType::Staking, delay: 0u32.into(), }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } #[precompile::public("addStakeLimit(bytes32,uint256,uint256,bool,uint256)")] #[precompile::payable] fn add_stake_limit( handle: &mut impl PrecompileHandle, address: H256, amount_rao: U256, limit_price_rao: U256, allow_partial: bool, netuid: U256, ) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let amount_staked: u64 = amount_rao.unique_saturated_into(); let limit_price: u64 = limit_price_rao.unique_saturated_into(); let hotkey = R::AccountId::from(address.0); let netuid = try_u16_from_u256(netuid)?; let call = pallet_subtensor::Call::::add_stake_limit { hotkey, netuid: netuid.into(), amount_staked: amount_staked.into(), limit_price: limit_price.into(), allow_partial, }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } #[precompile::public("removeStakeLimit(bytes32,uint256,uint256,bool,uint256)")] #[precompile::payable] fn remove_stake_limit( handle: &mut impl PrecompileHandle, address: H256, amount_alpha: U256, limit_price_rao: U256, allow_partial: bool, netuid: U256, ) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let hotkey = R::AccountId::from(address.0); let netuid = try_u16_from_u256(netuid)?; let amount_unstaked: u64 = amount_alpha.unique_saturated_into(); let limit_price: u64 = limit_price_rao.unique_saturated_into(); let call = pallet_subtensor::Call::::remove_stake_limit { hotkey, netuid: netuid.into(), amount_unstaked: amount_unstaked.into(), limit_price: limit_price.into(), allow_partial, }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } #[precompile::public("getTotalColdkeyStakeOnSubnet(bytes32,uint256)")] #[precompile::view] fn get_total_coldkey_stake_on_subnet( handle: &mut impl PrecompileHandle, coldkey: H256, netuid: U256, ) -> EvmResult { // StakingHotkeys + per-hotkey stake reads handle.record_db_reads::(2)?; let coldkey = R::AccountId::from(coldkey.0); let netuid = try_u16_from_u256(netuid)?; let stake = pallet_subtensor::Pallet::::get_total_stake_for_coldkey_on_subnet( &coldkey, netuid.into(), ); Ok(stake.to_u64().into()) } /// Current registration counter for `netuid`, used as part of the /// `AllowancesStorage` secondary key to invalidate approvals granted /// for a previous registration of the same netuid. fn current_subnet_counter(netuid: u16) -> u64 { pallet_subtensor::Pallet::::get_registered_subnet_counter(netuid.into()) } #[precompile::public("approve(address,uint256,uint256)")] fn approve( handle: &mut impl PrecompileHandle, spender_address: Address, origin_netuid: U256, amount_alpha: U256, ) -> EvmResult<()> { // AllowancesStorage write + RegisteredSubnetCounter read handle.record_db_reads::(1)?; handle.record_db_writes::(1)?; let approver = handle.context().caller; let spender = spender_address.0; let netuid = try_u16_from_u256(origin_netuid)?; let counter = Self::current_subnet_counter(netuid); if amount_alpha.is_zero() { AllowancesStorage::remove(approver, (spender, netuid, counter)); } else { AllowancesStorage::insert(approver, (spender, netuid, counter), amount_alpha); } Ok(()) } #[precompile::public("allowance(address,address,uint256)")] #[precompile::view] fn allowance( handle: &mut impl PrecompileHandle, source_address: Address, spender_address: Address, origin_netuid: U256, ) -> EvmResult { // AllowancesStorage read + RegisteredSubnetCounter read handle.record_db_reads::(2)?; let spender = spender_address.0; let netuid = try_u16_from_u256(origin_netuid)?; let counter = Self::current_subnet_counter(netuid); Ok(AllowancesStorage::get( source_address.0, (spender, netuid, counter), )) } #[precompile::public("increaseAllowance(address,uint256,uint256)")] fn increase_allowance( handle: &mut impl PrecompileHandle, spender_address: Address, origin_netuid: U256, amount_alpha_increase: U256, ) -> EvmResult<()> { if amount_alpha_increase.is_zero() { return Ok(()); } // AllowancesStorage read + write + RegisteredSubnetCounter read handle.record_db_reads::(2)?; handle.record_db_writes::(1)?; let approver = handle.context().caller; let spender = spender_address.0; let netuid = try_u16_from_u256(origin_netuid)?; let counter = Self::current_subnet_counter(netuid); let approval_key = (spender, netuid, counter); let current_amount = AllowancesStorage::get(approver, approval_key); let new_amount = current_amount.saturating_add(amount_alpha_increase); AllowancesStorage::insert(approver, approval_key, new_amount); Ok(()) } #[precompile::public("decreaseAllowance(address,uint256,uint256)")] fn decrease_allowance( handle: &mut impl PrecompileHandle, spender_address: Address, origin_netuid: U256, amount_alpha_decrease: U256, ) -> EvmResult<()> { if amount_alpha_decrease.is_zero() { return Ok(()); } // AllowancesStorage read + write + RegisteredSubnetCounter read handle.record_db_reads::(2)?; handle.record_db_writes::(1)?; let approver = handle.context().caller; let spender = spender_address.0; let netuid = try_u16_from_u256(origin_netuid)?; let counter = Self::current_subnet_counter(netuid); let approval_key = (spender, netuid, counter); let current_amount = AllowancesStorage::get(approver, approval_key); let new_amount = current_amount.saturating_sub(amount_alpha_decrease); if new_amount.is_zero() { AllowancesStorage::remove(approver, approval_key); } else { AllowancesStorage::insert(approver, approval_key, new_amount); } Ok(()) } fn try_consume_allowance( handle: &mut impl PrecompileHandle, approver: H160, spender: H160, netuid: u16, amount: U256, ) -> EvmResult<()> { if amount.is_zero() { return Ok(()); } // AllowancesStorage read + write + RegisteredSubnetCounter read handle.record_db_reads::(2)?; handle.record_db_writes::(1)?; let counter = Self::current_subnet_counter(netuid); let approval_key = (spender, netuid, counter); let current_amount = AllowancesStorage::get(approver, approval_key); let Some(new_amount) = current_amount.checked_sub(amount) else { return Err(revert("trying to spend more than allowed")); }; if new_amount.is_zero() { AllowancesStorage::remove(approver, approval_key); } else { AllowancesStorage::insert(approver, approval_key, new_amount); } Ok(()) } #[precompile::public("transferStakeFrom(address,address,bytes32,uint256,uint256,uint256)")] fn transfer_stake_from( handle: &mut impl PrecompileHandle, source_address: Address, destination_address: Address, hotkey: H256, origin_netuid: U256, destination_netuid: U256, amount_alpha: U256, ) -> EvmResult<()> { let spender = handle.context().caller; let source_address = source_address.0; let destination_coldkey = ::AddressMapping::into_account_id(destination_address.0); let hotkey = R::AccountId::from(hotkey.0); let origin_netuid = try_u16_from_u256(origin_netuid)?; let destination_netuid = try_u16_from_u256(destination_netuid)?; let alpha_amount: u64 = amount_alpha.unique_saturated_into(); Self::try_consume_allowance(handle, source_address, spender, origin_netuid, amount_alpha)?; let call = pallet_subtensor::Call::::transfer_stake { destination_coldkey, hotkey, origin_netuid: origin_netuid.into(), destination_netuid: destination_netuid.into(), alpha_amount: alpha_amount.into(), }; let source_id = ::AddressMapping::into_account_id(source_address); handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(source_id)) } } // Deprecated, exists for backward compatibility. pub struct StakingPrecompile(PhantomData); impl PrecompileExt for StakingPrecompile where R: frame_system::Config + pallet_evm::Config + pallet_subtensor::Config + pallet_proxy::Config + pallet_balances::Config + pallet_shield::Config + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, R::AccountId: From<[u8; 32]>, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> + From> + From> + GetDispatchInfo + Dispatchable + IsSubType> + IsSubType> + IsSubType> + IsSubType>, ::AddressMapping: AddressMapping, ::Balance: TryFrom, <::Lookup as StaticLookup>::Source: From, { const INDEX: u64 = 2049; } #[precompile_utils::precompile] impl StakingPrecompile where R: frame_system::Config + pallet_evm::Config + pallet_subtensor::Config + pallet_proxy::Config + pallet_balances::Config + pallet_shield::Config + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, R::AccountId: From<[u8; 32]>, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> + From> + From> + GetDispatchInfo + Dispatchable + IsSubType> + IsSubType> + IsSubType> + IsSubType>, ::AddressMapping: AddressMapping, ::Balance: TryFrom, <::Lookup as StaticLookup>::Source: From, { #[precompile::public("addStake(bytes32,uint256)")] #[precompile::payable] fn add_stake(handle: &mut impl PrecompileHandle, address: H256, netuid: U256) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let amount = handle.context().apparent_value; if !amount.is_zero() { Self::transfer_back_to_caller(&account_id, amount)?; } let amount_sub = handle.try_convert_apparent_value::()?; let hotkey = R::AccountId::from(address.0); let netuid = try_u16_from_u256(netuid)?; let amount_staked: u64 = amount_sub.unique_saturated_into(); let call = pallet_subtensor::Call::::add_stake { hotkey, netuid: netuid.into(), amount_staked: amount_staked.into(), }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } #[precompile::public("removeStake(bytes32,uint256,uint256)")] #[precompile::payable] fn remove_stake( handle: &mut impl PrecompileHandle, address: H256, amount: U256, netuid: U256, ) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let hotkey = R::AccountId::from(address.0); let netuid = try_u16_from_u256(netuid)?; let amount = EvmBalance::new(amount); let amount_unstaked = ::BalanceConverter::into_substrate_balance(amount) .map(|amount| amount.into_u64_saturating()) .ok_or(ExitError::OutOfFund)?; let call = pallet_subtensor::Call::::remove_stake { hotkey, netuid: netuid.into(), amount_unstaked: amount_unstaked.into(), }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } #[precompile::public("getTotalColdkeyStake(bytes32)")] #[precompile::view] fn get_total_coldkey_stake( handle: &mut impl PrecompileHandle, coldkey: H256, ) -> EvmResult { // StakingHotkeys + per-hotkey stake reads handle.record_db_reads::(2)?; let coldkey = R::AccountId::from(coldkey.0); // get total stake of coldkey let total_stake = pallet_subtensor::Pallet::::get_total_stake_for_coldkey(&coldkey).to_u64(); // Convert to EVM decimals let stake_u256: SubstrateBalance = total_stake.into(); let stake_eth = ::BalanceConverter::into_evm_balance(stake_u256) .map(|amount| amount.into_u256()) .ok_or(ExitError::InvalidRange)?; Ok(stake_eth) } #[precompile::public("getTotalHotkeyStake(bytes32)")] #[precompile::view] fn get_total_hotkey_stake(handle: &mut impl PrecompileHandle, hotkey: H256) -> EvmResult { // Per-subnet stake + alpha price reads handle.record_db_reads::(2)?; let hotkey = R::AccountId::from(hotkey.0); // get total stake of hotkey let total_stake = pallet_subtensor::Pallet::::get_total_stake_for_hotkey(&hotkey).to_u64(); // Convert to EVM decimals let stake_u256: SubstrateBalance = total_stake.into(); let stake_eth = ::BalanceConverter::into_evm_balance(stake_u256) .map(|amount| amount.into_u256()) .ok_or(ExitError::InvalidRange)?; Ok(stake_eth) } #[precompile::public("getStake(bytes32,bytes32,uint256)")] #[precompile::view] fn get_stake( handle: &mut impl PrecompileHandle, hotkey: H256, coldkey: H256, netuid: U256, ) -> EvmResult { // Worst-case V2 fallback reads for the alpha share pool. handle.record_db_reads::(STAKE_INFO_READS_PER_HOTKEY)?; let hotkey = R::AccountId::from(hotkey.0); let coldkey = R::AccountId::from(coldkey.0); let netuid = try_u16_from_u256(netuid)?; let stake = pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( &hotkey, &coldkey, netuid.into(), ); let stake: SubstrateBalance = u64::from(stake).into(); let stake = ::BalanceConverter::into_evm_balance(stake) .map(|amount| amount.into_u256()) .ok_or(ExitError::InvalidRange)?; Ok(stake) } #[precompile::public("addProxy(bytes32)")] #[precompile::payable] fn add_proxy(handle: &mut impl PrecompileHandle, delegate: H256) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let delegate = R::AccountId::from(delegate.0); let delegate = ::Lookup::unlookup(delegate); let call = pallet_proxy::Call::::add_proxy { delegate, proxy_type: ProxyType::Staking, delay: 0u32.into(), }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } #[precompile::public("removeProxy(bytes32)")] #[precompile::payable] fn remove_proxy(handle: &mut impl PrecompileHandle, delegate: H256) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let delegate = R::AccountId::from(delegate.0); let delegate = ::Lookup::unlookup(delegate); let call = pallet_proxy::Call::::remove_proxy { delegate, proxy_type: ProxyType::Staking, delay: 0u32.into(), }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } fn transfer_back_to_caller( account_id: &::AccountId, amount: U256, ) -> Result<(), PrecompileFailure> { let amount = EvmBalance::new(amount); let amount_sub = ::BalanceConverter::into_substrate_balance(amount) .ok_or(ExitError::OutOfFund)?; // Create a transfer call from the smart contract to the caller let value = amount_sub .into_u64_saturating() .try_into() .map_err(|_| ExitError::Other("Failed to convert u64 to Balance".into()))?; let transfer_call = ::RuntimeCall::from( pallet_balances::Call::::transfer_allow_death { dest: account_id.clone().into(), value, }, ); // Execute the transfer let transfer_result = transfer_call.dispatch(RawOrigin::Signed(Self::account_id()).into()); if let Err(dispatch_error) = transfer_result { log::error!("Transfer back to caller failed. Error: {dispatch_error:?}"); return Err(PrecompileFailure::Error { exit_status: ExitError::Other("Transfer back to caller failed".into()), }); } Ok(()) } } fn try_u16_from_u256(value: U256) -> Result { value.try_into().map_err(|_| PrecompileFailure::Error { exit_status: ExitError::Other("the value is outside of u16 bounds".into()), }) } fn try_u64_from_u256(value: U256) -> Result { value.try_into().map_err(|_| PrecompileFailure::Error { exit_status: ExitError::Other("the value is outside of u64 bounds".into()), }) } #[cfg(test)] mod tests { #![allow( clippy::arithmetic_side_effects, clippy::expect_used, clippy::unwrap_used, clippy::indexing_slicing )] use super::*; use crate::PrecompileExt; use crate::mock::{ AccountId, Proxy, Runtime, RuntimeCall, RuntimeOrigin, addr_from_index, assert_static_call, execute_precompile, fund_account, mapped_account, new_test_ext, precompiles, selector_u32, substrate_to_evm, }; use precompile_utils::prelude::RuntimeHelper; use precompile_utils::solidity::{encode_return_value, encode_with_selector}; use precompile_utils::testing::PrecompileTesterExt; use sp_core::{H160, H256}; use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaBalance, TaoBalance}; const TEST_NETUID_U16: u16 = 1; const SECOND_NETUID_U16: u16 = 2; const INVALID_NETUID_U16: u16 = 12_345; const TEMPO: u16 = 100; const RESERVE_TAO: u64 = 200_000_000_000; const RESERVE_ALPHA: u64 = 100_000_000_000; const INITIAL_STAKE_RAO: u64 = 20_000_000_000; const REMOVE_STAKE_RAO: u64 = 10_000_000_000; const PROXY_STAKE_RAO: u64 = 1_000_000_000; const COLDKEY_BALANCE: u64 = 100_000_000_000; const APPROVED_ALLOWANCE_RAO: u64 = 10_000_000_000; const TRANSFERRED_ALLOWANCE_RAO: u64 = 5_000_000_000; const ALLOWANCE_DECREASE_RAO: u64 = 2_000_000_000; fn setup_staking_subnet() -> NetUid { setup_staking_subnet_id(TEST_NETUID_U16) } fn setup_staking_subnet_id(netuid: u16) -> NetUid { let netuid = NetUid::from(netuid); pallet_subtensor::Pallet::::init_new_network(netuid, TEMPO); pallet_subtensor::Pallet::::set_network_registration_allowed(netuid, true); pallet_subtensor::Pallet::::set_max_allowed_uids(netuid, 4096); pallet_subtensor::FirstEmissionBlockNumber::::insert(netuid, 0); pallet_subtensor::SubtokenEnabled::::insert(netuid, true); pallet_subtensor::BurnHalfLife::::insert(netuid, 1); pallet_subtensor::BurnIncreaseMult::::insert(netuid, U64F64::from_num(1)); pallet_subtensor::SubnetTAO::::insert(netuid, TaoBalance::from(RESERVE_TAO)); pallet_subtensor::SubnetAlphaIn::::insert( netuid, AlphaBalance::from(RESERVE_ALPHA), ); netuid } fn stake_read_cost(hotkey_count: usize) -> u64 { let hotkey_count = u64::try_from(hotkey_count).expect("hotkey count fits in u64"); let reads = hotkey_count.saturating_mul(STAKE_INFO_READS_PER_HOTKEY); RuntimeHelper::::db_read_gas_cost().saturating_mul(reads) } fn stake_info_validation_cost(hotkey_count: usize) -> u64 { let hotkey_count = u64::try_from(hotkey_count).expect("hotkey count fits in u64"); STAKE_INFO_INPUT_GAS_PER_HOTKEY.saturating_mul(hotkey_count) } fn stake_info_cost(hotkey_count: usize) -> u64 { stake_info_validation_cost(hotkey_count).saturating_add(stake_read_cost(hotkey_count)) } fn hotkey() -> AccountId { AccountId::from([0x11; 32]) } fn delegate() -> AccountId { AccountId::from([0x22; 32]) } fn ensure_hotkey_exists(hotkey: &AccountId) { pallet_subtensor::Owner::::insert(hotkey, hotkey.clone()); } fn stake_for(hotkey: &AccountId, coldkey: &AccountId, netuid: NetUid) -> u64 { pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( hotkey, coldkey, netuid, ) .into() } fn total_coldkey_stake_on_subnet(coldkey: &AccountId, netuid: NetUid) -> u64 { pallet_subtensor::Pallet::::get_total_stake_for_coldkey_on_subnet(coldkey, netuid) .into() } fn add_stake_v1(caller: H160, hotkey: &AccountId, netuid: u16, amount_rao: u64) { ensure_hotkey_exists(hotkey); fund_account(&StakingPrecompile::::account_id(), amount_rao); let result = execute_precompile( &precompiles::>(), addr_from_index(StakingPrecompile::::INDEX), caller, encode_with_selector( selector_u32("addStake(bytes32,uint256)"), (H256::from_slice(hotkey.as_ref()), U256::from(netuid)), ), substrate_to_evm(amount_rao), ) .expect("staking v1 add stake should route to the precompile"); assert!(result.is_ok()); } fn add_stake_v2(caller: H160, hotkey: &AccountId, netuid: u16, amount_rao: u64) { ensure_hotkey_exists(hotkey); precompiles::>() .prepare_test( caller, addr_from_index(StakingPrecompileV2::::INDEX), encode_with_selector( selector_u32("addStake(bytes32,uint256,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::from(amount_rao), U256::from(netuid), ), ), ) .execute_returns(()); } fn assert_proxy_effects(caller: H160, netuid: NetUid) { let caller_account = mapped_account(caller); let hotkey = hotkey(); let delegate = delegate(); ensure_hotkey_exists(&hotkey); let proxies = pallet_subtensor_proxy::Proxies::::get(&caller_account).0; assert_eq!(proxies.len(), 1); assert_eq!(proxies[0].delegate, delegate); let stake_before = stake_for(&hotkey, &caller_account, netuid); let proxied_call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::add_stake { hotkey: hotkey.clone(), netuid, amount_staked: PROXY_STAKE_RAO.into(), }); let proxy_result = Proxy::proxy( RuntimeOrigin::signed(delegate.clone()), caller_account.clone().into(), Some(ProxyType::Staking), Box::new(proxied_call), ); assert!(proxy_result.is_ok()); let stake_after = stake_for(&hotkey, &caller_account, netuid); assert!(stake_after > stake_before); } fn setup_approval_state() -> (NetUid, H160, H160, AccountId, AccountId, AccountId) { let netuid = setup_staking_subnet(); let source = addr_from_index(0x2001); let spender = addr_from_index(0x2002); let source_account = mapped_account(source); let spender_account = mapped_account(spender); let hotkey = hotkey(); fund_account(&source_account, COLDKEY_BALANCE); add_stake_v2(source, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); ( netuid, source, spender, source_account, spender_account, hotkey, ) } fn assert_allowance(source: H160, spender: H160, caller: H160, expected: U256) { assert_static_call( &precompiles::>(), caller, addr_from_index(StakingPrecompileV2::::INDEX), encode_with_selector( selector_u32("allowance(address,address,uint256)"), ( precompile_utils::solidity::codec::Address(source), precompile_utils::solidity::codec::Address(spender), U256::from(TEST_NETUID_U16), ), ), expected, ); } #[test] fn staking_precompile_v2_returns_non_zero_stake_positions_for_requested_hotkeys() { new_test_ext().execute_with(|| { let netuid = setup_staking_subnet(); setup_staking_subnet_id(SECOND_NETUID_U16); let caller = addr_from_index(0x1101); let coldkey = mapped_account(caller); let hotkey_a = AccountId::from([0x31; 32]); let hotkey_b = AccountId::from([0x32; 32]); let hotkey_c = AccountId::from([0x33; 32]); fund_account(&coldkey, COLDKEY_BALANCE); add_stake_v2(caller, &hotkey_a, TEST_NETUID_U16, INITIAL_STAKE_RAO); add_stake_v2(caller, &hotkey_b, SECOND_NETUID_U16, INITIAL_STAKE_RAO); add_stake_v2(caller, &hotkey_c, TEST_NETUID_U16, INITIAL_STAKE_RAO); let requested_hotkeys = vec![ H256::from_slice(hotkey_a.as_ref()), H256::from_slice(hotkey_b.as_ref()), H256::from_slice(hotkey_c.as_ref()), ]; let expected: Vec<(H256, U256)> = [&hotkey_a, &hotkey_b, &hotkey_c] .into_iter() .filter_map(|hotkey| { let stake = stake_for(hotkey, &coldkey, netuid); (stake > 0).then(|| (H256::from_slice(hotkey.as_ref()), U256::from(stake))) }) .collect(); assert_eq!(expected.len(), 2); precompiles::>() .prepare_test( caller, addr_from_index(StakingPrecompileV2::::INDEX), encode_with_selector( selector_u32("getStakeInfoForColdkeyAndNetuid(bytes32,uint256,bytes32[])"), ( H256::from_slice(coldkey.as_ref()), U256::from(TEST_NETUID_U16), requested_hotkeys, ), ), ) .with_static_call(true) .expect_cost(stake_info_cost(3)) .execute_returns_raw(encode_return_value(expected)); }); } #[test] fn staking_precompile_v2_returns_empty_stake_info_for_unknown_coldkey() { new_test_ext().execute_with(|| { setup_staking_subnet(); let caller = addr_from_index(0x1102); let coldkey = AccountId::from([0x41; 32]); let hotkeys = vec![H256::repeat_byte(0x51), H256::repeat_byte(0x52)]; precompiles::>() .prepare_test( caller, addr_from_index(StakingPrecompileV2::::INDEX), encode_with_selector( selector_u32("getStakeInfoForColdkeyAndNetuid(bytes32,uint256,bytes32[])"), ( H256::from_slice(coldkey.as_ref()), U256::from(TEST_NETUID_U16), hotkeys, ), ), ) .with_static_call(true) .expect_cost(stake_info_cost(2)) .execute_returns_raw(encode_return_value(Vec::<(H256, U256)>::new())); }); } #[test] fn staking_precompile_v2_rejects_out_of_range_stake_info_netuid() { new_test_ext().execute_with(|| { let caller = addr_from_index(0x1103); let coldkey = AccountId::from([0x42; 32]); let invalid_netuid = U256::from(u32::from(u16::MAX) + 1); let result = execute_precompile( &precompiles::>(), addr_from_index(StakingPrecompileV2::::INDEX), caller, encode_with_selector( selector_u32("getStakeInfoForColdkeyAndNetuid(bytes32,uint256,bytes32[])"), ( H256::from_slice(coldkey.as_ref()), invalid_netuid, Vec::::new(), ), ), U256::zero(), ) .expect("staking V2 call routes to the precompile"); assert!(result.is_err()); }); } #[test] fn staking_precompile_v2_only_reads_caller_supplied_hotkeys() { new_test_ext().execute_with(|| { let netuid = setup_staking_subnet(); let caller = addr_from_index(0x1105); let coldkey = mapped_account(caller); let historical_hotkeys: Vec = (0..=MAX_STAKE_INFO_HOTKEYS) .map(|index| { let mut account = [0u8; 32]; let index = u64::try_from(index).expect("test index fits in u64"); account[..8].copy_from_slice(&index.to_le_bytes()); AccountId::from(account) }) .collect(); let active_hotkey = historical_hotkeys .last() .expect("historical hotkeys is non-empty") .clone(); pallet_subtensor::StakingHotkeys::::insert( &coldkey, historical_hotkeys.clone(), ); pallet_subtensor::Pallet::::increase_stake_for_hotkey_and_coldkey_on_subnet( &active_hotkey, &coldkey, netuid, AlphaBalance::from(INITIAL_STAKE_RAO), ); let active_stake = stake_for(&active_hotkey, &coldkey, netuid); assert!(active_stake > 0); precompiles::>() .prepare_test( caller, addr_from_index(StakingPrecompileV2::::INDEX), encode_with_selector( selector_u32("getStakeInfoForColdkeyAndNetuid(bytes32,uint256,bytes32[])"), ( H256::from_slice(coldkey.as_ref()), U256::from(TEST_NETUID_U16), vec![H256::from_slice(active_hotkey.as_ref())], ), ), ) .with_static_call(true) .expect_cost(stake_info_cost(1)) .execute_returns_raw(encode_return_value(vec![( H256::from_slice(active_hotkey.as_ref()), U256::from(active_stake), )])); }); } #[test] fn staking_precompile_v2_codec_rejects_more_than_64_requested_hotkeys() { new_test_ext().execute_with(|| { setup_staking_subnet(); let caller = addr_from_index(0x1106); let coldkey = AccountId::from([0x43; 32]); let hotkeys: Vec = (0..=MAX_STAKE_INFO_HOTKEYS) .map(|index| { let mut hotkey = [0u8; 32]; hotkey[..8].copy_from_slice(&index.to_le_bytes()); H256::from(hotkey) }) .collect(); precompiles::>() .prepare_test( caller, addr_from_index(StakingPrecompileV2::::INDEX), encode_with_selector( selector_u32("getStakeInfoForColdkeyAndNetuid(bytes32,uint256,bytes32[])"), ( H256::from_slice(coldkey.as_ref()), U256::from(TEST_NETUID_U16), hotkeys[..MAX_STAKE_INFO_HOTKEYS].to_vec(), ), ), ) .with_static_call(true) .expect_cost(stake_info_cost(MAX_STAKE_INFO_HOTKEYS)) .execute_returns_raw(encode_return_value(Vec::<(H256, U256)>::new())); precompiles::>() .prepare_test( caller, addr_from_index(StakingPrecompileV2::::INDEX), encode_with_selector( selector_u32("getStakeInfoForColdkeyAndNetuid(bytes32,uint256,bytes32[])"), ( H256::from_slice(coldkey.as_ref()), U256::from(TEST_NETUID_U16), hotkeys, ), ), ) .with_static_call(true) .expect_cost(0) .execute_reverts(|output| output == b"hotkeys: Value is too large for length"); }); } #[test] fn staking_precompile_v2_rejects_duplicate_requested_hotkeys() { new_test_ext().execute_with(|| { let netuid = setup_staking_subnet(); let caller = addr_from_index(0x1107); let coldkey = mapped_account(caller); let hotkey = AccountId::from([0x54; 32]); let requested_hotkey = H256::from_slice(hotkey.as_ref()); fund_account(&coldkey, COLDKEY_BALANCE); add_stake_v2(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); assert!(stake_for(&hotkey, &coldkey, netuid) > 0); precompiles::>() .prepare_test( caller, addr_from_index(StakingPrecompileV2::::INDEX), encode_with_selector( selector_u32("getStakeInfoForColdkeyAndNetuid(bytes32,uint256,bytes32[])"), ( H256::from_slice(coldkey.as_ref()), U256::from(TEST_NETUID_U16), vec![requested_hotkey, H256::repeat_byte(0x55), requested_hotkey], ), ), ) .with_static_call(true) .expect_cost(stake_info_validation_cost(3)) .execute_reverts(|output| output == b"duplicate stake info hotkey"); }); } #[test] fn staking_precompile_v2_returns_default_min_stake() { new_test_ext().execute_with(|| { let threshold = pallet_subtensor::DefaultMinStake::::get(); precompiles::>() .prepare_test( addr_from_index(0x1104), addr_from_index(StakingPrecompileV2::::INDEX), encode_with_selector(selector_u32("getDefaultMinStake()"), ()), ) .with_static_call(true) .expect_cost(RuntimeHelper::::db_read_gas_cost()) .execute_returns(U256::from(threshold.to_u64())); }); } #[test] fn staking_precompile_v2_manages_and_reads_stake_lock_lifecycle() { new_test_ext().execute_with(|| { let netuid = setup_staking_subnet(); let caller = addr_from_index(0x1120); let coldkey = mapped_account(caller); let origin_hotkey = AccountId::from([0x61; 32]); let destination_hotkey = AccountId::from([0x62; 32]); let locked = 8_000_000_000_u64; let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); fund_account(&coldkey, COLDKEY_BALANCE); add_stake_v2(caller, &origin_hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); pallet_subtensor::Owner::::insert(&origin_hotkey, &coldkey); pallet_subtensor::Owner::::insert(&destination_hotkey, &coldkey); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("lockStake(bytes32,uint256,uint256)"), ( H256::from_slice(origin_hotkey.as_ref()), U256::from(locked), U256::from(TEST_NETUID_U16), ), ), ) .execute_returns(()); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("setPerpetualLock(uint256,bool)"), (U256::from(TEST_NETUID_U16), true), ), ) .execute_returns(()); assert_eq!( pallet_subtensor::DecayingLock::::get(&coldkey, netuid), Some(false) ); frame_system::Pallet::::set_block_number(1_000); let expected = pallet_subtensor::Pallet::::get_coldkey_lock(&coldkey, netuid) .expect("lock exists"); assert_eq!(expected.locked_mass.to_u64(), locked); assert!(expected.conviction > U64F64::from_num(0)); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("getColdkeyLock(bytes32,uint256)"), ( H256::from_slice(coldkey.as_ref()), U256::from(TEST_NETUID_U16), ), ), ) .with_static_call(true) .expect_cost( RuntimeHelper::::db_read_gas_cost().saturating_mul(COLDKEY_LOCK_READS), ) .execute_returns(( true, H256::from_slice(origin_hotkey.as_ref()), U256::from(expected.locked_mass.to_u64()), expected.conviction.to_bits(), true, )); let expected_hotkey_conviction = pallet_subtensor::Pallet::::hotkey_conviction(&origin_hotkey, netuid); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("getHotkeyLock(bytes32,uint256)"), ( H256::from_slice(origin_hotkey.as_ref()), U256::from(TEST_NETUID_U16), ), ), ) .with_static_call(true) .expect_cost( RuntimeHelper::::db_read_gas_cost().saturating_mul(HOTKEY_LOCK_READS), ) .execute_returns(( true, U256::from(expected.locked_mass.to_u64()), expected_hotkey_conviction.to_bits(), )); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("moveLock(bytes32,uint256)"), ( H256::from_slice(destination_hotkey.as_ref()), U256::from(TEST_NETUID_U16), ), ), ) .execute_returns(()); let moved = pallet_subtensor::Lock::::get((&coldkey, netuid, &destination_hotkey)) .expect("lock moved to destination"); assert_eq!(moved.locked_mass, expected.locked_mass); assert_eq!(moved.conviction, expected.conviction); assert!(!pallet_subtensor::Lock::::contains_key(( &coldkey, netuid, &origin_hotkey ))); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("setPerpetualLock(uint256,bool)"), (U256::from(TEST_NETUID_U16), false), ), ) .execute_returns(()); frame_system::Pallet::::set_block_number(2_000); let decayed = pallet_subtensor::Pallet::::get_coldkey_lock(&coldkey, netuid) .expect("decaying lock remains above the cleanup threshold"); assert!(decayed.locked_mass < moved.locked_mass); assert_eq!( pallet_subtensor::DecayingLock::::get(&coldkey, netuid), None ); }); } #[test] fn staking_precompile_v2_exposes_lock_rates_conviction_batches_and_recipient_flag() { new_test_ext().execute_with(|| { let netuid = setup_staking_subnet(); let caller = addr_from_index(0x1121); let coldkey = mapped_account(caller); let hotkey = AccountId::from([0x63; 32]); let empty_hotkey = AccountId::from([0x64; 32]); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); fund_account(&coldkey, COLDKEY_BALANCE); add_stake_v2(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); ensure_hotkey_exists(&empty_hotkey); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("lockStake(bytes32,uint256,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::from(5_000_000_000_u64), U256::from(TEST_NETUID_U16), ), ), ) .execute_returns(()); frame_system::Pallet::::set_block_number(1_000); precompiles .prepare_test( caller, precompile_addr, encode_with_selector(selector_u32("getLockRates()"), ()), ) .with_static_call(true) .expect_cost(RuntimeHelper::::db_read_gas_cost().saturating_mul(2)) .execute_returns(( pallet_subtensor::UnlockRate::::get(), pallet_subtensor::MaturityRate::::get(), )); let candidates = vec![ H256::from_slice(hotkey.as_ref()), H256::from_slice(empty_hotkey.as_ref()), ]; let expected = vec![ pallet_subtensor::Pallet::::hotkey_conviction(&hotkey, netuid).to_bits(), 0, ]; precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("getHotkeyConvictions(uint256,bytes32[])"), (U256::from(TEST_NETUID_U16), candidates), ), ) .with_static_call(true) .expect_cost( stake_info_validation_cost(2).saturating_add( RuntimeHelper::::db_read_gas_cost() .saturating_mul(2 * HOTKEY_LOCK_READS), ), ) .execute_returns(expected); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("getRejectLockedAlpha(bytes32)"), (H256::from_slice(coldkey.as_ref()),), ), ) .with_static_call(true) .expect_cost(RuntimeHelper::::db_read_gas_cost()) .execute_returns(true); precompiles .prepare_test( caller, precompile_addr, encode_with_selector(selector_u32("setRejectLockedAlpha(bool)"), (false,)), ) .execute_returns(()); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("getRejectLockedAlpha(bytes32)"), (H256::from_slice(coldkey.as_ref()),), ), ) .with_static_call(true) .expect_cost(RuntimeHelper::::db_read_gas_cost()) .execute_returns(false); }); } #[test] fn staking_precompile_v2_reports_empty_lock_and_preconfigured_perpetual_mode() { new_test_ext().execute_with(|| { setup_staking_subnet(); let caller = addr_from_index(0x1123); let coldkey = mapped_account(caller); let hotkey = hotkey(); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("setPerpetualLock(uint256,bool)"), (U256::from(TEST_NETUID_U16), true), ), ) .execute_returns(()); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("getColdkeyLock(bytes32,uint256)"), ( H256::from_slice(coldkey.as_ref()), U256::from(TEST_NETUID_U16), ), ), ) .with_static_call(true) .expect_cost( RuntimeHelper::::db_read_gas_cost().saturating_mul(COLDKEY_LOCK_READS), ) .execute_returns((false, H256::zero(), U256::zero(), 0_u128, true)); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("getHotkeyLock(bytes32,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::from(TEST_NETUID_U16), ), ), ) .with_static_call(true) .expect_cost( RuntimeHelper::::db_read_gas_cost().saturating_mul(HOTKEY_LOCK_READS), ) .execute_returns((false, U256::zero(), 0_u128)); }); } #[test] fn staking_precompile_v2_reports_expired_unpersisted_locks_as_nonexistent() { new_test_ext().execute_with(|| { let netuid = setup_staking_subnet(); let caller = addr_from_index(0x1124); let coldkey = mapped_account(caller); let hotkey = AccountId::from([0x65; 32]); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); fund_account(&coldkey, COLDKEY_BALANCE); add_stake_v2(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); pallet_subtensor::SubnetOwnerHotkey::::insert(netuid, &hotkey); pallet_subtensor::UnlockRate::::set(1); pallet_subtensor::MaturityRate::::set(1); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("lockStake(bytes32,uint256,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::from(5_000_000_000_u64), U256::from(TEST_NETUID_U16), ), ), ) .execute_returns(()); assert!(pallet_subtensor::Lock::::contains_key(( &coldkey, netuid, &hotkey ))); assert!(pallet_subtensor::DecayingOwnerLock::::contains_key(netuid)); frame_system::Pallet::::set_block_number(100); let raw_lock = pallet_subtensor::Lock::::get((&coldkey, netuid, &hotkey)) .expect("stale individual lock remains in storage"); let (rolled_lock, _) = pallet_subtensor::staking::lock::ConvictionModel::roll_forward_lock( raw_lock, 100, pallet_subtensor::UnlockRate::::get(), pallet_subtensor::MaturityRate::::get(), true, false, ); assert!(rolled_lock.is_zero()); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("getColdkeyLock(bytes32,uint256)"), ( H256::from_slice(coldkey.as_ref()), U256::from(TEST_NETUID_U16), ), ), ) .with_static_call(true) .expect_cost( RuntimeHelper::::db_read_gas_cost().saturating_mul(COLDKEY_LOCK_READS), ) .execute_returns(( false, H256::from_slice(hotkey.as_ref()), U256::zero(), 0_u128, false, )); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("getHotkeyLock(bytes32,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::from(TEST_NETUID_U16), ), ), ) .with_static_call(true) .expect_cost( RuntimeHelper::::db_read_gas_cost().saturating_mul(HOTKEY_LOCK_READS), ) .execute_returns((false, U256::zero(), 0_u128)); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("getHotkeyConvictions(uint256,bytes32[])"), ( U256::from(TEST_NETUID_U16), vec![H256::from_slice(hotkey.as_ref())], ), ), ) .with_static_call(true) .expect_cost(stake_info_validation_cost(1).saturating_add( RuntimeHelper::::db_read_gas_cost().saturating_mul(HOTKEY_LOCK_READS), )) .execute_returns(vec![0_u128]); // View calls roll state in memory only, so this exercises stale rows. assert!(pallet_subtensor::Lock::::contains_key(( &coldkey, netuid, &hotkey ))); assert!(pallet_subtensor::DecayingOwnerLock::::contains_key(netuid)); }); } #[test] fn staking_precompile_v2_rejects_invalid_lock_inputs() { new_test_ext().execute_with(|| { setup_staking_subnet(); let caller = addr_from_index(0x1122); let coldkey = mapped_account(caller); let hotkey = hotkey(); fund_account(&coldkey, COLDKEY_BALANCE); add_stake_v2(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); precompiles::>() .prepare_test( caller, addr_from_index(StakingPrecompileV2::::INDEX), encode_with_selector( selector_u32("lockStake(bytes32,uint256,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::from(u64::MAX) + U256::one(), U256::from(TEST_NETUID_U16), ), ), ) .execute_error(ExitError::Other( "the value is outside of u64 bounds".into(), )); let requested = H256::from_slice(hotkey.as_ref()); precompiles::>() .prepare_test( caller, addr_from_index(StakingPrecompileV2::::INDEX), encode_with_selector( selector_u32("getHotkeyConvictions(uint256,bytes32[])"), (U256::from(TEST_NETUID_U16), vec![requested, requested]), ), ) .with_static_call(true) .expect_cost(stake_info_validation_cost(2)) .execute_reverts(|output| output == b"duplicate conviction hotkey"); let too_many_hotkeys = vec![requested; MAX_CONVICTION_HOTKEYS + 1]; precompiles::>() .prepare_test( caller, addr_from_index(StakingPrecompileV2::::INDEX), encode_with_selector( selector_u32("getHotkeyConvictions(uint256,bytes32[])"), (U256::from(TEST_NETUID_U16), too_many_hotkeys), ), ) .with_static_call(true) .expect_cost(0) .execute_reverts(|output| output == b"hotkeys: Value is too large for length"); }); } #[test] fn staking_precompile_v1_add_stake_and_reads_match_runtime_state() { new_test_ext().execute_with(|| { let netuid = setup_staking_subnet(); let caller = addr_from_index(0x1001); let caller_account = mapped_account(caller); let hotkey = hotkey(); fund_account(&caller_account, COLDKEY_BALANCE); let stake_before = stake_for(&hotkey, &caller_account, netuid); add_stake_v1(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); let stake_after = stake_for(&hotkey, &caller_account, netuid); assert!(stake_after > stake_before); precompiles::>() .prepare_test( caller, addr_from_index(StakingPrecompile::::INDEX), encode_with_selector( selector_u32("getStake(bytes32,bytes32,uint256)"), ( H256::from_slice(hotkey.as_ref()), H256::from_slice(caller_account.as_ref()), U256::from(TEST_NETUID_U16), ), ), ) .with_static_call(true) .expect_cost(stake_read_cost(1)) .execute_returns(substrate_to_evm(stake_after)); }); } #[test] fn staking_precompile_v2_add_stake_and_reads_match_runtime_state() { new_test_ext().execute_with(|| { let netuid = setup_staking_subnet(); let caller = addr_from_index(0x1002); let caller_account = mapped_account(caller); let hotkey = hotkey(); fund_account(&caller_account, COLDKEY_BALANCE); let stake_before = stake_for(&hotkey, &caller_account, netuid); add_stake_v2(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); let stake_after = stake_for(&hotkey, &caller_account, netuid); let total_coldkey_stake = total_coldkey_stake_on_subnet(&caller_account, netuid); assert!(stake_after > stake_before); assert!(total_coldkey_stake >= stake_after); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("getStake(bytes32,bytes32,uint256)"), ( H256::from_slice(hotkey.as_ref()), H256::from_slice(caller_account.as_ref()), U256::from(TEST_NETUID_U16), ), ), ) .with_static_call(true) .expect_cost(stake_read_cost(1)) .execute_returns(U256::from(stake_after)); assert_static_call( &precompiles, caller, precompile_addr, encode_with_selector( selector_u32("getTotalColdkeyStakeOnSubnet(bytes32,uint256)"), ( H256::from_slice(caller_account.as_ref()), U256::from(TEST_NETUID_U16), ), ), U256::from(total_coldkey_stake), ); }); } #[test] fn staking_precompile_v1_rejects_missing_subnet() { new_test_ext().execute_with(|| { let caller = addr_from_index(0x1003); let caller_account = mapped_account(caller); let hotkey = hotkey(); fund_account(&caller_account, COLDKEY_BALANCE); ensure_hotkey_exists(&hotkey); fund_account( &StakingPrecompile::::account_id(), INITIAL_STAKE_RAO, ); let rejected = execute_precompile( &precompiles::>(), addr_from_index(StakingPrecompile::::INDEX), caller, encode_with_selector( selector_u32("addStake(bytes32,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::from(INVALID_NETUID_U16), ), ), substrate_to_evm(INITIAL_STAKE_RAO), ) .expect("staking v1 add stake should route to the precompile"); assert!(rejected.is_err()); assert_eq!( stake_for(&hotkey, &caller_account, NetUid::from(INVALID_NETUID_U16)), 0, ); }); } #[test] fn staking_precompile_v2_rejects_missing_subnet() { new_test_ext().execute_with(|| { let caller = addr_from_index(0x1004); let caller_account = mapped_account(caller); let hotkey = hotkey(); fund_account(&caller_account, COLDKEY_BALANCE); ensure_hotkey_exists(&hotkey); let rejected = execute_precompile( &precompiles::>(), addr_from_index(StakingPrecompileV2::::INDEX), caller, encode_with_selector( selector_u32("addStake(bytes32,uint256,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::from(INITIAL_STAKE_RAO), U256::from(INVALID_NETUID_U16), ), ), U256::zero(), ) .expect("staking v2 add stake should route to the precompile"); assert!(rejected.is_err()); assert_eq!( stake_for(&hotkey, &caller_account, NetUid::from(INVALID_NETUID_U16)), 0, ); }); } #[test] fn staking_precompile_v1_remove_stake_reduces_stake() { new_test_ext().execute_with(|| { let netuid = setup_staking_subnet(); let caller = addr_from_index(0x1005); let caller_account = mapped_account(caller); let hotkey = hotkey(); fund_account(&caller_account, COLDKEY_BALANCE); add_stake_v1(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompile::::INDEX); let stake_before = stake_for(&hotkey, &caller_account, netuid); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("removeStake(bytes32,uint256,uint256)"), ( H256::from_slice(hotkey.as_ref()), substrate_to_evm(REMOVE_STAKE_RAO), U256::from(TEST_NETUID_U16), ), ), ) .execute_returns(()); let stake_after = stake_for(&hotkey, &caller_account, netuid); assert_eq!(stake_after, stake_before - REMOVE_STAKE_RAO); }); } #[test] fn staking_precompile_v2_remove_stake_reduces_stake() { new_test_ext().execute_with(|| { let netuid = setup_staking_subnet(); let caller = addr_from_index(0x1006); let caller_account = mapped_account(caller); let hotkey = hotkey(); fund_account(&caller_account, COLDKEY_BALANCE); add_stake_v2(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); let stake_before = stake_for(&hotkey, &caller_account, netuid); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("removeStake(bytes32,uint256,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::from(REMOVE_STAKE_RAO), U256::from(TEST_NETUID_U16), ), ), ) .execute_returns(()); let stake_after = stake_for(&hotkey, &caller_account, netuid); assert_eq!(stake_after, stake_before - REMOVE_STAKE_RAO); }); } #[test] fn staking_precompile_v2_add_stake_limit_increases_stake() { new_test_ext().execute_with(|| { let netuid = setup_staking_subnet(); let caller = addr_from_index(0x4001); let caller_account = mapped_account(caller); let hotkey = hotkey(); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); fund_account(&caller_account, COLDKEY_BALANCE); ensure_hotkey_exists(&hotkey); let stake_before = stake_for(&hotkey, &caller_account, netuid); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("addStakeLimit(bytes32,uint256,uint256,bool,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::from(INITIAL_STAKE_RAO), U256::from(1_000_000_000_000_u64), true, U256::from(TEST_NETUID_U16), ), ), ) .execute_returns(()); assert!(stake_for(&hotkey, &caller_account, netuid) > stake_before); }); } #[test] fn staking_precompile_v2_remove_stake_limit_decreases_stake() { new_test_ext().execute_with(|| { let netuid = setup_staking_subnet(); let caller = addr_from_index(0x4002); let caller_account = mapped_account(caller); let hotkey = hotkey(); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); fund_account(&caller_account, COLDKEY_BALANCE); ensure_hotkey_exists(&hotkey); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("addStakeLimit(bytes32,uint256,uint256,bool,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::from(INITIAL_STAKE_RAO), U256::from(1_000_000_000_000_u64), true, U256::from(TEST_NETUID_U16), ), ), ) .execute_returns(()); let stake_before = stake_for(&hotkey, &caller_account, netuid); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("removeStakeLimit(bytes32,uint256,uint256,bool,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::from(REMOVE_STAKE_RAO), U256::from(1_000_000_000_u64), true, U256::from(TEST_NETUID_U16), ), ), ) .execute_returns(()); assert!(stake_for(&hotkey, &caller_account, netuid) < stake_before); }); } #[test] fn staking_precompile_v2_remove_stake_full_limit_clears_stake() { new_test_ext().execute_with(|| { let netuid = setup_staking_subnet(); let caller = addr_from_index(0x4003); let caller_account = mapped_account(caller); let hotkey = hotkey(); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); fund_account(&caller_account, COLDKEY_BALANCE); ensure_hotkey_exists(&hotkey); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("addStakeLimit(bytes32,uint256,uint256,bool,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::from(INITIAL_STAKE_RAO), U256::from(1_000_000_000_000_u64), true, U256::from(TEST_NETUID_U16), ), ), ) .execute_returns(()); assert!(stake_for(&hotkey, &caller_account, netuid) > 0); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("removeStakeFullLimit(bytes32,uint256,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::from(TEST_NETUID_U16), U256::from(90_000_000_u64), ), ), ) .execute_returns(()); assert_eq!(stake_for(&hotkey, &caller_account, netuid), 0); }); } #[test] fn staking_precompile_v2_remove_stake_full_clears_stake() { new_test_ext().execute_with(|| { let netuid = setup_staking_subnet(); let caller = addr_from_index(0x4004); let caller_account = mapped_account(caller); let hotkey = hotkey(); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); fund_account(&caller_account, COLDKEY_BALANCE); ensure_hotkey_exists(&hotkey); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("addStakeLimit(bytes32,uint256,uint256,bool,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::from(INITIAL_STAKE_RAO), U256::from(1_000_000_000_000_u64), true, U256::from(TEST_NETUID_U16), ), ), ) .execute_returns(()); assert!(stake_for(&hotkey, &caller_account, netuid) > 0); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("removeStakeFull(bytes32,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::from(TEST_NETUID_U16), ), ), ) .execute_returns(()); assert_eq!(stake_for(&hotkey, &caller_account, netuid), 0); }); } #[test] fn staking_precompile_v2_getters_match_runtime_state() { new_test_ext().execute_with(|| { let netuid = setup_staking_subnet(); let caller = addr_from_index(0x4005); let caller_account = mapped_account(caller); let hotkey = hotkey(); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); fund_account(&caller_account, COLDKEY_BALANCE); add_stake_v2(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); let stake = stake_for(&hotkey, &caller_account, netuid); assert!(stake > 0); assert_static_call( &precompiles, caller, precompile_addr, encode_with_selector( selector_u32("getStake(bytes32,bytes32,uint256)"), ( H256::from_slice(hotkey.as_ref()), H256::from_slice(caller_account.as_ref()), U256::from(TEST_NETUID_U16), ), ), U256::from(stake), ); assert_static_call( &precompiles, caller, precompile_addr, encode_with_selector( selector_u32("getTotalAlphaStaked(bytes32,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::from(TEST_NETUID_U16), ), ), U256::from( pallet_subtensor::Pallet::::get_stake_for_hotkey_on_subnet( &hotkey, netuid, ) .to_u64(), ), ); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("getAlphaStakedValidators(bytes32,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::from(TEST_NETUID_U16), ), ), ) .with_static_call(true) .execute_returns_raw(encode_return_value(vec![H256::from_slice( caller_account.as_ref(), )])); assert_static_call( &precompiles, caller, precompile_addr, encode_with_selector(selector_u32("getNominatorMinRequiredStake()"), ()), U256::from(pallet_subtensor::Pallet::::get_nominator_min_required_stake()), ); }); } #[test] fn staking_precompile_v1_adds_and_removes_proxy() { new_test_ext().execute_with(|| { let netuid = setup_staking_subnet(); let caller = addr_from_index(0x1007); let caller_account = mapped_account(caller); let delegate = delegate(); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompile::::INDEX); fund_account(&caller_account, COLDKEY_BALANCE); fund_account(&delegate, COLDKEY_BALANCE); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("addProxy(bytes32)"), (H256::from_slice(delegate.as_ref()),), ), ) .execute_returns(()); assert_proxy_effects(caller, netuid); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("removeProxy(bytes32)"), (H256::from_slice(delegate.as_ref()),), ), ) .execute_returns(()); let proxies = pallet_subtensor_proxy::Proxies::::get(&caller_account).0; assert!(proxies.is_empty()); }); } #[test] fn staking_precompile_v2_adds_and_removes_proxy() { new_test_ext().execute_with(|| { let netuid = setup_staking_subnet(); let caller = addr_from_index(0x1008); let caller_account = mapped_account(caller); let delegate = delegate(); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); fund_account(&caller_account, COLDKEY_BALANCE); fund_account(&delegate, COLDKEY_BALANCE); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("addProxy(bytes32)"), (H256::from_slice(delegate.as_ref()),), ), ) .execute_returns(()); assert_proxy_effects(caller, netuid); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("removeProxy(bytes32)"), (H256::from_slice(delegate.as_ref()),), ), ) .execute_returns(()); let proxies = pallet_subtensor_proxy::Proxies::::get(&caller_account).0; assert!(proxies.is_empty()); }); } #[test] fn staking_precompile_v2_transfer_stake_from_requires_allowance() { new_test_ext().execute_with(|| { let (_, source, spender, _, _, hotkey) = setup_approval_state(); precompiles::>() .prepare_test( spender, addr_from_index(StakingPrecompileV2::::INDEX), encode_with_selector( selector_u32( "transferStakeFrom(address,address,bytes32,uint256,uint256,uint256)", ), ( precompile_utils::solidity::codec::Address(source), precompile_utils::solidity::codec::Address(spender), H256::from_slice(hotkey.as_ref()), U256::from(TEST_NETUID_U16), U256::from(TEST_NETUID_U16), U256::from(1_u64), ), ), ) .execute_reverts(|output| output == b"trying to spend more than allowed"); }); } #[test] fn staking_precompile_v2_transfer_stake_from_consumes_allowance_and_moves_stake() { new_test_ext().execute_with(|| { let (netuid, source, spender, source_account, spender_account, hotkey) = setup_approval_state(); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); precompiles .prepare_test( source, precompile_addr, encode_with_selector( selector_u32("approve(address,uint256,uint256)"), ( precompile_utils::solidity::codec::Address(spender), U256::from(TEST_NETUID_U16), U256::from(APPROVED_ALLOWANCE_RAO), ), ), ) .execute_returns(()); let source_stake_before = stake_for(&hotkey, &source_account, netuid); let spender_stake_before = stake_for(&hotkey, &spender_account, netuid); precompiles .prepare_test( spender, precompile_addr, encode_with_selector( selector_u32( "transferStakeFrom(address,address,bytes32,uint256,uint256,uint256)", ), ( precompile_utils::solidity::codec::Address(source), precompile_utils::solidity::codec::Address(spender), H256::from_slice(hotkey.as_ref()), U256::from(TEST_NETUID_U16), U256::from(TEST_NETUID_U16), U256::from(TRANSFERRED_ALLOWANCE_RAO), ), ), ) .execute_returns(()); assert_allowance( source, spender, source, U256::from(APPROVED_ALLOWANCE_RAO - TRANSFERRED_ALLOWANCE_RAO), ); assert_eq!( stake_for(&hotkey, &source_account, netuid), source_stake_before - TRANSFERRED_ALLOWANCE_RAO, ); assert_eq!( stake_for(&hotkey, &spender_account, netuid), spender_stake_before + TRANSFERRED_ALLOWANCE_RAO, ); }); } #[test] fn staking_precompile_v2_transfer_stake_from_rejects_amount_above_allowance() { new_test_ext().execute_with(|| { let (_, source, spender, _, _, hotkey) = setup_approval_state(); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); precompiles .prepare_test( source, precompile_addr, encode_with_selector( selector_u32("approve(address,uint256,uint256)"), ( precompile_utils::solidity::codec::Address(spender), U256::from(TEST_NETUID_U16), U256::from(TRANSFERRED_ALLOWANCE_RAO), ), ), ) .execute_returns(()); precompiles .prepare_test( spender, precompile_addr, encode_with_selector( selector_u32( "transferStakeFrom(address,address,bytes32,uint256,uint256,uint256)", ), ( precompile_utils::solidity::codec::Address(source), precompile_utils::solidity::codec::Address(spender), H256::from_slice(hotkey.as_ref()), U256::from(TEST_NETUID_U16), U256::from(TEST_NETUID_U16), U256::from(TRANSFERRED_ALLOWANCE_RAO + 1), ), ), ) .execute_reverts(|output| output == b"trying to spend more than allowed"); }); } #[test] fn staking_precompile_v2_approval_functions_update_allowance() { new_test_ext().execute_with(|| { let (_, source, spender, _, _, _) = setup_approval_state(); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); assert_allowance(source, spender, source, U256::zero()); precompiles .prepare_test( source, precompile_addr, encode_with_selector( selector_u32("approve(address,uint256,uint256)"), ( precompile_utils::solidity::codec::Address(spender), U256::from(TEST_NETUID_U16), U256::from(APPROVED_ALLOWANCE_RAO), ), ), ) .execute_returns(()); assert_allowance(source, spender, source, U256::from(APPROVED_ALLOWANCE_RAO)); precompiles .prepare_test( source, precompile_addr, encode_with_selector( selector_u32("increaseAllowance(address,uint256,uint256)"), ( precompile_utils::solidity::codec::Address(spender), U256::from(TEST_NETUID_U16), U256::from(APPROVED_ALLOWANCE_RAO), ), ), ) .execute_returns(()); assert_allowance( source, spender, source, U256::from(APPROVED_ALLOWANCE_RAO * 2), ); precompiles .prepare_test( source, precompile_addr, encode_with_selector( selector_u32("decreaseAllowance(address,uint256,uint256)"), ( precompile_utils::solidity::codec::Address(spender), U256::from(TEST_NETUID_U16), U256::from(ALLOWANCE_DECREASE_RAO), ), ), ) .execute_returns(()); assert_allowance( source, spender, source, U256::from(APPROVED_ALLOWANCE_RAO * 2 - ALLOWANCE_DECREASE_RAO), ); precompiles .prepare_test( source, precompile_addr, encode_with_selector( selector_u32("approve(address,uint256,uint256)"), ( precompile_utils::solidity::codec::Address(spender), U256::from(TEST_NETUID_U16), U256::zero(), ), ), ) .execute_returns(()); assert_allowance(source, spender, source, U256::zero()); }); } #[test] fn staking_precompile_v2_burn_alpha_reduces_stake() { new_test_ext().execute_with(|| { let netuid = setup_staking_subnet(); let caller = addr_from_index(0x3001); let caller_account = mapped_account(caller); let hotkey = hotkey(); let burn_amount = 20_000_000_000_u64; let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); fund_account(&caller_account, COLDKEY_BALANCE); add_stake_v2(caller, &hotkey, TEST_NETUID_U16, 50_000_000_000); let stake_before = stake_for(&hotkey, &caller_account, netuid); assert!(stake_before > 0); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("burnAlpha(bytes32,uint256,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::from(burn_amount), U256::from(TEST_NETUID_U16), ), ), ) .execute_returns(()); let stake_after = stake_for(&hotkey, &caller_account, netuid); assert_eq!(stake_after, stake_before - burn_amount); }); } // cargo test --package subtensor-precompiles --lib -- staking::tests::staking_precompile_v2_burn_alpha_caps_to_available_stake --exact --nocapture #[test] fn staking_precompile_v2_burn_alpha_caps_to_available_stake() { new_test_ext().execute_with(|| { let netuid = setup_staking_subnet(); let caller = addr_from_index(0x3002); let caller_account = mapped_account(caller); let hotkey = hotkey(); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); fund_account(&caller_account, COLDKEY_BALANCE); add_stake_v2(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); let stake_before = stake_for(&hotkey, &caller_account, netuid); assert!(stake_before > 0); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("burnAlpha(bytes32,uint256,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::from(stake_before + 10_000_000_000_u64), U256::from(TEST_NETUID_U16), ), ), ) .execute_returns(()); let stake_after = stake_for(&hotkey, &caller_account, netuid); assert_eq!(stake_after, 0); }); } #[test] fn staking_precompile_v2_burn_alpha_rejects_missing_subnet() { new_test_ext().execute_with(|| { let caller = addr_from_index(0x3003); let caller_account = mapped_account(caller); let hotkey = hotkey(); fund_account(&caller_account, COLDKEY_BALANCE); ensure_hotkey_exists(&hotkey); let rejected = execute_precompile( &precompiles::>(), addr_from_index(StakingPrecompileV2::::INDEX), caller, encode_with_selector( selector_u32("burnAlpha(bytes32,uint256,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::from(10_000_000_000_u64), U256::from(INVALID_NETUID_U16), ), ), U256::zero(), ) .expect("burnAlpha should route to the staking v2 precompile"); assert!(rejected.is_err()); }); } #[test] fn staking_precompile_v2_burn_zero_alpha_is_noop() { new_test_ext().execute_with(|| { let netuid = setup_staking_subnet(); let caller = addr_from_index(0x3004); let caller_account = mapped_account(caller); let hotkey = hotkey(); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); fund_account(&caller_account, COLDKEY_BALANCE); add_stake_v2(caller, &hotkey, TEST_NETUID_U16, 10_000_000_000); let stake_before = stake_for(&hotkey, &caller_account, netuid); precompiles .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("burnAlpha(bytes32,uint256,uint256)"), ( H256::from_slice(hotkey.as_ref()), U256::zero(), U256::from(TEST_NETUID_U16), ), ), ) .execute_returns(()); let stake_after = stake_for(&hotkey, &caller_account, netuid); assert_eq!(stake_after, stake_before); }); } }