diff --git a/Cargo.lock b/Cargo.lock index 78c1f9d10..f8e793a4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8278,6 +8278,7 @@ dependencies = [ "pallet-balances", "pallet-base-fee", "pallet-commitments", + "pallet-contracts 40.1.0", "pallet-crowdloan", "pallet-drand", "pallet-election-provider-multi-phase", @@ -8344,6 +8345,7 @@ dependencies = [ "sp-version", "substrate-fixed", "substrate-wasm-builder", + "subtensor-chain-extensions", "subtensor-custom-rpc-runtime-api", "subtensor-macros", "subtensor-precompiles", @@ -9285,6 +9287,37 @@ dependencies = [ "w3f-bls 0.1.3", ] +[[package]] +name = "pallet-contracts" +version = "40.1.0" +source = "git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-stable2503-6#598feddb893f5ad3923a62e41a2f179b6e10c30c" +dependencies = [ + "environmental", + "frame-benchmarking", + "frame-support", + "frame-system", + "impl-trait-for-tuples", + "log", + "pallet-balances", + "pallet-contracts-proc-macro 23.0.3 (git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-stable2503-6)", + "pallet-contracts-uapi 14.0.0 (git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-stable2503-6)", + "parity-scale-codec", + "paste", + "rand 0.8.5", + "rand_pcg", + "scale-info", + "serde", + "smallvec", + "sp-api", + "sp-core", + "sp-io", + "sp-runtime", + "staging-xcm", + "staging-xcm-builder", + "wasm-instrument", + "wasmi 0.32.3", +] + [[package]] name = "pallet-contracts" version = "41.0.0" @@ -9297,8 +9330,8 @@ dependencies = [ "impl-trait-for-tuples", "log", "pallet-balances", - "pallet-contracts-proc-macro", - "pallet-contracts-uapi", + "pallet-contracts-proc-macro 23.0.3 (git+https://github.com/opentensor/polkadot-sdk.git?rev=81fa2c54e94f824eba7dabe9dffd063481cb2d80)", + "pallet-contracts-uapi 14.0.0 (git+https://github.com/opentensor/polkadot-sdk.git?rev=81fa2c54e94f824eba7dabe9dffd063481cb2d80)", "parity-scale-codec", "paste", "rand 0.8.5", @@ -9325,8 +9358,8 @@ dependencies = [ "frame-system", "pallet-assets", "pallet-balances", - "pallet-contracts", - "pallet-contracts-uapi", + "pallet-contracts 41.0.0", + "pallet-contracts-uapi 14.0.0 (git+https://github.com/opentensor/polkadot-sdk.git?rev=81fa2c54e94f824eba7dabe9dffd063481cb2d80)", "pallet-message-queue", "pallet-timestamp", "pallet-xcm", @@ -9347,6 +9380,16 @@ dependencies = [ "xcm-simulator", ] +[[package]] +name = "pallet-contracts-proc-macro" +version = "23.0.3" +source = "git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-stable2503-6#598feddb893f5ad3923a62e41a2f179b6e10c30c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "pallet-contracts-proc-macro" version = "23.0.3" @@ -9357,6 +9400,17 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "pallet-contracts-uapi" +version = "14.0.0" +source = "git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-stable2503-6#598feddb893f5ad3923a62e41a2f179b6e10c30c" +dependencies = [ + "bitflags 1.3.2", + "parity-scale-codec", + "paste", + "scale-info", +] + [[package]] name = "pallet-contracts-uapi" version = "14.0.0" @@ -12454,7 +12508,7 @@ dependencies = [ "pallet-collator-selection", "pallet-collective", "pallet-collective-content", - "pallet-contracts", + "pallet-contracts 41.0.0", "pallet-contracts-mock-network", "pallet-conviction-voting", "pallet-core-fellowship", @@ -17901,6 +17955,35 @@ dependencies = [ "walkdir", ] +[[package]] +name = "subtensor-chain-extensions" +version = "0.1.0" +dependencies = [ + "frame-support", + "frame-system", + "log", + "num_enum", + "pallet-balances", + "pallet-contracts 40.1.0", + "pallet-crowdloan", + "pallet-drand", + "pallet-preimage", + "pallet-scheduler", + "pallet-subtensor", + "pallet-subtensor-proxy", + "pallet-subtensor-swap", + "pallet-subtensor-utility", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "subtensor-runtime-common", + "subtensor-swap-interface", +] + [[package]] name = "subtensor-custom-rpc" version = "0.0.2" diff --git a/Cargo.toml b/Cargo.toml index c7114b43a..613900491 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ members = [ "primitives/*", "runtime", "support/*", + "chain-extensions", ] resolver = "2" @@ -68,6 +69,7 @@ subtensor-precompiles = { default-features = false, path = "precompiles" } subtensor-runtime-common = { default-features = false, path = "common" } subtensor-swap-interface = { default-features = false, path = "pallets/swap-interface" } subtensor-transaction-fee = { default-features = false, path = "pallets/transaction-fee" } +subtensor-chain-extensions = { default-features = false, path = "chain-extensions" } ed25519-dalek = { version = "2.1.0", default-features = false } async-trait = "0.1" @@ -115,6 +117,7 @@ expander = "2" ahash = { version = "0.8", default-features = false } regex = { version = "1.11.1", default-features = false } ethereum = { version = "0.18.2", default-features = false } +num_enum = { version = "0.7.4", default-features = false } frame = { package = "polkadot-sdk-frame", git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-stable2503-6", default-features = false } frame-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-stable2503-6", default-features = false } @@ -145,6 +148,7 @@ pallet-transaction-payment = { git = "https://github.com/paritytech/polkadot-sdk pallet-transaction-payment-rpc = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-stable2503-6", default-features = false } pallet-transaction-payment-rpc-runtime-api = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-stable2503-6", default-features = false } pallet-root-testing = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-stable2503-6", default-features = false } +pallet-contracts = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-stable2503-6", default-features = false } # NPoS frame-election-provider-support = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-stable2503-6", default-features = false } diff --git a/README.md b/README.md index 5c3faaf03..2c74b9e8f 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ This repository contains Bittensor's substrate-chain. Subtensor contains the tru 1. Runs Bittensor's [consensus mechanism](./docs/consensus.md); 2. Advertises neuron information, IPs, etc., and 3. Facilitates value transfer via TAO. +4. Supports wasm smart contract functionality via `pallet-contracts` (see [contracts documentation](./docs/contracts.md)). ## System Requirements diff --git a/chain-extensions/Cargo.toml b/chain-extensions/Cargo.toml new file mode 100644 index 000000000..903c82367 --- /dev/null +++ b/chain-extensions/Cargo.toml @@ -0,0 +1,66 @@ +[package] +name = "subtensor-chain-extensions" +version = "0.1.0" +edition.workspace = true +authors = ['Francisco Silva '] +homepage = "https://taostats.io/" +publish = false +repository = "https://github.com/opentensor/subtensor/" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +frame-support.workspace = true +frame-system.workspace = true +log.workspace = true +sp-core.workspace = true +sp-io.workspace = true +sp-runtime.workspace = true +sp-std.workspace = true +codec = { workspace = true, features = ["derive"] } +scale-info = { workspace = true, features = ["derive"] } +subtensor-runtime-common.workspace = true +pallet-contracts.workspace = true +pallet-subtensor.workspace = true +pallet-subtensor-swap.workspace = true +pallet-balances.workspace = true +pallet-scheduler.workspace = true +pallet-preimage.workspace = true +pallet-timestamp.workspace = true +pallet-crowdloan.workspace = true +pallet-subtensor-utility.workspace = true +pallet-subtensor-proxy.workspace = true +pallet-drand.workspace = true +subtensor-swap-interface.workspace = true +num_enum.workspace = true + +[lints] +workspace = true + +[features] +default = ["std"] +std = [ + "frame-support/std", + "frame-system/std", + "log/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", + "codec/std", + "scale-info/std", + "subtensor-runtime-common/std", + "pallet-contracts/std", + "pallet-subtensor/std", + "pallet-subtensor-swap/std", + "pallet-balances/std", + "pallet-scheduler/std", + "pallet-preimage/std", + "pallet-timestamp/std", + "pallet-crowdloan/std", + "pallet-subtensor-utility/std", + "pallet-subtensor-proxy/std", + "pallet-drand/std", + "subtensor-swap-interface/std", +] diff --git a/chain-extensions/src/lib.rs b/chain-extensions/src/lib.rs new file mode 100644 index 000000000..e53fac765 --- /dev/null +++ b/chain-extensions/src/lib.rs @@ -0,0 +1,569 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +pub mod types; + +use crate::types::{FunctionId, Output}; +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{DebugNoBound, traits::Get}; +use frame_system::RawOrigin; +use pallet_contracts::chain_extension::{ + BufInBufOutState, ChainExtension, Environment, Ext, InitState, RetVal, SysConfig, +}; +use pallet_subtensor_proxy as pallet_proxy; +use pallet_subtensor_proxy::WeightInfo; +use sp_runtime::{DispatchError, Weight, traits::StaticLookup}; +use sp_std::marker::PhantomData; +use subtensor_runtime_common::{AlphaCurrency, NetUid, ProxyType, TaoCurrency}; + +#[derive(DebugNoBound)] +pub struct SubtensorChainExtension(PhantomData); + +impl Default for SubtensorChainExtension { + fn default() -> Self { + Self(PhantomData) + } +} + +impl ChainExtension for SubtensorChainExtension +where + T: pallet_subtensor::Config + + pallet_contracts::Config + + pallet_proxy::Config, + T::AccountId: Clone, + <::Lookup as StaticLookup>::Source: From<::AccountId>, +{ + fn call(&mut self, env: Environment) -> Result + where + E: Ext, + { + let mut adapter = ContractsEnvAdapter::::new(env); + Self::dispatch(&mut adapter) + } + + fn enabled() -> bool { + true + } +} + +impl SubtensorChainExtension +where + T: pallet_subtensor::Config + + pallet_contracts::Config + + pallet_proxy::Config, + T::AccountId: Clone, +{ + fn dispatch(env: &mut Env) -> Result + where + Env: SubtensorExtensionEnv, + <::Lookup as StaticLookup>::Source: From<::AccountId>, + { + let func_id: FunctionId = env.func_id().try_into().map_err(|_| { + DispatchError::Other( + "Invalid function id - does not correspond to any registered function", + ) + })?; + + match func_id { + FunctionId::GetStakeInfoForHotkeyColdkeyNetuidV1 => { + let (hotkey, coldkey, netuid): (T::AccountId, T::AccountId, NetUid) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + + let stake_info = + pallet_subtensor::Pallet::::get_stake_info_for_hotkey_coldkey_netuid( + hotkey, coldkey, netuid, + ); + + let encoded_result = stake_info.encode(); + + env.write_output(&encoded_result) + .map_err(|_| DispatchError::Other("Failed to write output"))?; + + Ok(RetVal::Converging(Output::Success as u32)) + } + FunctionId::AddStakeV1 => { + let weight = Weight::from_parts(340_800_000, 0) + .saturating_add(T::DbWeight::get().reads(24_u64)) + .saturating_add(T::DbWeight::get().writes(15)); + + env.charge_weight(weight)?; + + let (hotkey, netuid, amount_staked): (T::AccountId, NetUid, TaoCurrency) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + + let call_result = pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(env.caller()).into(), + hotkey, + netuid, + amount_staked, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), + Err(e) => { + let error_code = Output::from(e) as u32; + Ok(RetVal::Converging(error_code)) + } + } + } + FunctionId::RemoveStakeV1 => { + let weight = Weight::from_parts(196_800_000, 0) + .saturating_add(T::DbWeight::get().reads(19)) + .saturating_add(T::DbWeight::get().writes(10)); + + env.charge_weight(weight)?; + + let (hotkey, netuid, amount_unstaked): (T::AccountId, NetUid, AlphaCurrency) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + + let call_result = pallet_subtensor::Pallet::::remove_stake( + RawOrigin::Signed(env.caller()).into(), + hotkey, + netuid, + amount_unstaked, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), + Err(e) => { + let error_code = Output::from(e) as u32; + Ok(RetVal::Converging(error_code)) + } + } + } + FunctionId::UnstakeAllV1 => { + let weight = Weight::from_parts(28_830_000, 0) + .saturating_add(T::DbWeight::get().reads(6)) + .saturating_add(T::DbWeight::get().writes(0)); + + env.charge_weight(weight)?; + + let hotkey: T::AccountId = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + + let call_result = pallet_subtensor::Pallet::::unstake_all( + RawOrigin::Signed(env.caller()).into(), + hotkey, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), + Err(e) => { + let error_code = Output::from(e) as u32; + Ok(RetVal::Converging(error_code)) + } + } + } + FunctionId::UnstakeAllAlphaV1 => { + let weight = Weight::from_parts(358_500_000, 0) + .saturating_add(T::DbWeight::get().reads(36_u64)) + .saturating_add(T::DbWeight::get().writes(21_u64)); + + env.charge_weight(weight)?; + + let hotkey: T::AccountId = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + + let call_result = pallet_subtensor::Pallet::::unstake_all_alpha( + RawOrigin::Signed(env.caller()).into(), + hotkey, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), + Err(e) => { + let error_code = Output::from(e) as u32; + Ok(RetVal::Converging(error_code)) + } + } + } + FunctionId::MoveStakeV1 => { + let weight = Weight::from_parts(164_300_000, 0) + .saturating_add(T::DbWeight::get().reads(15_u64)) + .saturating_add(T::DbWeight::get().writes(7_u64)); + + env.charge_weight(weight)?; + + let ( + origin_hotkey, + destination_hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + ): (T::AccountId, T::AccountId, NetUid, NetUid, AlphaCurrency) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + + let call_result = pallet_subtensor::Pallet::::move_stake( + RawOrigin::Signed(env.caller()).into(), + origin_hotkey, + destination_hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), + Err(e) => { + let error_code = Output::from(e) as u32; + Ok(RetVal::Converging(error_code)) + } + } + } + FunctionId::TransferStakeV1 => { + let weight = Weight::from_parts(160_300_000, 0) + .saturating_add(T::DbWeight::get().reads(13_u64)) + .saturating_add(T::DbWeight::get().writes(6_u64)); + + env.charge_weight(weight)?; + + let (destination_coldkey, hotkey, origin_netuid, destination_netuid, alpha_amount): ( + T::AccountId, + T::AccountId, + NetUid, + NetUid, + AlphaCurrency, + ) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + + let call_result = pallet_subtensor::Pallet::::transfer_stake( + RawOrigin::Signed(env.caller()).into(), + destination_coldkey, + hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), + Err(e) => { + let error_code = Output::from(e) as u32; + Ok(RetVal::Converging(error_code)) + } + } + } + FunctionId::SwapStakeV1 => { + let weight = Weight::from_parts(351_300_000, 0) + .saturating_add(T::DbWeight::get().reads(35_u64)) + .saturating_add(T::DbWeight::get().writes(22_u64)); + + env.charge_weight(weight)?; + + let (hotkey, origin_netuid, destination_netuid, alpha_amount): ( + T::AccountId, + NetUid, + NetUid, + AlphaCurrency, + ) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + + let call_result = pallet_subtensor::Pallet::::swap_stake( + RawOrigin::Signed(env.caller()).into(), + hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), + Err(e) => { + let error_code = Output::from(e) as u32; + Ok(RetVal::Converging(error_code)) + } + } + } + FunctionId::AddStakeLimitV1 => { + let weight = Weight::from_parts(402_900_000, 0) + .saturating_add(T::DbWeight::get().reads(24_u64)) + .saturating_add(T::DbWeight::get().writes(15)); + + env.charge_weight(weight)?; + + let (hotkey, netuid, amount_staked, limit_price, allow_partial): ( + T::AccountId, + NetUid, + TaoCurrency, + TaoCurrency, + bool, + ) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + + let call_result = pallet_subtensor::Pallet::::add_stake_limit( + RawOrigin::Signed(env.caller()).into(), + hotkey, + netuid, + amount_staked, + limit_price, + allow_partial, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), + Err(e) => { + let error_code = Output::from(e) as u32; + Ok(RetVal::Converging(error_code)) + } + } + } + FunctionId::RemoveStakeLimitV1 => { + let weight = Weight::from_parts(377_400_000, 0) + .saturating_add(T::DbWeight::get().reads(28_u64)) + .saturating_add(T::DbWeight::get().writes(14)); + + env.charge_weight(weight)?; + + let (hotkey, netuid, amount_unstaked, limit_price, allow_partial): ( + T::AccountId, + NetUid, + AlphaCurrency, + TaoCurrency, + bool, + ) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + + let call_result = pallet_subtensor::Pallet::::remove_stake_limit( + RawOrigin::Signed(env.caller()).into(), + hotkey, + netuid, + amount_unstaked, + limit_price, + allow_partial, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), + Err(e) => { + let error_code = Output::from(e) as u32; + Ok(RetVal::Converging(error_code)) + } + } + } + FunctionId::SwapStakeLimitV1 => { + let weight = Weight::from_parts(411_500_000, 0) + .saturating_add(T::DbWeight::get().reads(35_u64)) + .saturating_add(T::DbWeight::get().writes(22_u64)); + + env.charge_weight(weight)?; + + let ( + hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + limit_price, + allow_partial, + ): ( + T::AccountId, + NetUid, + NetUid, + AlphaCurrency, + TaoCurrency, + bool, + ) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + + let call_result = pallet_subtensor::Pallet::::swap_stake_limit( + RawOrigin::Signed(env.caller()).into(), + hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + limit_price, + allow_partial, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), + Err(e) => { + let error_code = Output::from(e) as u32; + Ok(RetVal::Converging(error_code)) + } + } + } + FunctionId::RemoveStakeFullLimitV1 => { + let weight = Weight::from_parts(395_300_000, 0) + .saturating_add(T::DbWeight::get().reads(28_u64)) + .saturating_add(T::DbWeight::get().writes(14_u64)); + + env.charge_weight(weight)?; + + let (hotkey, netuid, limit_price): (T::AccountId, NetUid, Option) = + env.read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + + let call_result = pallet_subtensor::Pallet::::remove_stake_full_limit( + RawOrigin::Signed(env.caller()).into(), + hotkey, + netuid, + limit_price, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), + Err(e) => { + let error_code = Output::from(e) as u32; + Ok(RetVal::Converging(error_code)) + } + } + } + FunctionId::SetColdkeyAutoStakeHotkeyV1 => { + let weight = Weight::from_parts(29_930_000, 0) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)); + + env.charge_weight(weight)?; + + let (netuid, hotkey): (NetUid, T::AccountId) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + + let call_result = pallet_subtensor::Pallet::::set_coldkey_auto_stake_hotkey( + RawOrigin::Signed(env.caller()).into(), + netuid, + hotkey, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), + Err(e) => { + let error_code = Output::from(e) as u32; + Ok(RetVal::Converging(error_code)) + } + } + } + FunctionId::AddProxyV1 => { + let weight = ::WeightInfo::add_proxy( + ::MaxProxies::get(), + ); + + env.charge_weight(weight)?; + + let delegate: T::AccountId = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + + let delegate_lookup = + <::Lookup as StaticLookup>::Source::from(delegate); + + let call_result = pallet_proxy::Pallet::::add_proxy( + RawOrigin::Signed(env.caller()).into(), + delegate_lookup, + ProxyType::Staking, + 0u32.into(), + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), + Err(e) => { + let error_code = Output::from(e) as u32; + Ok(RetVal::Converging(error_code)) + } + } + } + FunctionId::RemoveProxyV1 => { + let weight = ::WeightInfo::remove_proxy( + ::MaxProxies::get(), + ); + + env.charge_weight(weight)?; + + let delegate: T::AccountId = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + + let delegate_lookup = + <::Lookup as StaticLookup>::Source::from(delegate); + + let call_result = pallet_proxy::Pallet::::remove_proxy( + RawOrigin::Signed(env.caller()).into(), + delegate_lookup, + ProxyType::Staking, + 0u32.into(), + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), + Err(e) => { + let error_code = Output::from(e) as u32; + Ok(RetVal::Converging(error_code)) + } + } + } + } + } +} + +trait SubtensorExtensionEnv { + fn func_id(&self) -> u16; + fn charge_weight(&mut self, weight: Weight) -> Result<(), DispatchError>; + fn read_as(&mut self) -> Result; + fn write_output(&mut self, data: &[u8]) -> Result<(), DispatchError>; + fn caller(&mut self) -> AccountId; +} + +struct ContractsEnvAdapter<'a, 'b, T, E> +where + T: pallet_subtensor::Config + pallet_contracts::Config, + E: Ext, +{ + env: Environment<'a, 'b, E, BufInBufOutState>, + _marker: PhantomData, +} + +impl<'a, 'b, T, E> ContractsEnvAdapter<'a, 'b, T, E> +where + T: pallet_subtensor::Config + pallet_contracts::Config, + T::AccountId: Clone, + E: Ext, +{ + fn new(env: Environment<'a, 'b, E, InitState>) -> Self { + Self { + env: env.buf_in_buf_out(), + _marker: PhantomData, + } + } +} + +impl<'a, 'b, T, E> SubtensorExtensionEnv for ContractsEnvAdapter<'a, 'b, T, E> +where + T: pallet_subtensor::Config + pallet_contracts::Config, + T::AccountId: Clone, + E: Ext, +{ + fn func_id(&self) -> u16 { + self.env.func_id() + } + + fn charge_weight(&mut self, weight: Weight) -> Result<(), DispatchError> { + self.env.charge_weight(weight).map(|_| ()) + } + + fn read_as(&mut self) -> Result { + self.env.read_as() + } + + fn write_output(&mut self, data: &[u8]) -> Result<(), DispatchError> { + self.env.write(data, false, None) + } + + fn caller(&mut self) -> T::AccountId { + self.env.ext().address().clone() + } +} diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs new file mode 100644 index 000000000..38521df62 --- /dev/null +++ b/chain-extensions/src/mock.rs @@ -0,0 +1,701 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::unwrap_used +)] + +use core::num::NonZeroU64; + +use frame_support::dispatch::DispatchResult; +use frame_support::traits::{Contains, Everything, InherentBuilder, InsideBoth}; +use frame_support::weights::Weight; +use frame_support::weights::constants::RocksDbWeight; +use frame_support::{PalletId, derive_impl}; +use frame_support::{assert_ok, parameter_types, traits::PrivilegeCmp}; +use frame_system as system; +use frame_system::{EnsureRoot, RawOrigin, limits, offchain::CreateTransactionBase}; +use pallet_contracts::HoldReason as ContractsHoldReason; +use pallet_subtensor::*; +use pallet_subtensor_proxy as pallet_proxy; +use pallet_subtensor_utility as pallet_utility; +use sp_core::{ConstU64, H256, U256, offchain::KeyTypeId}; +use sp_runtime::Perbill; +use sp_runtime::{ + BuildStorage, Percent, + traits::{BlakeTwo256, Convert, IdentityLookup}, +}; +use sp_std::{cell::RefCell, cmp::Ordering, sync::OnceLock}; +use subtensor_runtime_common::{AlphaCurrency, NetUid, TaoCurrency}; + +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system::{Pallet, Call, Config, Storage, Event} = 1, + Balances: pallet_balances::{Pallet, Call, Config, Storage, Event} = 2, + SubtensorModule: pallet_subtensor::{Pallet, Call, Storage, Event} = 7, + Utility: pallet_utility::{Pallet, Call, Storage, Event} = 8, + Scheduler: pallet_scheduler::{Pallet, Call, Storage, Event} = 9, + Preimage: pallet_preimage::{Pallet, Call, Storage, Event} = 10, + Drand: pallet_drand::{Pallet, Call, Storage, Event} = 11, + Swap: pallet_subtensor_swap::{Pallet, Call, Storage, Event} = 12, + Crowdloan: pallet_crowdloan::{Pallet, Call, Storage, Event} = 13, + Timestamp: pallet_timestamp::{Pallet, Call, Storage} = 14, + Contracts: pallet_contracts::{Pallet, Call, Storage, Event} = 15, + Proxy: pallet_proxy::{Pallet, Call, Storage, Event} = 16, + } +); + +pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"test"); + +#[allow(dead_code)] +pub type TestRuntimeCall = frame_system::Call; + +#[allow(dead_code)] +pub type AccountId = U256; + +// Balance of an account. +#[allow(dead_code)] +pub type Balance = u64; + +// An index to a block. +#[allow(dead_code)] +pub type BlockNumber = u64; + +pub struct DummyContractsRandomness; + +impl frame_support::traits::Randomness for DummyContractsRandomness { + fn random(_subject: &[u8]) -> (H256, BlockNumber) { + (H256::zero(), 0) + } +} + +pub struct WeightToBalance; + +impl Convert for WeightToBalance { + fn convert(weight: Weight) -> Balance { + weight.ref_time() + } +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Test { + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type MaxLocks = (); + type WeightInfo = (); + type MaxReserves = (); + type ReserveIdentifier = (); + type RuntimeHoldReason = ContractsHoldReason; + type FreezeIdentifier = (); + type MaxFreezes = (); +} + +#[derive_impl(pallet_timestamp::config_preludes::TestDefaultConfig)] +impl pallet_timestamp::Config for Test { + type MinimumPeriod = ConstU64<1>; +} + +#[derive_impl(pallet_contracts::config_preludes::TestDefaultConfig)] +impl pallet_contracts::Config for Test { + type Time = Timestamp; + type Randomness = DummyContractsRandomness; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type RuntimeHoldReason = ContractsHoldReason; + type RuntimeCall = RuntimeCall; + type CallFilter = Everything; + type WeightPrice = WeightToBalance; + type WeightInfo = (); + type ChainExtension = crate::SubtensorChainExtension; + type Schedule = ContractsSchedule; + type CallStack = [pallet_contracts::Frame; 5]; + type DepositPerByte = ContractsDepositPerByte; + type DepositPerItem = ContractsDepositPerItem; + type DefaultDepositLimit = ContractsDefaultDepositLimit; + type AddressGenerator = pallet_contracts::DefaultAddressGenerator; + type UnsafeUnstableInterface = ContractsUnstableInterface; + type UploadOrigin = frame_system::EnsureSigned; + type InstantiateOrigin = frame_system::EnsureSigned; + type CodeHashLockupDepositPercent = ContractsCodeHashLockupDepositPercent; + type MaxDelegateDependencies = ContractsMaxDelegateDependencies; + type MaxCodeLen = ContractsMaxCodeLen; + type MaxStorageKeyLen = ContractsMaxStorageKeyLen; + type MaxTransientStorageSize = ContractsMaxTransientStorageSize; + type MaxDebugBufferLen = ContractsMaxDebugBufferLen; + type Migrations = (); + type Debug = (); + type Environment = (); + type ApiVersion = (); + type Xcm = (); +} + +impl frame_support::traits::InstanceFilter for subtensor_runtime_common::ProxyType { + fn filter(&self, c: &RuntimeCall) -> bool { + match self { + subtensor_runtime_common::ProxyType::Any => true, + subtensor_runtime_common::ProxyType::Staking => matches!( + c, + RuntimeCall::SubtensorModule(pallet_subtensor::Call::add_stake { .. }) + | RuntimeCall::SubtensorModule(pallet_subtensor::Call::add_stake_limit { .. }) + | RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { .. }) + | RuntimeCall::SubtensorModule( + pallet_subtensor::Call::remove_stake_limit { .. } + ) + | RuntimeCall::SubtensorModule( + pallet_subtensor::Call::remove_stake_full_limit { .. } + ) + | RuntimeCall::SubtensorModule(pallet_subtensor::Call::unstake_all { .. }) + | RuntimeCall::SubtensorModule( + pallet_subtensor::Call::unstake_all_alpha { .. } + ) + | RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_stake { .. }) + | RuntimeCall::SubtensorModule(pallet_subtensor::Call::swap_stake_limit { .. }) + | RuntimeCall::SubtensorModule(pallet_subtensor::Call::move_stake { .. }) + | RuntimeCall::SubtensorModule(pallet_subtensor::Call::transfer_stake { .. }) + ), + _ => false, + } + } + + fn is_superset(&self, o: &Self) -> bool { + match (self, o) { + (subtensor_runtime_common::ProxyType::Any, _) => true, + _ => self == o, + } + } +} + +impl pallet_proxy::Config for Test { + type RuntimeCall = RuntimeCall; + type Currency = Balances; + type ProxyType = subtensor_runtime_common::ProxyType; + type ProxyDepositBase = ProxyDepositBase; + type ProxyDepositFactor = ProxyDepositFactor; + type MaxProxies = MaxProxies; + type WeightInfo = (); + type MaxPending = MaxPending; + type CallHasher = BlakeTwo256; + type AnnouncementDepositBase = AnnouncementDepositBase; + type AnnouncementDepositFactor = AnnouncementDepositFactor; + type BlockNumberProvider = System; +} + +pub struct NoNestingCallFilter; + +impl Contains for NoNestingCallFilter { + fn contains(call: &RuntimeCall) -> bool { + match call { + RuntimeCall::Utility(inner) => { + let calls = match inner { + pallet_utility::Call::force_batch { calls } => calls, + pallet_utility::Call::batch { calls } => calls, + pallet_utility::Call::batch_all { calls } => calls, + _ => &Vec::new(), + }; + + !calls.iter().any(|call| { + matches!(call, RuntimeCall::Utility(inner) if matches!(inner, pallet_utility::Call::force_batch { .. } | pallet_utility::Call::batch_all { .. } | pallet_utility::Call::batch { .. })) + }) + } + _ => true, + } + } +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl system::Config for Test { + type BaseCallFilter = InsideBoth; + type BlockWeights = BlockWeights; + type BlockLength = (); + type DbWeight = RocksDbWeight; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = U256; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; + type Nonce = u64; + type Block = Block; +} + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} + +parameter_types! { + pub ContractsSchedule: pallet_contracts::Schedule = Default::default(); + pub const ContractsDepositPerByte: Balance = 1; + pub const ContractsDepositPerItem: Balance = 10; + pub const ContractsDefaultDepositLimit: Balance = 1_000_000_000; + pub const ContractsCodeHashLockupDepositPercent: Perbill = Perbill::from_percent(0); + pub const ContractsMaxDelegateDependencies: u32 = 32; + pub const ContractsMaxCodeLen: u32 = 120_000; + pub const ContractsMaxStorageKeyLen: u32 = 256; + pub const ContractsMaxTransientStorageSize: u32 = 1024 * 1024; + pub const ContractsMaxDebugBufferLen: u32 = 2 * 1024 * 1024; + pub const ContractsUnstableInterface: bool = true; +} + +parameter_types! { + pub const ProxyDepositBase: Balance = 1; + pub const ProxyDepositFactor: Balance = 1; + pub const MaxProxies: u32 = 32; + pub const MaxPending: u32 = 32; + pub const AnnouncementDepositBase: Balance = 1; + pub const AnnouncementDepositFactor: Balance = 1; +} + +parameter_types! { + pub const InitialMinAllowedWeights: u16 = 0; + pub const InitialEmissionValue: u16 = 0; + pub const InitialMaxWeightsLimit: u16 = u16::MAX; + pub BlockWeights: limits::BlockWeights = limits::BlockWeights::with_sensible_defaults( + Weight::from_parts(2_000_000_000_000, u64::MAX), + Perbill::from_percent(75), + ); + pub const ExistentialDeposit: Balance = 1; + pub const TransactionByteFee: Balance = 100; + pub const SDebug:u64 = 1; + pub const InitialRho: u16 = 30; + pub const InitialAlphaSigmoidSteepness: i16 = 1000; + pub const InitialKappa: u16 = 32_767; + pub const InitialTempo: u16 = 360; + pub const SelfOwnership: u64 = 2; + pub const InitialImmunityPeriod: u16 = 2; + pub const InitialMinAllowedUids: u16 = 2; + pub const InitialMaxAllowedUids: u16 = 4; + pub const InitialBondsMovingAverage: u64 = 900_000; + pub const InitialBondsPenalty:u16 = u16::MAX; + pub const InitialBondsResetOn: bool = false; + pub const InitialStakePruningMin: u16 = 0; + pub const InitialFoundationDistribution: u64 = 0; + pub const InitialDefaultDelegateTake: u16 = 11_796; // 18%, same as in production + pub const InitialMinDelegateTake: u16 = 5_898; // 9%; + pub const InitialDefaultChildKeyTake: u16 = 0 ;// 0 % + pub const InitialMinChildKeyTake: u16 = 0; // 0 %; + pub const InitialMaxChildKeyTake: u16 = 11_796; // 18 %; + pub const InitialWeightsVersionKey: u16 = 0; + pub const InitialServingRateLimit: u64 = 0; // No limit. + pub const InitialTxRateLimit: u64 = 0; // Disable rate limit for testing + pub const InitialTxDelegateTakeRateLimit: u64 = 1; // 1 block take rate limit for testing + pub const InitialTxChildKeyTakeRateLimit: u64 = 1; // 1 block take rate limit for testing + pub const InitialBurn: u64 = 0; + pub const InitialMinBurn: u64 = 500_000; + pub const InitialMaxBurn: u64 = 1_000_000_000; + pub const MinBurnUpperBound: TaoCurrency = TaoCurrency::new(1_000_000_000); // 1 TAO + pub const MaxBurnLowerBound: TaoCurrency = TaoCurrency::new(100_000_000); // 0.1 TAO + pub const InitialValidatorPruneLen: u64 = 0; + pub const InitialScalingLawPower: u16 = 50; + pub const InitialMaxAllowedValidators: u16 = 100; + pub const InitialIssuance: u64 = 0; + pub const InitialDifficulty: u64 = 10000; + pub const InitialActivityCutoff: u16 = 5000; + pub const InitialAdjustmentInterval: u16 = 100; + pub const InitialAdjustmentAlpha: u64 = 0; // no weight to previous value. + pub const InitialMaxRegistrationsPerBlock: u16 = 3; + pub const InitialTargetRegistrationsPerInterval: u16 = 2; + pub const InitialPruningScore : u16 = u16::MAX; + pub const InitialRegistrationRequirement: u16 = u16::MAX; // Top 100% + pub const InitialMinDifficulty: u64 = 1; + pub const InitialMaxDifficulty: u64 = u64::MAX; + pub const InitialRAORecycledForRegistration: u64 = 0; + pub const InitialNetworkImmunityPeriod: u64 = 1_296_000; + pub const InitialNetworkMinLockCost: u64 = 100_000_000_000; + pub const InitialSubnetOwnerCut: u16 = 0; // 0%. 100% of rewards go to validators + miners. + pub const InitialNetworkLockReductionInterval: u64 = 2; // 2 blocks. + pub const InitialNetworkRateLimit: u64 = 0; + pub const InitialKeySwapCost: u64 = 1_000_000_000; + pub const InitialAlphaHigh: u16 = 58982; // Represents 0.9 as per the production default + pub const InitialAlphaLow: u16 = 45875; // Represents 0.7 as per the production default + pub const InitialLiquidAlphaOn: bool = false; // Default value for LiquidAlphaOn + pub const InitialYuma3On: bool = false; // Default value for Yuma3On + // pub const InitialNetworkMaxStake: u64 = u64::MAX; // (DEPRECATED) + pub const InitialColdkeySwapScheduleDuration: u64 = 5 * 24 * 60 * 60 / 12; // Default as 5 days + pub const InitialColdkeySwapRescheduleDuration: u64 = 24 * 60 * 60 / 12; // Default as 1 day + pub const InitialDissolveNetworkScheduleDuration: u64 = 5 * 24 * 60 * 60 / 12; // Default as 5 days + pub const InitialTaoWeight: u64 = 0; // 100% global weight. + pub const InitialEmaPriceHalvingPeriod: u64 = 201_600_u64; // 4 weeks + pub const DurationOfStartCall: u64 = 7 * 24 * 60 * 60 / 12; // Default as 7 days + pub const InitialKeySwapOnSubnetCost: u64 = 10_000_000; + pub const HotkeySwapOnSubnetInterval: u64 = 15; // 15 block, should be bigger than subnet number, then trigger clean up for all subnets + pub const MaxContributorsPerLeaseToRemove: u32 = 3; + pub const LeaseDividendsDistributionInterval: u32 = 100; + pub const MaxImmuneUidsPercentage: Percent = Percent::from_percent(80); + pub const EvmKeyAssociateRateLimit: u64 = 10; +} + +impl pallet_subtensor::Config for Test { + type RuntimeCall = RuntimeCall; + type Currency = Balances; + type InitialIssuance = InitialIssuance; + type SudoRuntimeCall = TestRuntimeCall; + type Scheduler = Scheduler; + type InitialMinAllowedWeights = InitialMinAllowedWeights; + type InitialEmissionValue = InitialEmissionValue; + type InitialMaxWeightsLimit = InitialMaxWeightsLimit; + type InitialTempo = InitialTempo; + type InitialDifficulty = InitialDifficulty; + type InitialAdjustmentInterval = InitialAdjustmentInterval; + type InitialAdjustmentAlpha = InitialAdjustmentAlpha; + type InitialTargetRegistrationsPerInterval = InitialTargetRegistrationsPerInterval; + type InitialRho = InitialRho; + type InitialAlphaSigmoidSteepness = InitialAlphaSigmoidSteepness; + type InitialKappa = InitialKappa; + type InitialMinAllowedUids = InitialMinAllowedUids; + type InitialMaxAllowedUids = InitialMaxAllowedUids; + type InitialValidatorPruneLen = InitialValidatorPruneLen; + type InitialScalingLawPower = InitialScalingLawPower; + type InitialImmunityPeriod = InitialImmunityPeriod; + type InitialActivityCutoff = InitialActivityCutoff; + type InitialMaxRegistrationsPerBlock = InitialMaxRegistrationsPerBlock; + type InitialPruningScore = InitialPruningScore; + type InitialBondsMovingAverage = InitialBondsMovingAverage; + type InitialBondsPenalty = InitialBondsPenalty; + type InitialBondsResetOn = InitialBondsResetOn; + type InitialMaxAllowedValidators = InitialMaxAllowedValidators; + type InitialDefaultDelegateTake = InitialDefaultDelegateTake; + type InitialMinDelegateTake = InitialMinDelegateTake; + type InitialDefaultChildKeyTake = InitialDefaultChildKeyTake; + type InitialMinChildKeyTake = InitialMinChildKeyTake; + type InitialMaxChildKeyTake = InitialMaxChildKeyTake; + type InitialTxChildKeyTakeRateLimit = InitialTxChildKeyTakeRateLimit; + type InitialWeightsVersionKey = InitialWeightsVersionKey; + type InitialMaxDifficulty = InitialMaxDifficulty; + type InitialMinDifficulty = InitialMinDifficulty; + type InitialServingRateLimit = InitialServingRateLimit; + type InitialTxRateLimit = InitialTxRateLimit; + type InitialTxDelegateTakeRateLimit = InitialTxDelegateTakeRateLimit; + type InitialBurn = InitialBurn; + type InitialMaxBurn = InitialMaxBurn; + type InitialMinBurn = InitialMinBurn; + type MinBurnUpperBound = MinBurnUpperBound; + type MaxBurnLowerBound = MaxBurnLowerBound; + type InitialRAORecycledForRegistration = InitialRAORecycledForRegistration; + type InitialNetworkImmunityPeriod = InitialNetworkImmunityPeriod; + type InitialNetworkMinLockCost = InitialNetworkMinLockCost; + type InitialSubnetOwnerCut = InitialSubnetOwnerCut; + type InitialNetworkLockReductionInterval = InitialNetworkLockReductionInterval; + type InitialNetworkRateLimit = InitialNetworkRateLimit; + type KeySwapCost = InitialKeySwapCost; + type AlphaHigh = InitialAlphaHigh; + type AlphaLow = InitialAlphaLow; + type LiquidAlphaOn = InitialLiquidAlphaOn; + type Yuma3On = InitialYuma3On; + type Preimages = Preimage; + type InitialColdkeySwapScheduleDuration = InitialColdkeySwapScheduleDuration; + type InitialColdkeySwapRescheduleDuration = InitialColdkeySwapRescheduleDuration; + type InitialDissolveNetworkScheduleDuration = InitialDissolveNetworkScheduleDuration; + type InitialTaoWeight = InitialTaoWeight; + type InitialEmaPriceHalvingPeriod = InitialEmaPriceHalvingPeriod; + type DurationOfStartCall = DurationOfStartCall; + type SwapInterface = pallet_subtensor_swap::Pallet; + type KeySwapOnSubnetCost = InitialKeySwapOnSubnetCost; + type HotkeySwapOnSubnetInterval = HotkeySwapOnSubnetInterval; + type ProxyInterface = FakeProxier; + type LeaseDividendsDistributionInterval = LeaseDividendsDistributionInterval; + type GetCommitments = (); + type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; + type CommitmentsInterface = CommitmentsI; + type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; +} + +// Swap-related parameter types +parameter_types! { + pub const SwapProtocolId: PalletId = PalletId(*b"ten/swap"); + pub const SwapMaxFeeRate: u16 = 10000; // 15.26% + pub const SwapMaxPositions: u32 = 100; + pub const SwapMinimumLiquidity: u64 = 1_000; + pub const SwapMinimumReserve: NonZeroU64 = NonZeroU64::new(100).unwrap(); +} + +impl pallet_subtensor_swap::Config for Test { + type SubnetInfo = SubtensorModule; + type BalanceOps = SubtensorModule; + type ProtocolId = SwapProtocolId; + type TaoReserve = TaoCurrencyReserve; + type AlphaReserve = AlphaCurrencyReserve; + type MaxFeeRate = SwapMaxFeeRate; + type MaxPositions = SwapMaxPositions; + type MinimumLiquidity = SwapMinimumLiquidity; + type MinimumReserve = SwapMinimumReserve; + type WeightInfo = (); +} + +pub struct OriginPrivilegeCmp; + +impl PrivilegeCmp for OriginPrivilegeCmp { + fn cmp_privilege(_left: &OriginCaller, _right: &OriginCaller) -> Option { + Some(Ordering::Less) + } +} + +pub struct CommitmentsI; +impl CommitmentsInterface for CommitmentsI { + fn purge_netuid(_netuid: NetUid) {} +} + +parameter_types! { + pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * + BlockWeights::get().max_block; + pub const MaxScheduledPerBlock: u32 = 50; + pub const NoPreimagePostponement: Option = Some(10); +} + +impl pallet_scheduler::Config for Test { + type RuntimeOrigin = RuntimeOrigin; + type RuntimeEvent = RuntimeEvent; + type PalletsOrigin = OriginCaller; + type RuntimeCall = RuntimeCall; + type MaximumWeight = MaximumSchedulerWeight; + type ScheduleOrigin = EnsureRoot; + type MaxScheduledPerBlock = MaxScheduledPerBlock; + type WeightInfo = pallet_scheduler::weights::SubstrateWeight; + type OriginPrivilegeCmp = OriginPrivilegeCmp; + type Preimages = Preimage; + type BlockNumberProvider = System; +} + +impl pallet_utility::Config for Test { + type RuntimeCall = RuntimeCall; + type PalletsOrigin = OriginCaller; + type WeightInfo = pallet_utility::weights::SubstrateWeight; +} + +parameter_types! { + pub const PreimageMaxSize: u32 = 4096 * 1024; + pub const PreimageBaseDeposit: Balance = 1; + pub const PreimageByteDeposit: Balance = 1; +} + +impl pallet_preimage::Config for Test { + type WeightInfo = pallet_preimage::weights::SubstrateWeight; + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type ManagerOrigin = EnsureRoot; + type Consideration = (); +} + +thread_local! { + pub static PROXIES: RefCell = const { RefCell::new(FakeProxier(vec![])) }; +} + +pub struct FakeProxier(pub Vec<(U256, U256)>); + +impl ProxyInterface for FakeProxier { + fn add_lease_beneficiary_proxy(beneficiary: &AccountId, lease: &AccountId) -> DispatchResult { + PROXIES.with_borrow_mut(|proxies| { + proxies.0.push((*beneficiary, *lease)); + }); + Ok(()) + } + + fn remove_lease_beneficiary_proxy( + beneficiary: &AccountId, + lease: &AccountId, + ) -> DispatchResult { + PROXIES.with_borrow_mut(|proxies| { + proxies.0.retain(|(b, l)| b != beneficiary && l != lease); + }); + Ok(()) + } +} + +parameter_types! { + pub const CrowdloanPalletId: PalletId = PalletId(*b"bt/cloan"); + pub const MinimumDeposit: u64 = 50; + pub const AbsoluteMinimumContribution: u64 = 10; + pub const MinimumBlockDuration: u64 = 20; + pub const MaximumBlockDuration: u64 = 100; + pub const RefundContributorsLimit: u32 = 5; + pub const MaxContributors: u32 = 10; +} + +impl pallet_crowdloan::Config for Test { + type PalletId = CrowdloanPalletId; + type Currency = Balances; + type RuntimeCall = RuntimeCall; + type WeightInfo = pallet_crowdloan::weights::SubstrateWeight; + type Preimages = Preimage; + type MinimumDeposit = MinimumDeposit; + type AbsoluteMinimumContribution = AbsoluteMinimumContribution; + type MinimumBlockDuration = MinimumBlockDuration; + type MaximumBlockDuration = MaximumBlockDuration; + type RefundContributorsLimit = RefundContributorsLimit; + type MaxContributors = MaxContributors; +} + +mod test_crypto { + use super::KEY_TYPE; + use sp_core::{ + U256, + sr25519::{Public as Sr25519Public, Signature as Sr25519Signature}, + }; + use sp_runtime::{ + app_crypto::{app_crypto, sr25519}, + traits::IdentifyAccount, + }; + + app_crypto!(sr25519, KEY_TYPE); + + pub struct TestAuthId; + + impl frame_system::offchain::AppCrypto for TestAuthId { + type RuntimeAppPublic = Public; + type GenericSignature = Sr25519Signature; + type GenericPublic = Sr25519Public; + } + + impl IdentifyAccount for Public { + type AccountId = U256; + + fn into_account(self) -> U256 { + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(self.as_ref()); + U256::from_big_endian(&bytes) + } + } +} + +pub type TestAuthId = test_crypto::TestAuthId; + +impl pallet_drand::Config for Test { + type AuthorityId = TestAuthId; + type Verifier = pallet_drand::verifier::QuicknetVerifier; + type UnsignedPriority = ConstU64<{ 1 << 20 }>; + type HttpFetchTimeout = ConstU64<1_000>; +} + +impl frame_system::offchain::SigningTypes for Test { + type Public = test_crypto::Public; + type Signature = test_crypto::Signature; +} + +pub type UncheckedExtrinsic = sp_runtime::testing::TestXt; + +impl frame_system::offchain::CreateTransactionBase for Test +where + RuntimeCall: From, +{ + type Extrinsic = UncheckedExtrinsic; + type RuntimeCall = RuntimeCall; +} + +impl frame_system::offchain::CreateInherent for Test +where + RuntimeCall: From, +{ + fn create_bare(call: Self::RuntimeCall) -> Self::Extrinsic { + UncheckedExtrinsic::new_inherent(call) + } +} + +impl frame_system::offchain::CreateSignedTransaction for Test +where + RuntimeCall: From, +{ + fn create_signed_transaction< + C: frame_system::offchain::AppCrypto, + >( + call: >::RuntimeCall, + _public: Self::Public, + _account: Self::AccountId, + nonce: Self::Nonce, + ) -> Option { + Some(UncheckedExtrinsic::new_signed(call, nonce.into(), (), ())) + } +} + +static TEST_LOGS_INIT: OnceLock<()> = OnceLock::new(); + +pub fn init_logs_for_tests() { + if TEST_LOGS_INIT.get().is_some() { + return; + } + let _ = TEST_LOGS_INIT.set(()); +} + +#[allow(dead_code)] +// Build genesis storage according to the mock runtime. +pub fn new_test_ext(block_number: BlockNumber) -> sp_io::TestExternalities { + init_logs_for_tests(); + let t = frame_system::GenesisConfig::::default() + .build_storage() + .unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(block_number)); + ext +} + +#[allow(dead_code)] +pub fn register_ok_neuron( + netuid: NetUid, + hotkey_account_id: U256, + coldkey_account_id: U256, + start_nonce: u64, +) { + let block_number: u64 = SubtensorModule::get_current_block_as_u64(); + let (nonce, work): (u64, Vec) = SubtensorModule::create_work_for_block_number( + netuid, + block_number, + start_nonce, + &hotkey_account_id, + ); + let result = SubtensorModule::register( + <::RuntimeOrigin>::signed(hotkey_account_id), + netuid, + block_number, + nonce, + work, + hotkey_account_id, + coldkey_account_id, + ); + assert_ok!(result); + log::info!( + "Register ok neuron: netuid: {netuid:?}, coldkey: {hotkey_account_id:?}, hotkey: {coldkey_account_id:?}" + ); +} + +#[allow(dead_code)] +pub fn add_dynamic_network(hotkey: &U256, coldkey: &U256) -> NetUid { + let netuid = SubtensorModule::get_next_netuid(); + let lock_cost = SubtensorModule::get_network_lock_cost(); + SubtensorModule::add_balance_to_coldkey_account(coldkey, lock_cost.into()); + + assert_ok!(SubtensorModule::register_network( + RawOrigin::Signed(*coldkey).into(), + *hotkey + )); + NetworkRegistrationAllowed::::insert(netuid, true); + NetworkPowRegistrationAllowed::::insert(netuid, true); + FirstEmissionBlockNumber::::insert(netuid, 0); + SubtokenEnabled::::insert(netuid, true); + netuid +} + +#[allow(dead_code)] +pub(crate) fn remove_stake_rate_limit_for_tests(hotkey: &U256, coldkey: &U256, netuid: NetUid) { + StakingOperationRateLimiter::::remove((hotkey, coldkey, netuid)); +} + +#[allow(dead_code)] +pub(crate) fn setup_reserves(netuid: NetUid, tao: TaoCurrency, alpha: AlphaCurrency) { + SubnetTAO::::set(netuid, tao); + SubnetAlphaIn::::set(netuid, alpha); +} diff --git a/chain-extensions/src/tests.rs b/chain-extensions/src/tests.rs new file mode 100644 index 000000000..f4c301388 --- /dev/null +++ b/chain-extensions/src/tests.rs @@ -0,0 +1,962 @@ +#![allow(clippy::unwrap_used)] + +use super::{SubtensorChainExtension, SubtensorExtensionEnv, mock}; +use crate::types::{FunctionId, Output}; +use codec::Encode; +use frame_support::{assert_ok, weights::Weight}; +use frame_system::RawOrigin; +use pallet_contracts::chain_extension::RetVal; +use pallet_subtensor::DefaultMinStake; +use sp_core::Get; +use sp_core::U256; +use sp_runtime::DispatchError; +use subtensor_runtime_common::{AlphaCurrency, Currency as CurrencyTrait, NetUid, TaoCurrency}; +use subtensor_swap_interface::SwapHandler; + +type AccountId = ::AccountId; + +#[derive(Clone)] +struct MockEnv { + func_id: u16, + caller: AccountId, + input: Vec, + output: Vec, + charged_weight: Option, + expected_weight: Option, +} + +#[test] +fn set_coldkey_auto_stake_hotkey_success_sets_destination() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(4901); + let owner_coldkey = U256::from(4902); + let coldkey = U256::from(5901); + let hotkey = U256::from(5902); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + + pallet_subtensor::Owner::::insert(hotkey, coldkey); + pallet_subtensor::OwnedHotkeys::::insert(coldkey, vec![hotkey]); + pallet_subtensor::Uids::::insert(netuid, hotkey, 0u16); + + assert_eq!( + pallet_subtensor::AutoStakeDestination::::get(coldkey, netuid), + None + ); + + let expected_weight = Weight::from_parts(29_930_000, 0) + .saturating_add(::DbWeight::get().reads(4)) + .saturating_add(::DbWeight::get().writes(2)); + + let mut env = MockEnv::new( + FunctionId::SetColdkeyAutoStakeHotkeyV1, + coldkey, + (netuid, hotkey).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + assert_eq!( + pallet_subtensor::AutoStakeDestination::::get(coldkey, netuid), + Some(hotkey) + ); + let coldkeys = + pallet_subtensor::AutoStakeDestinationColdkeys::::get(hotkey, netuid); + assert!(coldkeys.contains(&coldkey)); + }); +} + +#[test] +fn remove_stake_full_limit_success_with_limit_price() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(4801); + let owner_coldkey = U256::from(4802); + let coldkey = U256::from(5801); + let hotkey = U256::from(5802); + let stake_amount_raw: u64 = 340_000_000_000; + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + TaoCurrency::from(130_000_000_000), + AlphaCurrency::from(110_000_000_000), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &coldkey, + stake_amount_raw + 1_000_000_000, + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + hotkey, + netuid, + stake_amount_raw.into(), + )); + + mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); + + let expected_weight = Weight::from_parts(395_300_000, 0) + .saturating_add(::DbWeight::get().reads(28)) + .saturating_add(::DbWeight::get().writes(14)); + + let balance_before = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + + let mut env = MockEnv::new( + FunctionId::RemoveStakeFullLimitV1, + coldkey, + (hotkey, netuid, Option::::None).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + let balance_after = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + + assert!(alpha_after.is_zero()); + assert!(balance_after > balance_before); + }); +} + +#[test] +fn swap_stake_limit_with_tight_price_returns_slippage_error() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey_a = U256::from(4701); + let owner_coldkey_a = U256::from(4702); + let owner_hotkey_b = U256::from(4703); + let owner_coldkey_b = U256::from(4704); + let coldkey = U256::from(5701); + let hotkey = U256::from(5702); + + let stake_alpha = AlphaCurrency::from(150_000_000_000u64); + + let netuid_a = mock::add_dynamic_network(&owner_hotkey_a, &owner_coldkey_a); + let netuid_b = mock::add_dynamic_network(&owner_hotkey_b, &owner_coldkey_b); + + mock::setup_reserves( + netuid_a, + TaoCurrency::from(150_000_000_000), + AlphaCurrency::from(110_000_000_000), + ); + mock::setup_reserves( + netuid_b, + TaoCurrency::from(120_000_000_000), + AlphaCurrency::from(90_000_000_000), + ); + + mock::register_ok_neuron(netuid_a, hotkey, coldkey, 0); + mock::register_ok_neuron(netuid_b, hotkey, coldkey, 1); + + pallet_subtensor::Pallet::::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + netuid_a, + stake_alpha, + ); + + mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid_a); + + let alpha_origin_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_a, + ); + let alpha_destination_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_b, + ); + + let alpha_to_swap: AlphaCurrency = (alpha_origin_before.to_u64() / 8).into(); + let limit_price: TaoCurrency = 100u64.into(); + + let expected_weight = Weight::from_parts(411_500_000, 0) + .saturating_add(::DbWeight::get().reads(35)) + .saturating_add(::DbWeight::get().writes(22)); + + let mut env = MockEnv::new( + FunctionId::SwapStakeLimitV1, + coldkey, + (hotkey, netuid_a, netuid_b, alpha_to_swap, limit_price, true).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let alpha_origin_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_a, + ); + let alpha_destination_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_b, + ); + + assert!(alpha_origin_after <= alpha_origin_before); + assert!(alpha_destination_after >= alpha_destination_before); + }); +} + +#[test] +fn remove_stake_limit_success_respects_price_limit() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(4601); + let owner_coldkey = U256::from(4602); + let coldkey = U256::from(5601); + let hotkey = U256::from(5602); + let stake_amount_raw: u64 = 320_000_000_000; + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + TaoCurrency::from(120_000_000_000), + AlphaCurrency::from(100_000_000_000), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &coldkey, + stake_amount_raw + 1_000_000_000, + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + hotkey, + netuid, + stake_amount_raw.into(), + )); + + mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); + + let alpha_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + + let current_price = + ::SwapInterface::current_alpha_price( + netuid.into(), + ); + let limit_price_value = (current_price.to_num::() * 990_000_000f64).round() as u64; + let limit_price: TaoCurrency = limit_price_value.into(); + + let alpha_to_unstake: AlphaCurrency = (alpha_before.to_u64() / 2).into(); + + let expected_weight = Weight::from_parts(377_400_000, 0) + .saturating_add(::DbWeight::get().reads(28)) + .saturating_add(::DbWeight::get().writes(14)); + + let balance_before = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + + let mut env = MockEnv::new( + FunctionId::RemoveStakeLimitV1, + coldkey, + (hotkey, netuid, alpha_to_unstake, limit_price, true).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + let balance_after = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + + assert!(alpha_after < alpha_before); + assert!(balance_after > balance_before); + }); +} + +#[test] +fn add_stake_limit_success_executes_within_price_guard() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(4501); + let owner_coldkey = U256::from(4502); + let coldkey = U256::from(5501); + let hotkey = U256::from(5502); + let amount_raw: u64 = 900_000_000_000; + let limit_price: TaoCurrency = 24_000_000_000u64.into(); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + + mock::setup_reserves( + netuid, + TaoCurrency::from(150_000_000_000), + AlphaCurrency::from(100_000_000_000), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &coldkey, + amount_raw + 1_000_000_000, + ); + + let stake_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + let balance_before = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + + let expected_weight = Weight::from_parts(402_900_000, 0) + .saturating_add(::DbWeight::get().reads(24)) + .saturating_add(::DbWeight::get().writes(15)); + + let mut env = MockEnv::new( + FunctionId::AddStakeLimitV1, + coldkey, + ( + hotkey, + netuid, + TaoCurrency::from(amount_raw), + limit_price, + true, + ) + .encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let stake_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + let balance_after = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + + assert!(stake_after > stake_before); + assert!(stake_after > AlphaCurrency::ZERO); + assert!(balance_after < balance_before); + }); +} + +#[test] +fn swap_stake_success_moves_between_subnets() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey_a = U256::from(4401); + let owner_coldkey_a = U256::from(4402); + let owner_hotkey_b = U256::from(4403); + let owner_coldkey_b = U256::from(4404); + let coldkey = U256::from(5401); + let hotkey = U256::from(5402); + + let min_stake = DefaultMinStake::::get(); + let stake_amount_raw = min_stake.to_u64().saturating_mul(260); + + let netuid_a = mock::add_dynamic_network(&owner_hotkey_a, &owner_coldkey_a); + let netuid_b = mock::add_dynamic_network(&owner_hotkey_b, &owner_coldkey_b); + + mock::setup_reserves( + netuid_a, + stake_amount_raw.saturating_mul(18).into(), + AlphaCurrency::from(stake_amount_raw.saturating_mul(30)), + ); + mock::setup_reserves( + netuid_b, + stake_amount_raw.saturating_mul(20).into(), + AlphaCurrency::from(stake_amount_raw.saturating_mul(28)), + ); + + mock::register_ok_neuron(netuid_a, hotkey, coldkey, 0); + mock::register_ok_neuron(netuid_b, hotkey, coldkey, 1); + + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &coldkey, + stake_amount_raw + 1_000_000_000, + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + hotkey, + netuid_a, + stake_amount_raw.into(), + )); + + mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid_a); + + let alpha_origin_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_a, + ); + let alpha_destination_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_b, + ); + let alpha_to_swap: AlphaCurrency = (alpha_origin_before.to_u64() / 3).into(); + + let expected_weight = Weight::from_parts(351_300_000, 0) + .saturating_add(::DbWeight::get().reads(35)) + .saturating_add(::DbWeight::get().writes(22)); + + let mut env = MockEnv::new( + FunctionId::SwapStakeV1, + coldkey, + (hotkey, netuid_a, netuid_b, alpha_to_swap).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let alpha_origin_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_a, + ); + let alpha_destination_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid_b, + ); + + assert!(alpha_origin_after < alpha_origin_before); + assert!( + alpha_destination_after > alpha_destination_before, + "destination stake should increase" + ); + }); +} + +#[test] +fn transfer_stake_success_moves_between_coldkeys() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(4301); + let owner_coldkey = U256::from(4302); + let origin_coldkey = U256::from(5301); + let destination_coldkey = U256::from(5302); + let hotkey = U256::from(5303); + + let min_stake = DefaultMinStake::::get(); + let stake_amount_raw = min_stake.to_u64().saturating_mul(250); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + stake_amount_raw.saturating_mul(15).into(), + AlphaCurrency::from(stake_amount_raw.saturating_mul(25)), + ); + + mock::register_ok_neuron(netuid, hotkey, origin_coldkey, 0); + + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &origin_coldkey, + stake_amount_raw + 1_000_000_000, + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(origin_coldkey).into(), + hotkey, + netuid, + stake_amount_raw.into(), + )); + + mock::remove_stake_rate_limit_for_tests(&hotkey, &origin_coldkey, netuid); + + let alpha_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &origin_coldkey, + netuid, + ); + let alpha_to_transfer: AlphaCurrency = (alpha_before.to_u64() / 3).into(); + + let expected_weight = Weight::from_parts(160_300_000, 0) + .saturating_add(::DbWeight::get().reads(13)) + .saturating_add(::DbWeight::get().writes(6)); + + let mut env = MockEnv::new( + FunctionId::TransferStakeV1, + origin_coldkey, + ( + destination_coldkey, + hotkey, + netuid, + netuid, + alpha_to_transfer, + ) + .encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let origin_alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &origin_coldkey, + netuid, + ); + let destination_alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &destination_coldkey, + netuid, + ); + + assert_eq!(origin_alpha_after, alpha_before - alpha_to_transfer); + assert_eq!(destination_alpha_after, alpha_to_transfer); + }); +} + +#[test] +fn move_stake_success_moves_alpha_between_hotkeys() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(4201); + let owner_coldkey = U256::from(4202); + let coldkey = U256::from(5201); + let origin_hotkey = U256::from(5202); + let destination_hotkey = U256::from(5203); + + let min_stake = DefaultMinStake::::get(); + let stake_amount_raw = min_stake.to_u64().saturating_mul(240); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + stake_amount_raw.saturating_mul(15).into(), + AlphaCurrency::from(stake_amount_raw.saturating_mul(25)), + ); + + mock::register_ok_neuron(netuid, origin_hotkey, coldkey, 0); + mock::register_ok_neuron(netuid, destination_hotkey, coldkey, 1); + + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &coldkey, + stake_amount_raw + 1_000_000_000, + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + origin_hotkey, + netuid, + stake_amount_raw.into(), + )); + + mock::remove_stake_rate_limit_for_tests(&origin_hotkey, &coldkey, netuid); + + let alpha_before = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &origin_hotkey, + &coldkey, + netuid, + ); + let alpha_to_move: AlphaCurrency = (alpha_before.to_u64() / 2).into(); + + let expected_weight = Weight::from_parts(164_300_000, 0) + .saturating_add(::DbWeight::get().reads(15)) + .saturating_add(::DbWeight::get().writes(7)); + + let mut env = MockEnv::new( + FunctionId::MoveStakeV1, + coldkey, + ( + origin_hotkey, + destination_hotkey, + netuid, + netuid, + alpha_to_move, + ) + .encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let origin_alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &origin_hotkey, + &coldkey, + netuid, + ); + let destination_alpha_after = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &destination_hotkey, + &coldkey, + netuid, + ); + + assert_eq!(origin_alpha_after, alpha_before - alpha_to_move); + assert_eq!(destination_alpha_after, alpha_to_move); + }); +} + +#[test] +fn unstake_all_alpha_success_moves_stake_to_root() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(4101); + let owner_coldkey = U256::from(4102); + let coldkey = U256::from(5101); + let hotkey = U256::from(5102); + let min_stake = DefaultMinStake::::get(); + let stake_amount_raw = min_stake.to_u64().saturating_mul(220); + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + + mock::setup_reserves( + netuid, + stake_amount_raw.saturating_mul(20).into(), + AlphaCurrency::from(stake_amount_raw.saturating_mul(30)), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &coldkey, + stake_amount_raw + 1_000_000_000, + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + hotkey, + netuid, + stake_amount_raw.into(), + )); + + mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); + + let expected_weight = Weight::from_parts(358_500_000, 0) + .saturating_add(::DbWeight::get().reads(36)) + .saturating_add(::DbWeight::get().writes(21)); + + let mut env = MockEnv::new(FunctionId::UnstakeAllAlphaV1, coldkey, hotkey.encode()) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let subnet_alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + assert!(subnet_alpha <= AlphaCurrency::from(1_000)); + + let root_alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &coldkey, + NetUid::ROOT, + ); + assert!(root_alpha > AlphaCurrency::ZERO); + }); +} + +#[test] +fn add_proxy_success_creates_proxy_relationship() { + mock::new_test_ext(1).execute_with(|| { + let delegator = U256::from(6001); + let delegate = U256::from(6002); + + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &delegator, + 1_000_000_000, + ); + + assert_eq!( + pallet_subtensor_proxy::Proxies::::get(delegator) + .0 + .len(), + 0 + ); + + let mut env = MockEnv::new(FunctionId::AddProxyV1, delegator, delegate.encode()); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_success(ret); + + let proxies = pallet_subtensor_proxy::Proxies::::get(delegator).0; + assert_eq!(proxies.len(), 1); + assert_eq!(proxies[0].delegate, delegate); + assert_eq!( + proxies[0].proxy_type, + subtensor_runtime_common::ProxyType::Staking + ); + assert_eq!(proxies[0].delay, 0u64); + }); +} + +#[test] +fn remove_proxy_success_removes_proxy_relationship() { + mock::new_test_ext(1).execute_with(|| { + let delegator = U256::from(7001); + let delegate = U256::from(7002); + + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &delegator, + 1_000_000_000, + ); + + let mut add_env = MockEnv::new(FunctionId::AddProxyV1, delegator, delegate.encode()); + let ret = SubtensorChainExtension::::dispatch(&mut add_env).unwrap(); + assert_success(ret); + + let proxies_before = pallet_subtensor_proxy::Proxies::::get(delegator).0; + assert_eq!(proxies_before.len(), 1); + + let mut remove_env = MockEnv::new(FunctionId::RemoveProxyV1, delegator, delegate.encode()); + let ret = SubtensorChainExtension::::dispatch(&mut remove_env).unwrap(); + assert_success(ret); + + let proxies_after = pallet_subtensor_proxy::Proxies::::get(delegator).0; + assert_eq!(proxies_after.len(), 0); + }); +} + +impl MockEnv { + fn new(func_id: FunctionId, caller: AccountId, input: Vec) -> Self { + Self { + func_id: func_id as u16, + caller, + input, + output: Vec::new(), + charged_weight: None, + expected_weight: None, + } + } + + fn with_expected_weight(mut self, weight: Weight) -> Self { + self.expected_weight = Some(weight); + self + } + + fn charged_weight(&self) -> Option { + self.charged_weight + } + + fn output(&self) -> &[u8] { + &self.output + } +} + +impl SubtensorExtensionEnv for MockEnv { + fn func_id(&self) -> u16 { + self.func_id + } + + fn charge_weight(&mut self, weight: Weight) -> Result<(), DispatchError> { + if let Some(expected) = self.expected_weight { + if weight != expected { + return Err(DispatchError::Other( + "unexpected weight charged by mock env", + )); + } + } + self.charged_weight = Some(weight); + Ok(()) + } + + fn read_as(&mut self) -> Result { + T::decode(&mut &self.input[..]).map_err(|_| DispatchError::Other("mock env decode failure")) + } + + fn write_output(&mut self, data: &[u8]) -> Result<(), DispatchError> { + self.output.clear(); + self.output.extend_from_slice(data); + Ok(()) + } + + fn caller(&mut self) -> AccountId { + self.caller + } +} + +fn assert_success(ret: RetVal) { + match ret { + RetVal::Converging(code) => { + assert_eq!(code, Output::Success as u32, "expected success code") + } + _ => panic!("unexpected return value"), + } +} + +#[test] +fn get_stake_info_returns_encoded_runtime_value() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let hotkey = U256::from(11); + let coldkey = U256::from(22); + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + let expected = + pallet_subtensor::Pallet::::get_stake_info_for_hotkey_coldkey_netuid( + hotkey, coldkey, netuid, + ) + .encode(); + + let mut env = MockEnv::new( + FunctionId::GetStakeInfoForHotkeyColdkeyNetuidV1, + coldkey, + (hotkey, coldkey, netuid).encode(), + ); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + + assert_success(ret); + assert_eq!(env.output(), expected.as_slice()); + assert!(env.charged_weight().is_none()); + }); +} + +#[test] +fn add_stake_success_updates_stake_and_returns_success_code() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let coldkey = U256::from(101); + let hotkey = U256::from(202); + let min_stake = DefaultMinStake::::get(); + let amount_raw = min_stake.to_u64().saturating_mul(10); + let amount: TaoCurrency = amount_raw.into(); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::setup_reserves( + netuid, + (amount_raw * 1_000_000).into(), + AlphaCurrency::from(amount_raw * 10_000_000), + ); + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &coldkey, amount_raw, + ); + + assert!( + pallet_subtensor::Pallet::::get_total_stake_for_hotkey(&hotkey).is_zero() + ); + + let expected_weight = Weight::from_parts(340_800_000, 0) + .saturating_add(::DbWeight::get().reads(24)) + .saturating_add(::DbWeight::get().writes(15)); + + let mut env = MockEnv::new( + FunctionId::AddStakeV1, + coldkey, + (hotkey, netuid, amount).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + + assert_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let total_stake = + pallet_subtensor::Pallet::::get_total_stake_for_hotkey(&hotkey); + assert!(total_stake > TaoCurrency::ZERO); + }); +} + +#[test] +fn remove_stake_with_no_stake_returns_amount_too_low() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let coldkey = U256::from(301); + let hotkey = U256::from(302); + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + + let min_stake = DefaultMinStake::::get(); + let amount: AlphaCurrency = AlphaCurrency::from(min_stake.to_u64()); + + let expected_weight = Weight::from_parts(196_800_000, 0) + .saturating_add(::DbWeight::get().reads(19)) + .saturating_add(::DbWeight::get().writes(10)); + + let mut env = MockEnv::new( + FunctionId::RemoveStakeV1, + coldkey, + (hotkey, netuid, amount).encode(), + ) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + + match ret { + RetVal::Converging(code) => { + assert_eq!(code, Output::AmountTooLow as u32, "mismatched error output") + } + _ => panic!("unexpected return value"), + } + assert_eq!(env.charged_weight(), Some(expected_weight)); + assert!( + pallet_subtensor::Pallet::::get_total_stake_for_hotkey(&hotkey).is_zero() + ); + }); +} + +#[test] +fn unstake_all_success_unstakes_balance() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(4001); + let owner_coldkey = U256::from(4002); + let coldkey = U256::from(5001); + let hotkey = U256::from(5002); + let min_stake = DefaultMinStake::::get(); + let stake_amount_raw = min_stake.to_u64().saturating_mul(200); + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + + mock::setup_reserves( + netuid, + stake_amount_raw.saturating_mul(10).into(), + AlphaCurrency::from(stake_amount_raw.saturating_mul(20)), + ); + + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + pallet_subtensor::Pallet::::add_balance_to_coldkey_account( + &coldkey, + stake_amount_raw + 1_000_000_000, + ); + + assert_ok!(pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(coldkey).into(), + hotkey, + netuid, + stake_amount_raw.into(), + )); + + mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); + + let expected_weight = Weight::from_parts(28_830_000, 0) + .saturating_add(::DbWeight::get().reads(6)) + .saturating_add(::DbWeight::get().writes(0)); + + let pre_balance = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + + let mut env = MockEnv::new(FunctionId::UnstakeAllV1, coldkey, hotkey.encode()) + .with_expected_weight(expected_weight); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_success(ret); + assert_eq!(env.charged_weight(), Some(expected_weight)); + + let remaining_alpha = + pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, + ); + assert!(remaining_alpha <= AlphaCurrency::from(1_000)); + + let post_balance = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); + assert!(post_balance > pre_balance); + }); +} diff --git a/chain-extensions/src/types.rs b/chain-extensions/src/types.rs new file mode 100644 index 000000000..7c0bfe420 --- /dev/null +++ b/chain-extensions/src/types.rs @@ -0,0 +1,98 @@ +use codec::{Decode, Encode}; +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use sp_runtime::{DispatchError, ModuleError}; + +#[repr(u16)] +#[derive(TryFromPrimitive, IntoPrimitive, Decode, Encode)] +pub enum FunctionId { + GetStakeInfoForHotkeyColdkeyNetuidV1 = 0, + AddStakeV1 = 1, + RemoveStakeV1 = 2, + UnstakeAllV1 = 3, + UnstakeAllAlphaV1 = 4, + MoveStakeV1 = 5, + TransferStakeV1 = 6, + SwapStakeV1 = 7, + AddStakeLimitV1 = 8, + RemoveStakeLimitV1 = 9, + SwapStakeLimitV1 = 10, + RemoveStakeFullLimitV1 = 11, + SetColdkeyAutoStakeHotkeyV1 = 12, + AddProxyV1 = 13, + RemoveProxyV1 = 14, +} + +#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum Output { + /// Success + Success = 0, + /// Unknown error + RuntimeError = 1, + /// Not enough balance to stake + NotEnoughBalanceToStake = 2, + /// Coldkey is not associated with the hotkey + NonAssociatedColdKey = 3, + /// Error withdrawing balance + BalanceWithdrawalError = 4, + /// Hotkey is not registered + NotRegistered = 5, + /// Not enough stake to withdraw + NotEnoughStakeToWithdraw = 6, + /// Transaction rate limit exceeded + TxRateLimitExceeded = 7, + /// Slippage is too high for the transaction + SlippageTooHigh = 8, + /// Subnet does not exist + SubnetNotExists = 9, + /// Hotkey is not registered in subnet + HotKeyNotRegisteredInSubNet = 10, + /// Same auto stake hotkey already set + SameAutoStakeHotkeyAlreadySet = 11, + /// Insufficient balance + InsufficientBalance = 12, + /// Amount is too low + AmountTooLow = 13, + /// Insufficient liquidity + InsufficientLiquidity = 14, + /// Same netuid + SameNetuid = 15, + /// Too many proxies registered + ProxyTooMany = 16, + /// Proxy already exists + ProxyDuplicate = 17, + /// Cannot add self as proxy + ProxyNoSelfProxy = 18, + /// Proxy relationship not found + ProxyNotFound = 19, +} + +impl From for Output { + fn from(input: DispatchError) -> Self { + let error_text = match input { + DispatchError::Module(ModuleError { message, .. }) => message, + _ => Some("No module error Info"), + }; + match error_text { + Some("NotEnoughBalanceToStake") => Output::NotEnoughBalanceToStake, + Some("NonAssociatedColdKey") => Output::NonAssociatedColdKey, + Some("BalanceWithdrawalError") => Output::BalanceWithdrawalError, + Some("HotKeyNotRegisteredInSubNet") => Output::NotRegistered, + Some("HotKeyAccountNotExists") => Output::NotRegistered, + Some("NotEnoughStakeToWithdraw") => Output::NotEnoughStakeToWithdraw, + Some("TxRateLimitExceeded") => Output::TxRateLimitExceeded, + Some("SlippageTooHigh") => Output::SlippageTooHigh, + Some("SubnetNotExists") => Output::SubnetNotExists, + Some("SameAutoStakeHotkeyAlreadySet") => Output::SameAutoStakeHotkeyAlreadySet, + Some("InsufficientBalance") => Output::InsufficientBalance, + Some("AmountTooLow") => Output::AmountTooLow, + Some("InsufficientLiquidity") => Output::InsufficientLiquidity, + Some("SameNetuid") => Output::SameNetuid, + Some("TooMany") => Output::ProxyTooMany, + Some("Duplicate") => Output::ProxyDuplicate, + Some("NoSelfProxy") => Output::ProxyNoSelfProxy, + Some("NotFound") => Output::ProxyNotFound, + _ => Output::RuntimeError, + } + } +} diff --git a/docs/contracts.md b/docs/contracts.md new file mode 100644 index 000000000..e0b4456cf --- /dev/null +++ b/docs/contracts.md @@ -0,0 +1,105 @@ +# Smart Contracts on Subtensor + +## Overview + +Subtensor now supports smart contract functionality through the integration of `pallet-contracts`, enabling developers to deploy and execute WebAssembly (WASM) smart contracts on the network. Contracts are written in [ink!](https://use.ink/), a Rust-based embedded domain-specific language (eDSL) for writing smart contracts on Substrate-based chains. + +## Getting Started + +For general smart contract development on Subtensor, please refer to the official ink! documentation: +- [ink! Documentation](https://use.ink/docs/v5/) +- [ink! Getting Started Guide](https://use.ink/docs/v5/getting-started/setup) +- [ink! Examples](https://github.com/use-ink/ink-examples/tree/v5.x.x) + +## Subtensor-Specific Features + +### Chain Extension + +Subtensor provides a custom chain extension that allows smart contracts to interact with Subtensor-specific functionality: + +#### Available Functions + +| Function ID | Name | Description | Parameters | Returns | +|------------|------|-------------|------------|---------| +| 0 | `get_stake_info_for_hotkey_coldkey_netuid` | Query stake information | `(AccountId, AccountId, NetUid)` | `Option` | +| 1 | `add_stake` | Delegate stake from coldkey to hotkey | `(AccountId, NetUid, TaoCurrency)` | Error code | +| 2 | `remove_stake` | Withdraw stake from hotkey back to coldkey | `(AccountId, NetUid, AlphaCurrency)` | Error code | +| 3 | `unstake_all` | Unstake all TAO from a hotkey | `(AccountId)` | Error code | +| 4 | `unstake_all_alpha` | Unstake all Alpha from a hotkey | `(AccountId)` | Error code | +| 5 | `move_stake` | Move stake between hotkeys | `(AccountId, AccountId, NetUid, NetUid, AlphaCurrency)` | Error code | +| 6 | `transfer_stake` | Transfer stake between coldkeys | `(AccountId, AccountId, NetUid, NetUid, AlphaCurrency)` | Error code | +| 7 | `swap_stake` | Swap stake allocations between subnets | `(AccountId, NetUid, NetUid, AlphaCurrency)` | Error code | +| 8 | `add_stake_limit` | Delegate stake with a price limit | `(AccountId, NetUid, TaoCurrency, TaoCurrency, bool)` | Error code | +| 9 | `remove_stake_limit` | Withdraw stake with a price limit | `(AccountId, NetUid, AlphaCurrency, TaoCurrency, bool)` | Error code | +| 10 | `swap_stake_limit` | Swap stake between subnets with price limit | `(AccountId, NetUid, NetUid, AlphaCurrency, TaoCurrency, bool)` | Error code | +| 11 | `remove_stake_full_limit` | Fully withdraw stake with optional price limit | `(AccountId, NetUid, Option)` | Error code | +| 12 | `set_coldkey_auto_stake_hotkey` | Configure automatic stake destination | `(NetUid, AccountId)` | Error code | +| 13 | `add_proxy` | Add a staking proxy for the caller | `(AccountId)` | Error code | +| 14 | `remove_proxy` | Remove a staking proxy for the caller | `(AccountId)` | Error code | + +Example usage in your ink! contract: +```rust +#[ink::chain_extension(extension = 0)] +pub trait SubtensorExtension { + type ErrorCode = SubtensorError; + + #[ink(function = 0)] + fn get_stake_info( + hotkey: AccountId, + coldkey: AccountId, + netuid: u16, + ) -> Result, SubtensorError>; +} +``` + +#### Error Codes + +Chain extension functions that modify state return error codes as `u32` values. The following codes are defined: + +| Code | Name | Description | +|------|------|-------------| +| 0 | `Success` | Operation completed successfully | +| 1 | `RuntimeError` | Unknown runtime error occurred | +| 2 | `NotEnoughBalanceToStake` | Insufficient balance to complete stake operation | +| 3 | `NonAssociatedColdKey` | Coldkey is not associated with the hotkey | +| 4 | `BalanceWithdrawalError` | Error occurred during balance withdrawal | +| 5 | `NotRegistered` | Hotkey is not registered in the subnet | +| 6 | `NotEnoughStakeToWithdraw` | Insufficient stake available for withdrawal | +| 7 | `TxRateLimitExceeded` | Transaction rate limit has been exceeded | +| 8 | `SlippageTooHigh` | Price slippage exceeds acceptable threshold | +| 9 | `SubnetNotExists` | Specified subnet does not exist | +| 10 | `HotKeyNotRegisteredInSubNet` | Hotkey is not registered in the specified subnet | +| 11 | `SameAutoStakeHotkeyAlreadySet` | Auto-stake hotkey is already configured | +| 12 | `InsufficientBalance` | Account has insufficient balance | +| 13 | `AmountTooLow` | Transaction amount is below minimum threshold | +| 14 | `InsufficientLiquidity` | Insufficient liquidity for swap operation | +| 15 | `SameNetuid` | Source and destination subnets are the same | +| 16 | `ProxyTooMany` | Too many proxies registered | +| 17 | `ProxyDuplicate` | Proxy already exists | +| 18 | `ProxyNoSelfProxy` | Cannot add self as proxy | +| 19 | `ProxyNotFound` | Proxy relationship not found | + +### Call Filter + +For security, contracts can only dispatch a limited set of runtime calls: + +**Whitelisted Calls:** +- `Proxy::proxy` - Execute proxy calls + +All other runtime calls are restricted and cannot be dispatched from contracts. + +### Configuration Parameters + +| Parameter | Value | Description | +|-----------|-------|-------------| +| Maximum code size | 128 KB | Maximum size of contract WASM code | +| Call stack depth | 5 frames | Maximum nested contract call depth | +| Runtime memory | 1 GB | Memory available during contract execution | +| Validator runtime memory | 2 GB | Memory available for validators | +| Transient storage | 1 MB | Maximum transient storage size | + + +## Additional Resources + +- [cargo-contract CLI Tool](https://github.com/paritytech/cargo-contract) +- [Contracts UI](https://contracts-ui.substrate.io/) diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index b5fa4bf83..9760ac1b5 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -93,6 +93,11 @@ pallet-commitments.workspace = true # for prod_or_fast! macro runtime-common.workspace = true + +# Wasm smart contracts support +pallet-contracts.workspace = true +subtensor-chain-extensions.workspace = true + # NPoS frame-election-provider-support = { workspace = true } pallet-authority-discovery = { workspace = true } @@ -263,6 +268,8 @@ std = [ "pallet-subtensor-swap/std", "pallet-subtensor-swap-runtime-api/std", "subtensor-swap-interface/std", + "pallet-contracts/std", + "subtensor-chain-extensions/std", "ethereum/std", ] runtime-benchmarks = [ @@ -296,6 +303,7 @@ runtime-benchmarks = [ "pallet-nomination-pools/runtime-benchmarks", "pallet-offences/runtime-benchmarks", "sp-staking/runtime-benchmarks", + "pallet-contracts/runtime-benchmarks", # EVM + Frontier "pallet-ethereum/runtime-benchmarks", @@ -335,6 +343,7 @@ try-runtime = [ "pallet-babe/try-runtime", "pallet-session/try-runtime", "pallet-staking/try-runtime", + "pallet-contracts/try-runtime", "pallet-election-provider-multi-phase/try-runtime", "frame-election-provider-support/try-runtime", "pallet-authority-discovery/try-runtime", diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 389a01a98..3ca067414 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -25,7 +25,7 @@ use frame_support::{ pallet_prelude::Get, traits::{Contains, InsideBoth, LinearStoragePrice, fungible::HoldConsideration}, }; -use frame_system::{EnsureRoot, EnsureRootWithSuccess}; +use frame_system::{EnsureRoot, EnsureRootWithSuccess, EnsureSigned}; use pallet_commitments::{CanCommit, OnMetadataCommitment}; use pallet_grandpa::{AuthorityId as GrandpaId, fg_primitives}; use pallet_registry::CanRegisterIdentity; @@ -1450,6 +1450,89 @@ impl pallet_crowdloan::Config for Runtime { type MaxContributors = MaxContributors; } +fn contracts_schedule() -> pallet_contracts::Schedule { + pallet_contracts::Schedule { + limits: pallet_contracts::Limits { + runtime_memory: 1024 * 1024 * 1024, + validator_runtime_memory: 1024 * 1024 * 1024 * 2, + ..Default::default() + }, + ..Default::default() + } +} + +const CONTRACT_STORAGE_KEY_PERCENT: Balance = 15; +const CONTRACT_STORAGE_BYTE_PERCENT: Balance = 6; + +/// Contracts deposits charged at 15% of the existential deposit per key, 6% per byte. +pub const fn contract_deposit(items: u32, bytes: u32) -> Balance { + let key_fee = + (EXISTENTIAL_DEPOSIT as Balance).saturating_mul(CONTRACT_STORAGE_KEY_PERCENT) / 100; + let byte_fee = + (EXISTENTIAL_DEPOSIT as Balance).saturating_mul(CONTRACT_STORAGE_BYTE_PERCENT) / 100; + + (items as Balance) + .saturating_mul(key_fee) + .saturating_add((bytes as Balance).saturating_mul(byte_fee)) +} + +parameter_types! { + pub const ContractDepositPerItem: Balance = contract_deposit(1, 0); + pub const ContractDepositPerByte: Balance = contract_deposit(0, 1); + pub const ContractDefaultDepositLimit: Balance = contract_deposit(1024, 1024 * 1024); + pub ContractsSchedule: pallet_contracts::Schedule = contracts_schedule::(); + pub const CodeHashLockupDepositPercent: Perbill = Perbill::from_percent(30); + pub const ContractMaxDelegateDependencies: u32 = 32; +} + +pub struct ContractCallFilter; + +/// Whitelist dispatchables that are allowed to be called from contracts +impl Contains for ContractCallFilter { + fn contains(call: &RuntimeCall) -> bool { + match call { + RuntimeCall::Proxy(inner) => matches!(inner, pallet_proxy::Call::proxy { .. }), + _ => false, + } + } +} + +impl pallet_contracts::Config for Runtime { + type Time = Timestamp; + type Randomness = RandomnessCollectiveFlip; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type CallFilter = ContractCallFilter; + type DepositPerItem = ContractDepositPerItem; + type DepositPerByte = ContractDepositPerByte; + type DefaultDepositLimit = ContractDefaultDepositLimit; + type CallStack = [pallet_contracts::Frame; 5]; + type WeightPrice = pallet_transaction_payment::Pallet; + type WeightInfo = pallet_contracts::weights::SubstrateWeight; + type ChainExtension = subtensor_chain_extensions::SubtensorChainExtension; + type Schedule = ContractsSchedule; + type AddressGenerator = pallet_contracts::DefaultAddressGenerator; + type MaxCodeLen = ConstU32<{ 128 * 1024 }>; + type MaxStorageKeyLen = ConstU32<128>; + type UnsafeUnstableInterface = ConstBool; + type MaxDebugBufferLen = ConstU32<{ 2 * 1024 * 1024 }>; + type RuntimeHoldReason = RuntimeHoldReason; + #[cfg(not(feature = "runtime-benchmarks"))] + type Migrations = (); + #[cfg(feature = "runtime-benchmarks")] + type Migrations = pallet_contracts::migration::codegen::BenchMigrations; + type MaxDelegateDependencies = ContractMaxDelegateDependencies; + type CodeHashLockupDepositPercent = CodeHashLockupDepositPercent; + type Debug = (); + type Environment = (); + type Xcm = (); + type MaxTransientStorageSize = ConstU32<{ 1024 * 1024 }>; + type UploadOrigin = EnsureSigned; + type InstantiateOrigin = EnsureSigned; + type ApiVersion = (); +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub struct Runtime @@ -1486,6 +1569,7 @@ construct_runtime!( Drand: pallet_drand = 26, Crowdloan: pallet_crowdloan = 27, Swap: pallet_subtensor_swap = 28, + Contracts: pallet_contracts = 29, } ); @@ -1609,6 +1693,8 @@ fn generate_genesis_json() -> Vec { json_str.as_bytes().to_vec() } +type EventRecord = frame_system::EventRecord; + impl_runtime_apis! { impl sp_api::Core for Runtime { fn version() -> RuntimeVersion { @@ -2072,6 +2158,77 @@ impl_runtime_apis! { } } + impl pallet_contracts::ContractsApi + for Runtime + { + fn call( + origin: AccountId, + dest: AccountId, + value: Balance, + gas_limit: Option, + storage_deposit_limit: Option, + input_data: Vec, + ) -> pallet_contracts::ContractExecResult { + let gas_limit = gas_limit.unwrap_or(BlockWeights::get().max_block); + Contracts::bare_call( + origin, + dest, + value, + gas_limit, + storage_deposit_limit, + input_data, + pallet_contracts::DebugInfo::Skip, + pallet_contracts::CollectEvents::Skip, + pallet_contracts::Determinism::Enforced, + ) + } + + fn instantiate( + origin: AccountId, + value: Balance, + gas_limit: Option, + storage_deposit_limit: Option, + code: pallet_contracts::Code, + data: Vec, + salt: Vec, + ) -> pallet_contracts::ContractInstantiateResult + { + let gas_limit = gas_limit.unwrap_or(BlockWeights::get().max_block); + Contracts::bare_instantiate( + origin, + value, + gas_limit, + storage_deposit_limit, + code, + data, + salt, + pallet_contracts::DebugInfo::Skip, + pallet_contracts::CollectEvents::Skip, + ) + } + + fn upload_code( + origin: AccountId, + code: Vec, + storage_deposit_limit: Option, + determinism: pallet_contracts::Determinism, + ) -> pallet_contracts::CodeUploadResult { + Contracts::bare_upload_code( + origin, + code, + storage_deposit_limit, + determinism, + ) + } + + fn get_storage( + address: AccountId, + key: Vec, + ) -> pallet_contracts::GetStorageResult { + Contracts::get_storage(address, key) + } + } + #[cfg(feature = "runtime-benchmarks")] impl frame_benchmarking::Benchmark for Runtime { fn benchmark_metadata(extra: bool) -> (