From 19f09f1e4267a2201355621bfdfa23777925e16d Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Wed, 27 Aug 2025 20:27:53 +0200 Subject: [PATCH 01/47] Add pallet-contracts dependency --- Cargo.lock | 54 ++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + runtime/Cargo.toml | 7 ++++++ 3 files changed, 62 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index b2048237d..4c1bf9e18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6894,6 +6894,7 @@ dependencies = [ "pallet-base-fee", "pallet-collective", "pallet-commitments", + "pallet-contracts", "pallet-crowdloan", "pallet-drand", "pallet-election-provider-multi-phase", @@ -7549,6 +7550,58 @@ dependencies = [ "w3f-bls", ] +[[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", + "pallet-contracts-uapi", + "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", +] + +[[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.104", +] + +[[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-crowdloan" version = "0.1.0" @@ -13464,6 +13517,7 @@ checksum = "1c6a0d765f5807e98a091107bae0a56ea3799f66a5de47b2c84c94a39c09974e" dependencies = [ "cfg-if", "hashbrown 0.14.5", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3415b8624..070e03b5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,6 +142,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/runtime/Cargo.toml b/runtime/Cargo.toml index a23b8cb5b..2e217e423 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -97,6 +97,10 @@ pallet-commitments.workspace = true # for prod_or_fast! macro runtime-common.workspace = true + +# Smart contracts support +pallet-contracts.workspace = true + # NPoS frame-election-provider-support = { workspace = true } pallet-authority-discovery = { workspace = true } @@ -267,6 +271,7 @@ std = [ "pallet-subtensor-swap/std", "pallet-subtensor-swap-runtime-api/std", "subtensor-swap-interface/std", + "pallet-contracts/std", ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", @@ -301,6 +306,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", @@ -342,6 +348,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", From 7d65d8ff6ba0378526d67f5a4f22532b9924b337 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Wed, 27 Aug 2025 20:29:23 +0200 Subject: [PATCH 02/47] Integrate contracts pallet into runtime --- runtime/src/lib.rs | 134 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 2 deletions(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index d49d5147e..0690d990b 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -22,9 +22,9 @@ use frame_support::{ dispatch::{DispatchResult, DispatchResultWithPostInfo}, genesis_builder_helper::{build_state, get_preset}, pallet_prelude::Get, - traits::{Contains, InsideBoth, LinearStoragePrice, fungible::HoldConsideration}, + traits::{Contains, InsideBoth, LinearStoragePrice, Nothing, fungible::HoldConsideration}, }; -use frame_system::{EnsureNever, EnsureRoot, EnsureRootWithSuccess, RawOrigin}; +use frame_system::{EnsureNever, EnsureRoot, EnsureRootWithSuccess, EnsureSigned, RawOrigin}; use pallet_commitments::{CanCommit, OnMetadataCommitment}; use pallet_grandpa::{AuthorityId as GrandpaId, fg_primitives}; use pallet_registry::CanRegisterIdentity; @@ -1590,6 +1590,62 @@ impl pallet_crowdloan::Config for Runtime { type MaxContributors = MaxContributors; } +// Contracts pallet configuration +parameter_types! { + pub const ContractDepositPerItem: Balance = deposit(1, 0); + pub const ContractDepositPerByte: Balance = deposit(0, 1); + pub const ContractDefaultDepositLimit: Balance = deposit(1024, 1024 * 1024); + pub ContractSchedule: pallet_contracts::Schedule = { + let mut schedule = pallet_contracts::Schedule::::default(); + schedule.limits.validator_runtime_memory = 1024 * 1024 * 1024; // 1 GB + schedule + }; + pub const CodeHashLockupDepositPercent: Perbill = Perbill::from_percent(10); + pub const ContractMaxDelegateDependencies: u32 = 32; +} + +impl pallet_contracts::Config for Runtime { + type Time = Timestamp; + type Randomness = RandomnessCollectiveFlip; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + /// The safest default is to allow no calls at all. + /// + /// Runtimes should whitelist dispatchables that are allowed to be called from contracts + /// and make sure they are stable. Dispatchables exposed to contracts are not allowed to + /// change because that would break already deployed contracts. The `Call` structure itself + /// is not allowed to change the indices of existing pallets, too. + type CallFilter = Nothing; + 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 = (); + type Schedule = ContractSchedule; + type AddressGenerator = pallet_contracts::DefaultAddressGenerator; + type MaxCodeLen = ConstU32<{ 123 * 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 @@ -1626,6 +1682,7 @@ construct_runtime!( Drand: pallet_drand = 26, Crowdloan: pallet_crowdloan = 27, Swap: pallet_subtensor_swap = 28, + Contracts: pallet_contracts = 29, } ); @@ -1739,6 +1796,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 { @@ -2198,6 +2257,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) -> ( From d241271caacc9ff227c64a06b3b83f548cabe95a Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Wed, 27 Aug 2025 20:29:48 +0200 Subject: [PATCH 03/47] Add contracts genesis config to localnet --- node/src/chain_spec/localnet.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/node/src/chain_spec/localnet.rs b/node/src/chain_spec/localnet.rs index 3f0743199..e2da0a7aa 100644 --- a/node/src/chain_spec/localnet.rs +++ b/node/src/chain_spec/localnet.rs @@ -134,5 +134,6 @@ fn localnet_genesis( "evmChainId": { "chainId": 42, }, + "contracts": {}, }) } From 27c2510fec2a9d0e6a806e31c6fa54e9a0386fff Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 28 Aug 2025 12:29:23 +0200 Subject: [PATCH 04/47] fix(chain-spec): remove empty contracts from localnet config --- node/src/chain_spec/localnet.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/node/src/chain_spec/localnet.rs b/node/src/chain_spec/localnet.rs index e2da0a7aa..3f0743199 100644 --- a/node/src/chain_spec/localnet.rs +++ b/node/src/chain_spec/localnet.rs @@ -134,6 +134,5 @@ fn localnet_genesis( "evmChainId": { "chainId": 42, }, - "contracts": {}, }) } From ac2089ad56d7ddcc7e0c3c40ec343ccef70113d8 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Tue, 2 Sep 2025 11:48:51 +0200 Subject: [PATCH 05/47] Configure contracts pallet with call filter and custom schedule --- runtime/src/lib.rs | 45 ++++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 0690d990b..5d6cbc01c 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -22,7 +22,7 @@ use frame_support::{ dispatch::{DispatchResult, DispatchResultWithPostInfo}, genesis_builder_helper::{build_state, get_preset}, pallet_prelude::Get, - traits::{Contains, InsideBoth, LinearStoragePrice, Nothing, fungible::HoldConsideration}, + traits::{Contains, InsideBoth, LinearStoragePrice, fungible::HoldConsideration}, }; use frame_system::{EnsureNever, EnsureRoot, EnsureRootWithSuccess, EnsureSigned, RawOrigin}; use pallet_commitments::{CanCommit, OnMetadataCommitment}; @@ -1591,32 +1591,47 @@ impl pallet_crowdloan::Config for Runtime { } // Contracts pallet configuration +fn contract_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() + } +} + parameter_types! { pub const ContractDepositPerItem: Balance = deposit(1, 0); pub const ContractDepositPerByte: Balance = deposit(0, 1); pub const ContractDefaultDepositLimit: Balance = deposit(1024, 1024 * 1024); - pub ContractSchedule: pallet_contracts::Schedule = { - let mut schedule = pallet_contracts::Schedule::::default(); - schedule.limits.validator_runtime_memory = 1024 * 1024 * 1024; // 1 GB - schedule - }; - pub const CodeHashLockupDepositPercent: Perbill = Perbill::from_percent(10); + pub ContractSchedule: pallet_contracts::Schedule = contract_schedule::(); + pub const CodeHashLockupDepositPercent: Perbill = Perbill::from_percent(0); 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 { + matches!( + call, + RuntimeCall::SubtensorModule(pallet_subtensor::Call::move_stake { .. }) + | RuntimeCall::SubtensorModule(pallet_subtensor::Call::transfer_stake { .. }) + | RuntimeCall::Proxy(pallet_proxy::Call::proxy { .. }) + ) + } +} + impl pallet_contracts::Config for Runtime { type Time = Timestamp; type Randomness = RandomnessCollectiveFlip; type Currency = Balances; type RuntimeEvent = RuntimeEvent; type RuntimeCall = RuntimeCall; - /// The safest default is to allow no calls at all. - /// - /// Runtimes should whitelist dispatchables that are allowed to be called from contracts - /// and make sure they are stable. Dispatchables exposed to contracts are not allowed to - /// change because that would break already deployed contracts. The `Call` structure itself - /// is not allowed to change the indices of existing pallets, too. - type CallFilter = Nothing; + type CallFilter = ContractCallFilter; type DepositPerItem = ContractDepositPerItem; type DepositPerByte = ContractDepositPerByte; type DefaultDepositLimit = ContractDefaultDepositLimit; @@ -1626,7 +1641,7 @@ impl pallet_contracts::Config for Runtime { type ChainExtension = (); type Schedule = ContractSchedule; type AddressGenerator = pallet_contracts::DefaultAddressGenerator; - type MaxCodeLen = ConstU32<{ 123 * 1024 }>; + type MaxCodeLen = ConstU32<{ 128 * 1024 }>; type MaxStorageKeyLen = ConstU32<128>; type UnsafeUnstableInterface = ConstBool; type MaxDebugBufferLen = ConstU32<{ 2 * 1024 * 1024 }>; From 594c9300e374b0bdd8e7179b3f600fef8bf07e8e Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 11 Sep 2025 17:18:23 +0200 Subject: [PATCH 06/47] feat(runtime): add Subtensor chain extension for contracts --- runtime/src/chain_extension.rs | 52 ++++++++++++++++++++++++++++++++++ runtime/src/lib.rs | 3 +- 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 runtime/src/chain_extension.rs diff --git a/runtime/src/chain_extension.rs b/runtime/src/chain_extension.rs new file mode 100644 index 000000000..801bc83fa --- /dev/null +++ b/runtime/src/chain_extension.rs @@ -0,0 +1,52 @@ +use codec::Encode; +use pallet_contracts::chain_extension::{ + ChainExtension, Environment, Ext, InitState, RetVal, SysConfig, +}; +use sp_runtime::{AccountId32, DispatchError}; +use subtensor_runtime_common::NetUid; + +use crate::{Runtime, SubtensorModule}; + +#[derive(Default)] +pub struct SubtensorChainExtension; + +impl ChainExtension for SubtensorChainExtension { + fn call(&mut self, env: Environment) -> Result + where + E::T: SysConfig, + { + let func_id = env.func_id(); + + match func_id { + // Function ID 1001: get_stake_info_for_hotkey_coldkey_netuid + 1001 => { + let mut env = env.buf_in_buf_out(); + + let input: (AccountId32, AccountId32, NetUid) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + + let (hotkey, coldkey, netuid) = input; + + let stake_info = SubtensorModule::get_stake_info_for_hotkey_coldkey_netuid( + hotkey, coldkey, netuid, + ); + + let encoded_result = stake_info.encode(); + + env.write(&encoded_result, false, None) + .map_err(|_| DispatchError::Other("Failed to write output"))?; + + Ok(RetVal::Converging(0)) + } + _ => { + log::error!("Called an unregistered chain extension function: {func_id}",); + Err(DispatchError::Other("Unimplemented function ID")) + } + } + } + + fn enabled() -> bool { + true + } +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 5d6cbc01c..20381f91c 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -10,6 +10,7 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); use core::num::NonZeroU64; +pub mod chain_extension; pub mod check_nonce; mod migrations; pub mod transaction_payment_wrapper; @@ -1638,7 +1639,7 @@ impl pallet_contracts::Config for Runtime { type CallStack = [pallet_contracts::Frame; 5]; type WeightPrice = pallet_transaction_payment::Pallet; type WeightInfo = pallet_contracts::weights::SubstrateWeight; - type ChainExtension = (); + type ChainExtension = chain_extension::SubtensorChainExtension; type Schedule = ContractSchedule; type AddressGenerator = pallet_contracts::DefaultAddressGenerator; type MaxCodeLen = ConstU32<{ 128 * 1024 }>; From f13e1a00f78e2fba8317c34209db878b59d22566 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Mon, 15 Sep 2025 11:39:47 +0200 Subject: [PATCH 07/47] docs(runtime): clarify comment for smart contracts support --- runtime/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 2e217e423..9a0f2cf15 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -98,7 +98,7 @@ pallet-commitments.workspace = true # for prod_or_fast! macro runtime-common.workspace = true -# Smart contracts support +# Wasm smart contracts support pallet-contracts.workspace = true # NPoS From ceca15ebe925f0fe390508980828fe9875122ae4 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Mon, 15 Sep 2025 11:40:11 +0200 Subject: [PATCH 08/47] refactor(runtime): rename ContractSchedule to ContractsSchedule --- runtime/src/lib.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 20381f91c..d5d916ca0 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1591,8 +1591,7 @@ impl pallet_crowdloan::Config for Runtime { type MaxContributors = MaxContributors; } -// Contracts pallet configuration -fn contract_schedule() -> pallet_contracts::Schedule { +fn contracts_schedule() -> pallet_contracts::Schedule { pallet_contracts::Schedule { limits: pallet_contracts::Limits { runtime_memory: 1024 * 1024 * 1024, @@ -1607,7 +1606,7 @@ parameter_types! { pub const ContractDepositPerItem: Balance = deposit(1, 0); pub const ContractDepositPerByte: Balance = deposit(0, 1); pub const ContractDefaultDepositLimit: Balance = deposit(1024, 1024 * 1024); - pub ContractSchedule: pallet_contracts::Schedule = contract_schedule::(); + pub ContractsSchedule: pallet_contracts::Schedule = contracts_schedule::(); pub const CodeHashLockupDepositPercent: Perbill = Perbill::from_percent(0); pub const ContractMaxDelegateDependencies: u32 = 32; } @@ -1640,7 +1639,7 @@ impl pallet_contracts::Config for Runtime { type WeightPrice = pallet_transaction_payment::Pallet; type WeightInfo = pallet_contracts::weights::SubstrateWeight; type ChainExtension = chain_extension::SubtensorChainExtension; - type Schedule = ContractSchedule; + type Schedule = ContractsSchedule; type AddressGenerator = pallet_contracts::DefaultAddressGenerator; type MaxCodeLen = ConstU32<{ 128 * 1024 }>; type MaxStorageKeyLen = ConstU32<128>; From 587ee48cfe14deb11beee45ef529652a0560696a Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Mon, 15 Sep 2025 12:36:17 +0200 Subject: [PATCH 09/47] docs: add smart contracts documentation --- README.md | 5 ++-- docs/contracts.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 docs/contracts.md diff --git a/README.md b/README.md index 5c3faaf03..957d157ff 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,9 @@ This repository contains Bittensor's substrate-chain. Subtensor contains the trusted logic which: 1. Runs Bittensor's [consensus mechanism](./docs/consensus.md); -2. Advertises neuron information, IPs, etc., and -3. Facilitates value transfer via TAO. +2. Advertises neuron information, IPs, etc.; +3. Facilitates value transfer via TAO; and +4. Supports wasm smart contract functionality via `pallet-contracts` (see [contracts documentation](./docs/contracts.md)). ## System Requirements diff --git a/docs/contracts.md b/docs/contracts.md new file mode 100644 index 000000000..af3eacdf6 --- /dev/null +++ b/docs/contracts.md @@ -0,0 +1,66 @@ +# 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 | +|------------|------|-------------|------------|---------| +| 1001 | `get_stake_info_for_hotkey_coldkey_netuid` | Query stake information | `(AccountId32, AccountId32, NetUid)` | Stake information | + +Example usage in your ink! contract: +```rust +#[ink::chain_extension(extension = 0)] +pub trait SubtensorExtension { + type ErrorCode = SubtensorError; + + #[ink(function = 1001)] + fn get_stake_info( + hotkey: AccountId, + coldkey: AccountId, + netuid: u16, + ) -> Result, SubtensorError>; +} +``` + +### Call Filter + +For security, contracts can only dispatch a limited set of runtime calls: + +**Whitelisted Calls:** +- `SubtensorModule::move_stake` - Move stake between hotkeys +- `SubtensorModule::transfer_stake` - Transfer stake between accounts +- `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/) From 1b6aec5fb945a3041413df6f7e2ca25366050471 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Mon, 15 Sep 2025 12:42:42 +0200 Subject: [PATCH 10/47] fix docs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 957d157ff..2c74b9e8f 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ This repository contains Bittensor's substrate-chain. Subtensor contains the trusted logic which: 1. Runs Bittensor's [consensus mechanism](./docs/consensus.md); -2. Advertises neuron information, IPs, etc.; -3. Facilitates value transfer via TAO; and +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 From c8ca999d9b9a3d6ac0f9a0e0caae5ff734024e30 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 18 Sep 2025 08:52:22 +0200 Subject: [PATCH 11/47] feat(runtime): update code hash lockup deposit percent to 30 --- runtime/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index d5d916ca0..30de93dc9 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1607,7 +1607,7 @@ parameter_types! { pub const ContractDepositPerByte: Balance = deposit(0, 1); pub const ContractDefaultDepositLimit: Balance = deposit(1024, 1024 * 1024); pub ContractsSchedule: pallet_contracts::Schedule = contracts_schedule::(); - pub const CodeHashLockupDepositPercent: Perbill = Perbill::from_percent(0); + pub const CodeHashLockupDepositPercent: Perbill = Perbill::from_percent(30); pub const ContractMaxDelegateDependencies: u32 = 32; } From af8692872dfa50247ca7d8b77e5c1bf0f36d583b Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 18 Sep 2025 10:18:44 +0200 Subject: [PATCH 12/47] feat(runtime): customize contract storage deposit calculation Implement custom deposit calculation for contract storage with 15% of existential deposit per key and 6% per byte --- runtime/src/lib.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 30de93dc9..a631af288 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1602,10 +1602,25 @@ fn contracts_schedule() -> pallet_contracts::Schedu } } +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 = deposit(1, 0); - pub const ContractDepositPerByte: Balance = deposit(0, 1); - pub const ContractDefaultDepositLimit: Balance = deposit(1024, 1024 * 1024); + 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; From c569dac116e33f338bf09a83e346f6c0a0958cf9 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 18 Sep 2025 11:51:27 +0200 Subject: [PATCH 13/47] feat(runtime): expand contract call whitelist for staking operations --- runtime/src/lib.rs | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index a631af288..d5604e485 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1631,12 +1631,30 @@ pub struct ContractCallFilter; /// Whitelist dispatchables that are allowed to be called from contracts impl Contains for ContractCallFilter { fn contains(call: &RuntimeCall) -> bool { - matches!( - call, - RuntimeCall::SubtensorModule(pallet_subtensor::Call::move_stake { .. }) - | RuntimeCall::SubtensorModule(pallet_subtensor::Call::transfer_stake { .. }) - | RuntimeCall::Proxy(pallet_proxy::Call::proxy { .. }) - ) + match call { + RuntimeCall::SubtensorModule(inner) => matches!( + inner, + pallet_subtensor::Call::add_stake { .. } + | pallet_subtensor::Call::remove_stake { .. } + | pallet_subtensor::Call::unstake_all { .. } + | pallet_subtensor::Call::unstake_all_alpha { .. } + | pallet_subtensor::Call::move_stake { .. } + | pallet_subtensor::Call::transfer_stake { .. } + | pallet_subtensor::Call::swap_stake { .. } + | pallet_subtensor::Call::add_stake_limit { .. } + | pallet_subtensor::Call::remove_stake_limit { .. } + | pallet_subtensor::Call::swap_stake_limit { .. } + | pallet_subtensor::Call::remove_stake_full_limit { .. } + | pallet_subtensor::Call::set_coldkey_auto_stake_hotkey { .. } + ), + RuntimeCall::Proxy(inner) => matches!( + inner, + pallet_proxy::Call::proxy { .. } + | pallet_proxy::Call::add_proxy { .. } + | pallet_proxy::Call::create_pure { .. } + ), + _ => false, + } } } From ef563d356249b89bc2c1e6ad53d0850d1e5cd749 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 18 Sep 2025 11:51:40 +0200 Subject: [PATCH 14/47] docs: document expanded contract call whitelist --- docs/contracts.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/contracts.md b/docs/contracts.md index af3eacdf6..61f01a6c9 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -43,9 +43,21 @@ pub trait SubtensorExtension { For security, contracts can only dispatch a limited set of runtime calls: **Whitelisted Calls:** +- `SubtensorModule::add_stake` - Delegate stake from a coldkey to a hotkey +- `SubtensorModule::remove_stake` - Withdraw stake from a hotkey back to the caller +- `SubtensorModule::unstake_all` - Unstake all funds associated with a hotkey +- `SubtensorModule::unstake_all_alpha` - Unstake all alpha stake from a hotkey - `SubtensorModule::move_stake` - Move stake between hotkeys -- `SubtensorModule::transfer_stake` - Transfer stake between accounts +- `SubtensorModule::transfer_stake` - Transfer stake between coldkeys (optionally across subnets) +- `SubtensorModule::swap_stake` - Swap stake allocations between subnets +- `SubtensorModule::add_stake_limit` - Delegate stake with a price limit +- `SubtensorModule::remove_stake_limit` - Withdraw staked funds with a price limit +- `SubtensorModule::swap_stake_limit` - Swap stake between subnets with a price limit +- `SubtensorModule::remove_stake_full_limit` - Fully withdraw stake subject to a price limit +- `SubtensorModule::set_coldkey_auto_stake_hotkey` - Configure the automatic stake destination for a coldkey - `Proxy::proxy` - Execute proxy calls +- `Proxy::add_proxy` - Add a proxy relationship +- `Proxy::create_pure` - Create a pure proxy account All other runtime calls are restricted and cannot be dispatched from contracts. From 12788fcd917eaf9921cb2854adce66991a4e522f Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Wed, 24 Sep 2025 10:24:59 +0200 Subject: [PATCH 15/47] fix(runtime): remove create_pure from proxy call filter --- runtime/src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index d5604e485..9a7ed01d9 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1649,9 +1649,7 @@ impl Contains for ContractCallFilter { ), RuntimeCall::Proxy(inner) => matches!( inner, - pallet_proxy::Call::proxy { .. } - | pallet_proxy::Call::add_proxy { .. } - | pallet_proxy::Call::create_pure { .. } + pallet_proxy::Call::proxy { .. } | pallet_proxy::Call::add_proxy { .. } ), _ => false, } From 33b0083d2db868653ccbf70fd965c6eea6985af3 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Mon, 13 Oct 2025 17:28:16 +0200 Subject: [PATCH 16/47] Add DoNotBreakSmartContracts trait for contract API stability Mirror all ContractCallFilter dispatchables with pattern matching to detect breaking changes at compile time. --- runtime/src/lib.rs | 218 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 4bf10091c..0e4a003a7 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1691,6 +1691,224 @@ impl Contains for ContractCallFilter { } } +/// Trait to prevent breaking changes to smart contract APIs. +/// This trait mirrors all dispatchables exposed through ContractCallFilter. +/// If any function signature changes, compilation will fail, alerting developers +/// to potential breaking changes for smart contracts. +pub trait DoNotBreakSmartContracts { + fn add_stake(hotkey: AccountId32, netuid: NetUid, amount_staked: TaoCurrency) { + let _ = pallet_subtensor::Pallet::::add_stake( + RuntimeOrigin::none(), + hotkey, + netuid, + amount_staked, + ); + } + + fn remove_stake(hotkey: AccountId32, netuid: NetUid, amount_unstaked: AlphaCurrency) { + let _ = pallet_subtensor::Pallet::::remove_stake( + RuntimeOrigin::none(), + hotkey, + netuid, + amount_unstaked, + ); + } + + fn unstake_all(hotkey: AccountId32) { + let _ = pallet_subtensor::Pallet::::unstake_all(RuntimeOrigin::none(), hotkey); + } + + fn unstake_all_alpha(hotkey: AccountId32) { + let _ = + pallet_subtensor::Pallet::::unstake_all_alpha(RuntimeOrigin::none(), hotkey); + } + + fn move_stake( + origin_hotkey: AccountId32, + destination_hotkey: AccountId32, + origin_netuid: NetUid, + destination_netuid: NetUid, + alpha_amount: AlphaCurrency, + ) { + let _ = pallet_subtensor::Pallet::::move_stake( + RuntimeOrigin::none(), + origin_hotkey, + destination_hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + ); + } + + fn transfer_stake( + destination_coldkey: AccountId32, + hotkey: AccountId32, + origin_netuid: NetUid, + destination_netuid: NetUid, + alpha_amount: AlphaCurrency, + ) { + let _ = pallet_subtensor::Pallet::::transfer_stake( + RuntimeOrigin::none(), + destination_coldkey, + hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + ); + } + + fn swap_stake( + hotkey: AccountId32, + origin_netuid: NetUid, + destination_netuid: NetUid, + alpha_amount: AlphaCurrency, + ) { + let _ = pallet_subtensor::Pallet::::swap_stake( + RuntimeOrigin::none(), + hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + ); + } + + fn add_stake_limit( + hotkey: AccountId32, + netuid: NetUid, + amount_staked: TaoCurrency, + limit_price: TaoCurrency, + allow_partial: bool, + ) { + let _ = pallet_subtensor::Pallet::::add_stake_limit( + RuntimeOrigin::none(), + hotkey, + netuid, + amount_staked, + limit_price, + allow_partial, + ); + } + + fn remove_stake_limit( + hotkey: AccountId32, + netuid: NetUid, + amount_unstaked: AlphaCurrency, + limit_price: TaoCurrency, + allow_partial: bool, + ) { + let _ = pallet_subtensor::Pallet::::remove_stake_limit( + RuntimeOrigin::none(), + hotkey, + netuid, + amount_unstaked, + limit_price, + allow_partial, + ); + } + + fn swap_stake_limit( + hotkey: AccountId32, + origin_netuid: NetUid, + destination_netuid: NetUid, + alpha_amount: AlphaCurrency, + limit_price: TaoCurrency, + allow_partial: bool, + ) { + let _ = pallet_subtensor::Pallet::::swap_stake_limit( + RuntimeOrigin::none(), + hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + limit_price, + allow_partial, + ); + } + + fn remove_stake_full_limit( + hotkey: AccountId32, + netuid: NetUid, + limit_price: Option, + ) { + let _ = pallet_subtensor::Pallet::::remove_stake_full_limit( + RuntimeOrigin::none(), + hotkey, + netuid, + limit_price, + ); + } + + fn set_coldkey_auto_stake_hotkey(netuid: NetUid, hotkey: AccountId32) { + let _ = pallet_subtensor::Pallet::::set_coldkey_auto_stake_hotkey( + RuntimeOrigin::none(), + netuid, + hotkey, + ); + } + + fn proxy(real: AccountId32, force_proxy_type: Option, call: Box) { + if let Some(proxy_type) = force_proxy_type { + match proxy_type { + ProxyType::Any => {} + ProxyType::Owner => {} + ProxyType::NonCritical => {} + ProxyType::NonTransfer => {} + ProxyType::Senate => {} + ProxyType::NonFungibile => {} + ProxyType::Triumvirate => {} + ProxyType::Governance => {} + ProxyType::Staking => {} + ProxyType::Registration => {} + ProxyType::Transfer => {} + ProxyType::SmallTransfer => {} + ProxyType::RootWeights => {} + ProxyType::ChildKeys => {} + ProxyType::SudoUncheckedSetCode => {} + ProxyType::SwapHotkey => {} + ProxyType::SubnetLeaseBeneficiary => {} + } + }; + + let real_lookup = sp_runtime::MultiAddress::Id(real); + let _ = pallet_proxy::Pallet::::proxy( + RuntimeOrigin::none(), + real_lookup, + force_proxy_type, + call, + ); + } + + fn add_proxy(delegate: AccountId32, proxy_type: ProxyType, delay: BlockNumber) { + match proxy_type { + ProxyType::Any => {} + ProxyType::Owner => {} + ProxyType::NonCritical => {} + ProxyType::NonTransfer => {} + ProxyType::Senate => {} + ProxyType::NonFungibile => {} + ProxyType::Triumvirate => {} + ProxyType::Governance => {} + ProxyType::Staking => {} + ProxyType::Registration => {} + ProxyType::Transfer => {} + ProxyType::SmallTransfer => {} + ProxyType::RootWeights => {} + ProxyType::ChildKeys => {} + ProxyType::SudoUncheckedSetCode => {} + ProxyType::SwapHotkey => {} + ProxyType::SubnetLeaseBeneficiary => {} + } + + let delegate_lookup = sp_runtime::MultiAddress::Id(delegate); + let _ = pallet_proxy::Pallet::::add_proxy( + RuntimeOrigin::none(), + delegate_lookup, + proxy_type, + delay, + ); + } +} + impl pallet_contracts::Config for Runtime { type Time = Timestamp; type Randomness = RandomnessCollectiveFlip; From 2610ac19ce6337763728f41e5d78b524e034053c Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Mon, 13 Oct 2025 17:49:05 +0200 Subject: [PATCH 17/47] docs(contracts): remove Proxy::create_pure from allowed calls --- docs/contracts.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/contracts.md b/docs/contracts.md index 61f01a6c9..068b9d566 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -57,7 +57,6 @@ For security, contracts can only dispatch a limited set of runtime calls: - `SubtensorModule::set_coldkey_auto_stake_hotkey` - Configure the automatic stake destination for a coldkey - `Proxy::proxy` - Execute proxy calls - `Proxy::add_proxy` - Add a proxy relationship -- `Proxy::create_pure` - Create a pure proxy account All other runtime calls are restricted and cannot be dispatched from contracts. From a49015947c8af4cd0419120ad62829bc484f1feb Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Mon, 13 Oct 2025 17:57:32 +0200 Subject: [PATCH 18/47] refactor(runtime): rename DoNotBreakSmartContracts trait add version suffix to signal a versioned interface --- runtime/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 11c1349c6..fcb9ac626 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1695,7 +1695,7 @@ impl Contains for ContractCallFilter { /// This trait mirrors all dispatchables exposed through ContractCallFilter. /// If any function signature changes, compilation will fail, alerting developers /// to potential breaking changes for smart contracts. -pub trait DoNotBreakSmartContracts { +pub trait DoNotBreakSmartContractsV1 { fn add_stake(hotkey: AccountId32, netuid: NetUid, amount_staked: TaoCurrency) { let _ = pallet_subtensor::Pallet::::add_stake( RuntimeOrigin::none(), From cf70929718ad29099c22f63e56638a86737f46b4 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Wed, 15 Oct 2025 17:56:53 +0200 Subject: [PATCH 19/47] refactor(runtime): remove DoNotBreakSmartContractsV1 trait --- runtime/src/lib.rs | 218 --------------------------------------------- 1 file changed, 218 deletions(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index fcb9ac626..ba21b9321 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1691,224 +1691,6 @@ impl Contains for ContractCallFilter { } } -/// Trait to prevent breaking changes to smart contract APIs. -/// This trait mirrors all dispatchables exposed through ContractCallFilter. -/// If any function signature changes, compilation will fail, alerting developers -/// to potential breaking changes for smart contracts. -pub trait DoNotBreakSmartContractsV1 { - fn add_stake(hotkey: AccountId32, netuid: NetUid, amount_staked: TaoCurrency) { - let _ = pallet_subtensor::Pallet::::add_stake( - RuntimeOrigin::none(), - hotkey, - netuid, - amount_staked, - ); - } - - fn remove_stake(hotkey: AccountId32, netuid: NetUid, amount_unstaked: AlphaCurrency) { - let _ = pallet_subtensor::Pallet::::remove_stake( - RuntimeOrigin::none(), - hotkey, - netuid, - amount_unstaked, - ); - } - - fn unstake_all(hotkey: AccountId32) { - let _ = pallet_subtensor::Pallet::::unstake_all(RuntimeOrigin::none(), hotkey); - } - - fn unstake_all_alpha(hotkey: AccountId32) { - let _ = - pallet_subtensor::Pallet::::unstake_all_alpha(RuntimeOrigin::none(), hotkey); - } - - fn move_stake( - origin_hotkey: AccountId32, - destination_hotkey: AccountId32, - origin_netuid: NetUid, - destination_netuid: NetUid, - alpha_amount: AlphaCurrency, - ) { - let _ = pallet_subtensor::Pallet::::move_stake( - RuntimeOrigin::none(), - origin_hotkey, - destination_hotkey, - origin_netuid, - destination_netuid, - alpha_amount, - ); - } - - fn transfer_stake( - destination_coldkey: AccountId32, - hotkey: AccountId32, - origin_netuid: NetUid, - destination_netuid: NetUid, - alpha_amount: AlphaCurrency, - ) { - let _ = pallet_subtensor::Pallet::::transfer_stake( - RuntimeOrigin::none(), - destination_coldkey, - hotkey, - origin_netuid, - destination_netuid, - alpha_amount, - ); - } - - fn swap_stake( - hotkey: AccountId32, - origin_netuid: NetUid, - destination_netuid: NetUid, - alpha_amount: AlphaCurrency, - ) { - let _ = pallet_subtensor::Pallet::::swap_stake( - RuntimeOrigin::none(), - hotkey, - origin_netuid, - destination_netuid, - alpha_amount, - ); - } - - fn add_stake_limit( - hotkey: AccountId32, - netuid: NetUid, - amount_staked: TaoCurrency, - limit_price: TaoCurrency, - allow_partial: bool, - ) { - let _ = pallet_subtensor::Pallet::::add_stake_limit( - RuntimeOrigin::none(), - hotkey, - netuid, - amount_staked, - limit_price, - allow_partial, - ); - } - - fn remove_stake_limit( - hotkey: AccountId32, - netuid: NetUid, - amount_unstaked: AlphaCurrency, - limit_price: TaoCurrency, - allow_partial: bool, - ) { - let _ = pallet_subtensor::Pallet::::remove_stake_limit( - RuntimeOrigin::none(), - hotkey, - netuid, - amount_unstaked, - limit_price, - allow_partial, - ); - } - - fn swap_stake_limit( - hotkey: AccountId32, - origin_netuid: NetUid, - destination_netuid: NetUid, - alpha_amount: AlphaCurrency, - limit_price: TaoCurrency, - allow_partial: bool, - ) { - let _ = pallet_subtensor::Pallet::::swap_stake_limit( - RuntimeOrigin::none(), - hotkey, - origin_netuid, - destination_netuid, - alpha_amount, - limit_price, - allow_partial, - ); - } - - fn remove_stake_full_limit( - hotkey: AccountId32, - netuid: NetUid, - limit_price: Option, - ) { - let _ = pallet_subtensor::Pallet::::remove_stake_full_limit( - RuntimeOrigin::none(), - hotkey, - netuid, - limit_price, - ); - } - - fn set_coldkey_auto_stake_hotkey(netuid: NetUid, hotkey: AccountId32) { - let _ = pallet_subtensor::Pallet::::set_coldkey_auto_stake_hotkey( - RuntimeOrigin::none(), - netuid, - hotkey, - ); - } - - fn proxy(real: AccountId32, force_proxy_type: Option, call: Box) { - if let Some(proxy_type) = force_proxy_type { - match proxy_type { - ProxyType::Any => {} - ProxyType::Owner => {} - ProxyType::NonCritical => {} - ProxyType::NonTransfer => {} - ProxyType::Senate => {} - ProxyType::NonFungibile => {} - ProxyType::Triumvirate => {} - ProxyType::Governance => {} - ProxyType::Staking => {} - ProxyType::Registration => {} - ProxyType::Transfer => {} - ProxyType::SmallTransfer => {} - ProxyType::RootWeights => {} - ProxyType::ChildKeys => {} - ProxyType::SudoUncheckedSetCode => {} - ProxyType::SwapHotkey => {} - ProxyType::SubnetLeaseBeneficiary => {} - } - }; - - let real_lookup = sp_runtime::MultiAddress::Id(real); - let _ = pallet_proxy::Pallet::::proxy( - RuntimeOrigin::none(), - real_lookup, - force_proxy_type, - call, - ); - } - - fn add_proxy(delegate: AccountId32, proxy_type: ProxyType, delay: BlockNumber) { - match proxy_type { - ProxyType::Any => {} - ProxyType::Owner => {} - ProxyType::NonCritical => {} - ProxyType::NonTransfer => {} - ProxyType::Senate => {} - ProxyType::NonFungibile => {} - ProxyType::Triumvirate => {} - ProxyType::Governance => {} - ProxyType::Staking => {} - ProxyType::Registration => {} - ProxyType::Transfer => {} - ProxyType::SmallTransfer => {} - ProxyType::RootWeights => {} - ProxyType::ChildKeys => {} - ProxyType::SudoUncheckedSetCode => {} - ProxyType::SwapHotkey => {} - ProxyType::SubnetLeaseBeneficiary => {} - } - - let delegate_lookup = sp_runtime::MultiAddress::Id(delegate); - let _ = pallet_proxy::Pallet::::add_proxy( - RuntimeOrigin::none(), - delegate_lookup, - proxy_type, - delay, - ); - } -} - impl pallet_contracts::Config for Runtime { type Time = Timestamp; type Randomness = RandomnessCollectiveFlip; From cf3009411c1e3774df95b776437432a9a1649166 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Wed, 15 Oct 2025 18:02:57 +0200 Subject: [PATCH 20/47] build: add chain-extensions workspace member --- Cargo.lock | 20 ++++++++++++++++++++ Cargo.toml | 3 +++ 2 files changed, 23 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 2d60bd481..a59c1404b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17954,6 +17954,26 @@ dependencies = [ "walkdir", ] +[[package]] +name = "subtensor-chain-extensions" +version = "0.1.0" +dependencies = [ + "frame-support", + "frame-system", + "log", + "num_enum", + "pallet-contracts 40.1.0", + "pallet-subtensor", + "pallet-subtensor-swap", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "subtensor-runtime-common", +] + [[package]] name = "subtensor-custom-rpc" version = "0.0.2" diff --git a/Cargo.toml b/Cargo.toml index eaae00515..9a5896664 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ members = [ "primitives/*", "runtime", "support/*", + "chain-extensions", ] resolver = "2" @@ -69,6 +70,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" @@ -116,6 +118,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 } From da84eedf8213fdd0bf353b12337db7172dfd80de Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Wed, 15 Oct 2025 18:03:17 +0200 Subject: [PATCH 21/47] feat(chain-extensions): add chain extensions crate --- chain-extensions/Cargo.toml | 48 +++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 chain-extensions/Cargo.toml diff --git a/chain-extensions/Cargo.toml b/chain-extensions/Cargo.toml new file mode 100644 index 000000000..f4dcd988c --- /dev/null +++ b/chain-extensions/Cargo.toml @@ -0,0 +1,48 @@ +[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 +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", +] From 7e7c364258a41eeac52f483a0c9b49bfe4c7da45 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Wed, 15 Oct 2025 18:03:46 +0200 Subject: [PATCH 22/47] feat(chain-extensions): add FunctionId and Outcome types Define chain extension function IDs for staking operations and outcome codes for error handling --- chain-extensions/src/types.rs | 84 +++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 chain-extensions/src/types.rs diff --git a/chain-extensions/src/types.rs b/chain-extensions/src/types.rs new file mode 100644 index 000000000..de98567e5 --- /dev/null +++ b/chain-extensions/src/types.rs @@ -0,0 +1,84 @@ +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, +} + +#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum Outcome { + /// 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, +} + +impl From for Outcome { + 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") => Outcome::NotEnoughBalanceToStake, + Some("NonAssociatedColdKey") => Outcome::NonAssociatedColdKey, + Some("BalanceWithdrawalError") => Outcome::BalanceWithdrawalError, + Some("HotKeyNotRegisteredInSubNet") => Outcome::NotRegistered, + Some("HotKeyAccountNotExists") => Outcome::NotRegistered, + Some("NotEnoughStakeToWithdraw") => Outcome::NotEnoughStakeToWithdraw, + Some("TxRateLimitExceeded") => Outcome::TxRateLimitExceeded, + Some("SlippageTooHigh") => Outcome::SlippageTooHigh, + Some("SubnetNotExists") => Outcome::SubnetNotExists, + Some("SameAutoStakeHotkeyAlreadySet") => Outcome::SameAutoStakeHotkeyAlreadySet, + Some("InsufficientBalance") => Outcome::InsufficientBalance, + Some("AmountTooLow") => Outcome::AmountTooLow, + Some("InsufficientLiquidity") => Outcome::InsufficientLiquidity, + Some("SameNetuid") => Outcome::SameNetuid, + _ => Outcome::RuntimeError, + } + } +} From bab0039eeafef7518c723f4a0485189f637adeac Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Wed, 15 Oct 2025 18:04:17 +0200 Subject: [PATCH 23/47] feat(chain-extensions): implement staking chain extensions Expose 12 subtensor pallet functions to smart contracts: - GetStakeInfoForHotkeyColdkeyNetuidV1 (query) - AddStakeV1, RemoveStakeV1 - UnstakeAllV1, UnstakeAllAlphaV1 - MoveStakeV1, TransferStakeV1, SwapStakeV1 - AddStakeLimitV1, RemoveStakeLimitV1, SwapStakeLimitV1 - RemoveStakeFullLimitV1, SetColdkeyAutoStakeHotkeyV1 --- chain-extensions/src/lib.rs | 409 ++++++++++++++++++++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 chain-extensions/src/lib.rs diff --git a/chain-extensions/src/lib.rs b/chain-extensions/src/lib.rs new file mode 100644 index 000000000..a8e09afea --- /dev/null +++ b/chain-extensions/src/lib.rs @@ -0,0 +1,409 @@ +mod types; + +use crate::types::{FunctionId, Outcome}; +use codec::Encode; +use frame_support::{DebugNoBound, traits::Get}; +use frame_system::RawOrigin; +use pallet_contracts::chain_extension::{ + ChainExtension, Environment, Ext, InitState, RetVal, SysConfig, +}; +use sp_runtime::{DispatchError, Weight, traits::StaticLookup}; +use sp_std::marker::PhantomData; +use subtensor_runtime_common::{AlphaCurrency, NetUid, TaoCurrency}; + +#[derive(DebugNoBound)] +pub struct SubtensorChainExtension(PhantomData); + +impl ChainExtension for SubtensorChainExtension +where + T: pallet_subtensor::Config + pallet_contracts::Config, + <::Lookup as StaticLookup>::Source: From<::AccountId>, +{ + fn call(&mut self, env: Environment) -> Result + where + E: Ext, + { + let mut env = env.buf_in_buf_out(); + let func_id = 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 input: (T::AccountId, T::AccountId, NetUid) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + let (hotkey, coldkey, netuid) = input; + + let stake_info = + pallet_subtensor::Pallet::::get_stake_info_for_hotkey_coldkey_netuid( + hotkey, coldkey, netuid, + ); + + let encoded_result = stake_info.encode(); + + env.write(&encoded_result, false, None) + .map_err(|_| DispatchError::Other("Failed to write output"))?; + + Ok(RetVal::Converging(0)) + } + // Weights are copied from the pallet calls. Use `WeightInfo` when available. + 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 input: (T::AccountId, NetUid, TaoCurrency) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + let (hotkey, netuid, amount_staked) = input; + + let call_result = pallet_subtensor::Pallet::::add_stake( + RawOrigin::Signed(env.ext().address().clone()).into(), + hotkey, + netuid, + amount_staked, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Err(e) => { + let error_code = Outcome::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 input: (T::AccountId, NetUid, AlphaCurrency) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + let (hotkey, netuid, amount_unstaked) = input; + + let call_result = pallet_subtensor::Pallet::::remove_stake( + RawOrigin::Signed(env.ext().address().clone()).into(), + hotkey, + netuid, + amount_unstaked, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Err(e) => { + let error_code = Outcome::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.ext().address().clone()).into(), + hotkey, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Err(e) => { + let error_code = Outcome::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.ext().address().clone()).into(), + hotkey, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Err(e) => { + let error_code = Outcome::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 input: (T::AccountId, T::AccountId, NetUid, NetUid, AlphaCurrency) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + let ( + origin_hotkey, + destination_hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + ) = input; + + let call_result = pallet_subtensor::Pallet::::move_stake( + RawOrigin::Signed(env.ext().address().clone()).into(), + origin_hotkey, + destination_hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Err(e) => { + let error_code = Outcome::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 input: (T::AccountId, T::AccountId, NetUid, NetUid, AlphaCurrency) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + let (destination_coldkey, hotkey, origin_netuid, destination_netuid, alpha_amount) = + input; + + let call_result = pallet_subtensor::Pallet::::transfer_stake( + RawOrigin::Signed(env.ext().address().clone()).into(), + destination_coldkey, + hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Err(e) => { + let error_code = Outcome::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 input: (T::AccountId, NetUid, NetUid, AlphaCurrency) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + let (hotkey, origin_netuid, destination_netuid, alpha_amount) = input; + + let call_result = pallet_subtensor::Pallet::::swap_stake( + RawOrigin::Signed(env.ext().address().clone()).into(), + hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Err(e) => { + let error_code = Outcome::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 input: (T::AccountId, NetUid, TaoCurrency, TaoCurrency, bool) = + env.read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + let (hotkey, netuid, amount_staked, limit_price, allow_partial) = input; + + let call_result = pallet_subtensor::Pallet::::add_stake_limit( + RawOrigin::Signed(env.ext().address().clone()).into(), + hotkey, + netuid, + amount_staked, + limit_price, + allow_partial, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Err(e) => { + let error_code = Outcome::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 input: (T::AccountId, NetUid, AlphaCurrency, TaoCurrency, bool) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + let (hotkey, netuid, amount_unstaked, limit_price, allow_partial) = input; + + let call_result = pallet_subtensor::Pallet::::remove_stake_limit( + RawOrigin::Signed(env.ext().address().clone()).into(), + hotkey, + netuid, + amount_unstaked, + limit_price, + allow_partial, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Err(e) => { + let error_code = Outcome::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 input: ( + T::AccountId, + NetUid, + NetUid, + AlphaCurrency, + TaoCurrency, + bool, + ) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + let ( + hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + limit_price, + allow_partial, + ) = input; + + let call_result = pallet_subtensor::Pallet::::swap_stake_limit( + RawOrigin::Signed(env.ext().address().clone()).into(), + hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + limit_price, + allow_partial, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Err(e) => { + let error_code = Outcome::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 input: (T::AccountId, NetUid, Option) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + let (hotkey, netuid, limit_price) = input; + + let call_result = pallet_subtensor::Pallet::::remove_stake_full_limit( + RawOrigin::Signed(env.ext().address().clone()).into(), + hotkey, + netuid, + limit_price, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Err(e) => { + let error_code = Outcome::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 input: (NetUid, T::AccountId) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + let (netuid, hotkey) = input; + + let call_result = pallet_subtensor::Pallet::::set_coldkey_auto_stake_hotkey( + RawOrigin::Signed(env.ext().address().clone()).into(), + netuid, + hotkey, + ); + + match call_result { + Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Err(e) => { + let error_code = Outcome::from(e) as u32; + Ok(RetVal::Converging(error_code)) + } + } + } + } + } + + fn enabled() -> bool { + true + } +} From 3724e33a7f046f8d932b0ef76e6acb2e07a9760d Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Wed, 15 Oct 2025 18:05:32 +0200 Subject: [PATCH 24/47] refactor(chain-ext): rename Outcome to Output --- chain-extensions/src/lib.rs | 50 +++++++++++++++++------------------ chain-extensions/src/types.rs | 34 ++++++++++++------------ 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/chain-extensions/src/lib.rs b/chain-extensions/src/lib.rs index a8e09afea..6d5febf66 100644 --- a/chain-extensions/src/lib.rs +++ b/chain-extensions/src/lib.rs @@ -1,6 +1,6 @@ mod types; -use crate::types::{FunctionId, Outcome}; +use crate::types::{FunctionId, Output}; use codec::Encode; use frame_support::{DebugNoBound, traits::Get}; use frame_system::RawOrigin; @@ -70,9 +70,9 @@ where ); match call_result { - Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), Err(e) => { - let error_code = Outcome::from(e) as u32; + let error_code = Output::from(e) as u32; Ok(RetVal::Converging(error_code)) } } @@ -97,9 +97,9 @@ where ); match call_result { - Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), Err(e) => { - let error_code = Outcome::from(e) as u32; + let error_code = Output::from(e) as u32; Ok(RetVal::Converging(error_code)) } } @@ -121,9 +121,9 @@ where ); match call_result { - Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), Err(e) => { - let error_code = Outcome::from(e) as u32; + let error_code = Output::from(e) as u32; Ok(RetVal::Converging(error_code)) } } @@ -145,9 +145,9 @@ where ); match call_result { - Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), Err(e) => { - let error_code = Outcome::from(e) as u32; + let error_code = Output::from(e) as u32; Ok(RetVal::Converging(error_code)) } } @@ -180,9 +180,9 @@ where ); match call_result { - Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), Err(e) => { - let error_code = Outcome::from(e) as u32; + let error_code = Output::from(e) as u32; Ok(RetVal::Converging(error_code)) } } @@ -210,9 +210,9 @@ where ); match call_result { - Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), Err(e) => { - let error_code = Outcome::from(e) as u32; + let error_code = Output::from(e) as u32; Ok(RetVal::Converging(error_code)) } } @@ -238,9 +238,9 @@ where ); match call_result { - Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), Err(e) => { - let error_code = Outcome::from(e) as u32; + let error_code = Output::from(e) as u32; Ok(RetVal::Converging(error_code)) } } @@ -267,9 +267,9 @@ where ); match call_result { - Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), Err(e) => { - let error_code = Outcome::from(e) as u32; + let error_code = Output::from(e) as u32; Ok(RetVal::Converging(error_code)) } } @@ -296,9 +296,9 @@ where ); match call_result { - Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), Err(e) => { - let error_code = Outcome::from(e) as u32; + let error_code = Output::from(e) as u32; Ok(RetVal::Converging(error_code)) } } @@ -340,9 +340,9 @@ where ); match call_result { - Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), Err(e) => { - let error_code = Outcome::from(e) as u32; + let error_code = Output::from(e) as u32; Ok(RetVal::Converging(error_code)) } } @@ -367,9 +367,9 @@ where ); match call_result { - Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), Err(e) => { - let error_code = Outcome::from(e) as u32; + let error_code = Output::from(e) as u32; Ok(RetVal::Converging(error_code)) } } @@ -393,9 +393,9 @@ where ); match call_result { - Ok(_) => Ok(RetVal::Converging(Outcome::Success as u32)), + Ok(_) => Ok(RetVal::Converging(Output::Success as u32)), Err(e) => { - let error_code = Outcome::from(e) as u32; + let error_code = Output::from(e) as u32; Ok(RetVal::Converging(error_code)) } } diff --git a/chain-extensions/src/types.rs b/chain-extensions/src/types.rs index de98567e5..aa7154867 100644 --- a/chain-extensions/src/types.rs +++ b/chain-extensions/src/types.rs @@ -22,7 +22,7 @@ pub enum FunctionId { #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug)] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] -pub enum Outcome { +pub enum Output { /// Success Success = 0, /// Unknown error @@ -57,28 +57,28 @@ pub enum Outcome { SameNetuid = 15, } -impl From for Outcome { +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") => Outcome::NotEnoughBalanceToStake, - Some("NonAssociatedColdKey") => Outcome::NonAssociatedColdKey, - Some("BalanceWithdrawalError") => Outcome::BalanceWithdrawalError, - Some("HotKeyNotRegisteredInSubNet") => Outcome::NotRegistered, - Some("HotKeyAccountNotExists") => Outcome::NotRegistered, - Some("NotEnoughStakeToWithdraw") => Outcome::NotEnoughStakeToWithdraw, - Some("TxRateLimitExceeded") => Outcome::TxRateLimitExceeded, - Some("SlippageTooHigh") => Outcome::SlippageTooHigh, - Some("SubnetNotExists") => Outcome::SubnetNotExists, - Some("SameAutoStakeHotkeyAlreadySet") => Outcome::SameAutoStakeHotkeyAlreadySet, - Some("InsufficientBalance") => Outcome::InsufficientBalance, - Some("AmountTooLow") => Outcome::AmountTooLow, - Some("InsufficientLiquidity") => Outcome::InsufficientLiquidity, - Some("SameNetuid") => Outcome::SameNetuid, - _ => Outcome::RuntimeError, + 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, + _ => Output::RuntimeError, } } } From c6547fd75cb046ab6fe6878b18443b522dffe74b Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 16 Oct 2025 14:52:43 +0200 Subject: [PATCH 25/47] build(chain-ext): add test dependencies --- Cargo.lock | 10 ++++++++++ chain-extensions/Cargo.toml | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index a59c1404b..f58e912ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17962,9 +17962,18 @@ dependencies = [ "frame-system", "log", "num_enum", + "pallet-balances", "pallet-contracts 40.1.0", + "pallet-crowdloan", + "pallet-drand", + "pallet-membership", + "pallet-preimage", + "pallet-scheduler", "pallet-subtensor", + "pallet-subtensor-collective", "pallet-subtensor-swap", + "pallet-subtensor-utility", + "pallet-timestamp", "parity-scale-codec", "scale-info", "sp-core", @@ -17972,6 +17981,7 @@ dependencies = [ "sp-runtime", "sp-std", "subtensor-runtime-common", + "subtensor-swap-interface", ] [[package]] diff --git a/chain-extensions/Cargo.toml b/chain-extensions/Cargo.toml index f4dcd988c..16b457675 100644 --- a/chain-extensions/Cargo.toml +++ b/chain-extensions/Cargo.toml @@ -24,6 +24,16 @@ subtensor-runtime-common.workspace = true pallet-contracts.workspace = true pallet-subtensor.workspace = true pallet-subtensor-swap.workspace = true +pallet-balances.workspace = true +pallet-membership.workspace = true +pallet-scheduler.workspace = true +pallet-preimage.workspace = true +pallet-timestamp.workspace = true +pallet-crowdloan.workspace = true +pallet-subtensor-collective.workspace = true +pallet-subtensor-utility.workspace = true +pallet-drand.workspace = true +subtensor-swap-interface.workspace = true num_enum.workspace = true [lints] @@ -45,4 +55,14 @@ std = [ "pallet-contracts/std", "pallet-subtensor/std", "pallet-subtensor-swap/std", + "pallet-balances/std", + "pallet-membership/std", + "pallet-scheduler/std", + "pallet-preimage/std", + "pallet-timestamp/std", + "pallet-crowdloan/std", + "pallet-subtensor-collective/std", + "pallet-subtensor-utility/std", + "pallet-drand/std", + "subtensor-swap-interface/std", ] From 2279c43b6f19c5d81f4725052f1d672649828ada Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 16 Oct 2025 14:53:29 +0200 Subject: [PATCH 26/47] refactor(chain-ext): introduce testable extension environment trait --- chain-extensions/src/lib.rs | 206 ++++++++++++++++++++++++++---------- 1 file changed, 150 insertions(+), 56 deletions(-) diff --git a/chain-extensions/src/lib.rs b/chain-extensions/src/lib.rs index 6d5febf66..dcdebeb9d 100644 --- a/chain-extensions/src/lib.rs +++ b/chain-extensions/src/lib.rs @@ -1,11 +1,16 @@ +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + mod types; use crate::types::{FunctionId, Output}; -use codec::Encode; +use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::{DebugNoBound, traits::Get}; use frame_system::RawOrigin; use pallet_contracts::chain_extension::{ - ChainExtension, Environment, Ext, InitState, RetVal, SysConfig, + BufInBufOutState, ChainExtension, Environment, Ext, InitState, RetVal, SysConfig, }; use sp_runtime::{DispatchError, Weight, traits::StaticLookup}; use sp_std::marker::PhantomData; @@ -14,17 +19,42 @@ use subtensor_runtime_common::{AlphaCurrency, NetUid, 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, + T::AccountId: Clone, <::Lookup as StaticLookup>::Source: From<::AccountId>, { fn call(&mut self, env: Environment) -> Result where E: Ext, { - let mut env = env.buf_in_buf_out(); - let func_id = env.func_id().try_into().map_err(|_| { + let mut adapter = ContractsEnvAdapter::::new(env); + Self::dispatch(&mut adapter) + } + + fn enabled() -> bool { + true + } +} + +impl SubtensorChainExtension +where + T: pallet_subtensor::Config + pallet_contracts::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", ) @@ -32,10 +62,9 @@ where match func_id { FunctionId::GetStakeInfoForHotkeyColdkeyNetuidV1 => { - let input: (T::AccountId, T::AccountId, NetUid) = env + let (hotkey, coldkey, netuid): (T::AccountId, T::AccountId, NetUid) = env .read_as() .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; - let (hotkey, coldkey, netuid) = input; let stake_info = pallet_subtensor::Pallet::::get_stake_info_for_hotkey_coldkey_netuid( @@ -44,12 +73,11 @@ where let encoded_result = stake_info.encode(); - env.write(&encoded_result, false, None) + env.write_output(&encoded_result) .map_err(|_| DispatchError::Other("Failed to write output"))?; Ok(RetVal::Converging(0)) } - // Weights are copied from the pallet calls. Use `WeightInfo` when available. FunctionId::AddStakeV1 => { let weight = Weight::from_parts(340_800_000, 0) .saturating_add(T::DbWeight::get().reads(24_u64)) @@ -57,13 +85,12 @@ where env.charge_weight(weight)?; - let input: (T::AccountId, NetUid, TaoCurrency) = env + let (hotkey, netuid, amount_staked): (T::AccountId, NetUid, TaoCurrency) = env .read_as() .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; - let (hotkey, netuid, amount_staked) = input; let call_result = pallet_subtensor::Pallet::::add_stake( - RawOrigin::Signed(env.ext().address().clone()).into(), + RawOrigin::Signed(env.caller()).into(), hotkey, netuid, amount_staked, @@ -84,13 +111,12 @@ where env.charge_weight(weight)?; - let input: (T::AccountId, NetUid, AlphaCurrency) = env + let (hotkey, netuid, amount_unstaked): (T::AccountId, NetUid, AlphaCurrency) = env .read_as() .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; - let (hotkey, netuid, amount_unstaked) = input; let call_result = pallet_subtensor::Pallet::::remove_stake( - RawOrigin::Signed(env.ext().address().clone()).into(), + RawOrigin::Signed(env.caller()).into(), hotkey, netuid, amount_unstaked, @@ -116,7 +142,7 @@ where .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; let call_result = pallet_subtensor::Pallet::::unstake_all( - RawOrigin::Signed(env.ext().address().clone()).into(), + RawOrigin::Signed(env.caller()).into(), hotkey, ); @@ -140,7 +166,7 @@ where .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; let call_result = pallet_subtensor::Pallet::::unstake_all_alpha( - RawOrigin::Signed(env.ext().address().clone()).into(), + RawOrigin::Signed(env.caller()).into(), hotkey, ); @@ -159,19 +185,18 @@ where env.charge_weight(weight)?; - let input: (T::AccountId, T::AccountId, NetUid, NetUid, AlphaCurrency) = env - .read_as() - .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; let ( origin_hotkey, destination_hotkey, origin_netuid, destination_netuid, alpha_amount, - ) = input; + ): (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.ext().address().clone()).into(), + RawOrigin::Signed(env.caller()).into(), origin_hotkey, destination_hotkey, origin_netuid, @@ -194,14 +219,18 @@ where env.charge_weight(weight)?; - let input: (T::AccountId, T::AccountId, NetUid, NetUid, AlphaCurrency) = env + 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 (destination_coldkey, hotkey, origin_netuid, destination_netuid, alpha_amount) = - input; let call_result = pallet_subtensor::Pallet::::transfer_stake( - RawOrigin::Signed(env.ext().address().clone()).into(), + RawOrigin::Signed(env.caller()).into(), destination_coldkey, hotkey, origin_netuid, @@ -224,13 +253,17 @@ where env.charge_weight(weight)?; - let input: (T::AccountId, NetUid, NetUid, AlphaCurrency) = env + 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 (hotkey, origin_netuid, destination_netuid, alpha_amount) = input; let call_result = pallet_subtensor::Pallet::::swap_stake( - RawOrigin::Signed(env.ext().address().clone()).into(), + RawOrigin::Signed(env.caller()).into(), hotkey, origin_netuid, destination_netuid, @@ -252,13 +285,18 @@ where env.charge_weight(weight)?; - let input: (T::AccountId, NetUid, TaoCurrency, TaoCurrency, bool) = - env.read_as() - .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; - let (hotkey, netuid, amount_staked, limit_price, allow_partial) = input; + 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.ext().address().clone()).into(), + RawOrigin::Signed(env.caller()).into(), hotkey, netuid, amount_staked, @@ -281,13 +319,18 @@ where env.charge_weight(weight)?; - let input: (T::AccountId, NetUid, AlphaCurrency, TaoCurrency, bool) = env + 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 (hotkey, netuid, amount_unstaked, limit_price, allow_partial) = input; let call_result = pallet_subtensor::Pallet::::remove_stake_limit( - RawOrigin::Signed(env.ext().address().clone()).into(), + RawOrigin::Signed(env.caller()).into(), hotkey, netuid, amount_unstaked, @@ -310,7 +353,14 @@ where env.charge_weight(weight)?; - let input: ( + let ( + hotkey, + origin_netuid, + destination_netuid, + alpha_amount, + limit_price, + allow_partial, + ): ( T::AccountId, NetUid, NetUid, @@ -320,17 +370,9 @@ where ) = env .read_as() .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; - let ( - hotkey, - origin_netuid, - destination_netuid, - alpha_amount, - limit_price, - allow_partial, - ) = input; let call_result = pallet_subtensor::Pallet::::swap_stake_limit( - RawOrigin::Signed(env.ext().address().clone()).into(), + RawOrigin::Signed(env.caller()).into(), hotkey, origin_netuid, destination_netuid, @@ -354,13 +396,12 @@ where env.charge_weight(weight)?; - let input: (T::AccountId, NetUid, Option) = env - .read_as() - .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; - let (hotkey, netuid, limit_price) = input; + 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.ext().address().clone()).into(), + RawOrigin::Signed(env.caller()).into(), hotkey, netuid, limit_price, @@ -381,13 +422,12 @@ where env.charge_weight(weight)?; - let input: (NetUid, T::AccountId) = env + let (netuid, hotkey): (NetUid, T::AccountId) = env .read_as() .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; - let (netuid, hotkey) = input; let call_result = pallet_subtensor::Pallet::::set_coldkey_auto_stake_hotkey( - RawOrigin::Signed(env.ext().address().clone()).into(), + RawOrigin::Signed(env.caller()).into(), netuid, hotkey, ); @@ -402,8 +442,62 @@ where } } } +} - fn enabled() -> bool { - true +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() } } From dab4c46542530193b7b4aeb91292e2f476e8d4da Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 16 Oct 2025 14:53:50 +0200 Subject: [PATCH 27/47] test(chain-ext): add test infrastructure and test suite --- chain-extensions/src/mock.rs | 1140 +++++++++++++++++++++++++++++++++ chain-extensions/src/tests.rs | 901 ++++++++++++++++++++++++++ 2 files changed, 2041 insertions(+) create mode 100644 chain-extensions/src/mock.rs create mode 100644 chain-extensions/src/tests.rs diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs new file mode 100644 index 000000000..ee15a8ef9 --- /dev/null +++ b/chain-extensions/src/mock.rs @@ -0,0 +1,1140 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::unwrap_used +)] + +use core::num::NonZeroU64; + +use frame_support::dispatch::{DispatchResult, DispatchResultWithPostInfo}; +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::{Hooks, PrivilegeCmp}, +}; +use frame_system as system; +use frame_system::{EnsureNever, EnsureRoot, RawOrigin, limits, offchain::CreateTransactionBase}; +use pallet_contracts::HoldReason as ContractsHoldReason; +use pallet_subtensor::utils::rate_limiting::TransactionType; +use pallet_subtensor::*; +use pallet_subtensor_collective as pallet_collective; +use pallet_subtensor_collective::MemberCount; +use pallet_subtensor_utility as pallet_utility; +use sp_core::{ConstU64, Get, 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::Currency as CurrencyTrait; +use subtensor_runtime_common::{AlphaCurrency, NetUid, TaoCurrency}; +use subtensor_swap_interface::{Order, SwapHandler}; + +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, + Triumvirate: pallet_collective::::{Pallet, Call, Storage, Origin, Event, Config} = 3, + TriumvirateMembers: pallet_membership::::{Pallet, Call, Storage, Event, Config} = 4, + Senate: pallet_collective::::{Pallet, Call, Storage, Origin, Event, Config} = 5, + SenateMembers: pallet_membership::::{Pallet, Call, Storage, Event, Config} = 6, + 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, + } +); + +#[allow(dead_code)] +pub type SubtensorCall = pallet_subtensor::Call; + +#[allow(dead_code)] +pub type SubtensorEvent = pallet_subtensor::Event; + +#[allow(dead_code)] +pub type BalanceCall = pallet_balances::Call; + +#[allow(dead_code)] +pub type TestRuntimeCall = frame_system::Call; + +pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"test"); + +#[allow(dead_code)] +pub type AccountId = U256; + +// The address format for describing accounts. +#[allow(dead_code)] +pub type Address = AccountId; + +// 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 = (); +} + +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 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 InitialSenateRequiredStakePercentage: u64 = 2; // 2 percent of total stake + 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; +} + +// Configure collective pallet for council +parameter_types! { + pub const CouncilMotionDuration: BlockNumber = 100; + pub const CouncilMaxProposals: u32 = 10; + pub const CouncilMaxMembers: u32 = 3; +} + +// Configure collective pallet for Senate +parameter_types! { + pub const SenateMaxMembers: u32 = 12; +} + +use pallet_collective::{CanPropose, CanVote, GetVotingMembers}; +pub struct CanProposeToTriumvirate; +impl CanPropose for CanProposeToTriumvirate { + fn can_propose(account: &AccountId) -> bool { + Triumvirate::is_member(account) + } +} + +pub struct CanVoteToTriumvirate; +impl CanVote for CanVoteToTriumvirate { + fn can_vote(_: &AccountId) -> bool { + //Senate::is_member(account) + false // Disable voting from pallet_collective::vote + } +} + +use pallet_subtensor::{CollectiveInterface, MemberManagement, StakeThreshold}; +pub struct ManageSenateMembers; +impl MemberManagement for ManageSenateMembers { + fn add_member(account: &AccountId) -> DispatchResultWithPostInfo { + let who = *account; + SenateMembers::add_member(RawOrigin::Root.into(), who) + } + + fn remove_member(account: &AccountId) -> DispatchResultWithPostInfo { + let who = *account; + SenateMembers::remove_member(RawOrigin::Root.into(), who) + } + + fn swap_member(rm: &AccountId, add: &AccountId) -> DispatchResultWithPostInfo { + let remove = *rm; + let add = *add; + + Triumvirate::remove_votes(rm)?; + SenateMembers::swap_member(RawOrigin::Root.into(), remove, add) + } + + fn is_member(account: &AccountId) -> bool { + SenateMembers::members().contains(account) + } + + fn members() -> Vec { + SenateMembers::members().into() + } + + fn max_members() -> u32 { + SenateMaxMembers::get() + } +} + +pub struct GetSenateMemberCount; +impl GetVotingMembers for GetSenateMemberCount { + fn get_count() -> MemberCount { + Senate::members().len() as u32 + } +} +impl Get for GetSenateMemberCount { + fn get() -> MemberCount { + SenateMaxMembers::get() + } +} + +pub struct TriumvirateVotes; +impl CollectiveInterface for TriumvirateVotes { + fn remove_votes(hotkey: &AccountId) -> Result { + Triumvirate::remove_votes(hotkey) + } + + fn add_vote( + hotkey: &AccountId, + proposal: H256, + index: u32, + approve: bool, + ) -> Result { + Triumvirate::do_vote(*hotkey, proposal, index, approve) + } +} + +// We call pallet_collective TriumvirateCollective +#[allow(dead_code)] +type TriumvirateCollective = pallet_collective::Instance1; +impl pallet_collective::Config for Test { + type RuntimeOrigin = RuntimeOrigin; + type Proposal = RuntimeCall; + type MotionDuration = CouncilMotionDuration; + type MaxProposals = CouncilMaxProposals; + type MaxMembers = GetSenateMemberCount; + type DefaultVote = pallet_collective::PrimeDefaultVote; + type WeightInfo = pallet_collective::weights::SubstrateWeight; + type SetMembersOrigin = EnsureNever; + type CanPropose = CanProposeToTriumvirate; + type CanVote = CanVoteToTriumvirate; + type GetVotingMembers = GetSenateMemberCount; +} + +// We call council members Triumvirate +#[allow(dead_code)] +type TriumvirateMembership = pallet_membership::Instance1; +impl pallet_membership::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AddOrigin = EnsureRoot; + type RemoveOrigin = EnsureRoot; + type SwapOrigin = EnsureRoot; + type ResetOrigin = EnsureRoot; + type PrimeOrigin = EnsureRoot; + type MembershipInitialized = Triumvirate; + type MembershipChanged = Triumvirate; + type MaxMembers = CouncilMaxMembers; + type WeightInfo = pallet_membership::weights::SubstrateWeight; +} + +// This is a dummy collective instance for managing senate members +// Probably not the best solution, but fastest implementation +#[allow(dead_code)] +type SenateCollective = pallet_collective::Instance2; +impl pallet_collective::Config for Test { + type RuntimeOrigin = RuntimeOrigin; + type Proposal = RuntimeCall; + type MotionDuration = CouncilMotionDuration; + type MaxProposals = CouncilMaxProposals; + type MaxMembers = SenateMaxMembers; + type DefaultVote = pallet_collective::PrimeDefaultVote; + type WeightInfo = pallet_collective::weights::SubstrateWeight; + type SetMembersOrigin = EnsureNever; + type CanPropose = (); + type CanVote = (); + type GetVotingMembers = (); +} + +// We call our top K delegates membership Senate +#[allow(dead_code)] +type SenateMembership = pallet_membership::Instance2; +impl pallet_membership::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AddOrigin = EnsureRoot; + type RemoveOrigin = EnsureRoot; + type SwapOrigin = EnsureRoot; + type ResetOrigin = EnsureRoot; + type PrimeOrigin = EnsureRoot; + type MembershipInitialized = Senate; + type MembershipChanged = Senate; + type MaxMembers = SenateMaxMembers; + type WeightInfo = pallet_membership::weights::SubstrateWeight; +} + +impl pallet_subtensor::Config for Test { + type RuntimeCall = RuntimeCall; + type Currency = Balances; + type InitialIssuance = InitialIssuance; + type SudoRuntimeCall = TestRuntimeCall; + type CouncilOrigin = frame_system::EnsureSigned; + type SenateMembers = ManageSenateMembers; + type TriumvirateInterface = TriumvirateVotes; + 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 InitialSenateRequiredStakePercentage = InitialSenateRequiredStakePercentage; + 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 test_ext_with_balances(balances: Vec<(U256, u128)>) -> sp_io::TestExternalities { + init_logs_for_tests(); + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .unwrap(); + + pallet_balances::GenesisConfig:: { + balances: balances + .iter() + .map(|(a, b)| (*a, *b as u64)) + .collect::>(), + dev_accounts: None, + } + .assimilate_storage(&mut t) + .unwrap(); + + t.into() +} + +#[allow(dead_code)] +pub(crate) fn step_block(n: u16) { + for _ in 0..n { + Scheduler::on_finalize(System::block_number()); + SubtensorModule::on_finalize(System::block_number()); + System::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + System::on_initialize(System::block_number()); + SubtensorModule::on_initialize(System::block_number()); + Scheduler::on_initialize(System::block_number()); + } +} + +#[allow(dead_code)] +pub(crate) fn run_to_block(n: u64) { + run_to_block_ext(n, false) +} + +#[allow(dead_code)] +pub(crate) fn run_to_block_ext(n: u64, enable_events: bool) { + while System::block_number() < n { + Scheduler::on_finalize(System::block_number()); + SubtensorModule::on_finalize(System::block_number()); + System::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + System::on_initialize(System::block_number()); + if !enable_events { + System::events().iter().for_each(|event| { + log::info!("Event: {:?}", event.event); + }); + System::reset_events(); + } + SubtensorModule::on_initialize(System::block_number()); + Scheduler::on_initialize(System::block_number()); + } +} + +#[allow(dead_code)] +pub(crate) fn next_block_no_epoch(netuid: NetUid) -> u64 { + // high tempo to skip automatic epochs in on_initialize + let high_tempo: u16 = u16::MAX - 1; + let old_tempo: u16 = SubtensorModule::get_tempo(netuid); + + SubtensorModule::set_tempo(netuid, high_tempo); + let new_block = next_block(); + SubtensorModule::set_tempo(netuid, old_tempo); + + new_block +} + +#[allow(dead_code)] +pub(crate) fn run_to_block_no_epoch(netuid: NetUid, n: u64) { + // high tempo to skip automatic epochs in on_initialize + let high_tempo: u16 = u16::MAX - 1; + let old_tempo: u16 = SubtensorModule::get_tempo(netuid); + + SubtensorModule::set_tempo(netuid, high_tempo); + run_to_block(n); + SubtensorModule::set_tempo(netuid, old_tempo); +} + +#[allow(dead_code)] +pub(crate) fn step_epochs(count: u16, netuid: NetUid) { + for _ in 0..count { + let blocks_to_next_epoch = SubtensorModule::blocks_until_next_epoch( + netuid, + SubtensorModule::get_tempo(netuid), + SubtensorModule::get_current_block_as_u64(), + ); + log::info!("Blocks to next epoch: {blocks_to_next_epoch:?}"); + step_block(blocks_to_next_epoch as u16); + + assert!(SubtensorModule::should_run_epoch( + netuid, + SubtensorModule::get_current_block_as_u64() + )); + step_block(1); + } +} + +/// Increments current block by 1, running all hooks associated with doing so, and asserts +/// that the block number was in fact incremented. +/// +/// Returns the new block number. +#[allow(dead_code)] +#[cfg(test)] +pub(crate) fn next_block() -> u64 { + let mut block = System::block_number(); + block += 1; + run_to_block(block); + assert_eq!(System::block_number(), block); + block +} + +#[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_network(netuid: NetUid, tempo: u16, _modality: u16) { + SubtensorModule::init_new_network(netuid, tempo); + SubtensorModule::set_network_registration_allowed(netuid, true); + SubtensorModule::set_network_pow_registration_allowed(netuid, true); + FirstEmissionBlockNumber::::insert(netuid, 1); + SubtokenEnabled::::insert(netuid, true); +} + +#[allow(dead_code)] +pub fn add_network_without_emission_block(netuid: NetUid, tempo: u16, _modality: u16) { + SubtensorModule::init_new_network(netuid, tempo); + SubtensorModule::set_network_registration_allowed(netuid, true); + SubtensorModule::set_network_pow_registration_allowed(netuid, true); +} + +#[allow(dead_code)] +pub fn add_network_disable_subtoken(netuid: NetUid, tempo: u16, _modality: u16) { + SubtensorModule::init_new_network(netuid, tempo); + SubtensorModule::set_network_registration_allowed(netuid, true); + SubtensorModule::set_network_pow_registration_allowed(netuid, true); + SubtokenEnabled::::insert(netuid, false); +} + +#[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 fn add_dynamic_network_without_emission_block(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); + netuid +} + +#[allow(dead_code)] +pub fn add_dynamic_network_disable_commit_reveal(hotkey: &U256, coldkey: &U256) -> NetUid { + let netuid = add_dynamic_network(hotkey, coldkey); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); + netuid +} + +#[allow(dead_code)] +pub fn add_network_disable_commit_reveal(netuid: NetUid, tempo: u16, _modality: u16) { + add_network(netuid, tempo, _modality); + SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); +} + +#[allow(dead_code)] +pub fn wait_set_pending_children_cooldown(netuid: NetUid) { + let cooldown = DefaultPendingCooldown::::get(); + step_block(cooldown as u16); // Wait for cooldown to pass + step_epochs(1, netuid); // Run next epoch +} + +#[allow(dead_code)] +pub fn wait_and_set_pending_children(netuid: NetUid) { + let original_block = System::block_number(); + wait_set_pending_children_cooldown(netuid); + SubtensorModule::do_set_pending_children(netuid); + System::set_block_number(original_block); +} + +#[allow(dead_code)] +pub fn mock_schedule_children( + coldkey: &U256, + parent: &U256, + netuid: NetUid, + child_vec: &[(u64, U256)], +) { + // Set minimum stake for setting children + StakeThreshold::::put(0); + + // Set initial parent-child relationship + assert_ok!(SubtensorModule::do_schedule_children( + RuntimeOrigin::signed(*coldkey), + *parent, + netuid, + child_vec.to_vec() + )); +} + +#[allow(dead_code)] +pub fn mock_set_children(coldkey: &U256, parent: &U256, netuid: NetUid, child_vec: &[(u64, U256)]) { + mock_schedule_children(coldkey, parent, netuid, child_vec); + wait_and_set_pending_children(netuid); +} + +#[allow(dead_code)] +pub fn mock_set_children_no_epochs(netuid: NetUid, parent: &U256, child_vec: &[(u64, U256)]) { + let backup_block = SubtensorModule::get_current_block_as_u64(); + PendingChildKeys::::insert(netuid, parent, (child_vec, 0)); + System::set_block_number(1); + SubtensorModule::do_set_pending_children(netuid); + System::set_block_number(backup_block); +} + +// Helper function to wait for the rate limit +#[allow(dead_code)] +pub fn step_rate_limit(transaction_type: &TransactionType, netuid: NetUid) { + // Check rate limit + let limit = transaction_type.rate_limit_on_subnet::(netuid); + + // Step that many blocks + step_block(limit as u16); +} + +#[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); +} + +#[allow(dead_code)] +pub(crate) fn swap_tao_to_alpha(netuid: NetUid, tao: TaoCurrency) -> (AlphaCurrency, u64) { + if netuid.is_root() { + return (tao.to_u64().into(), 0); + } + + let order = GetAlphaForTao::::with_amount(tao); + let result = ::SwapInterface::swap( + netuid.into(), + order, + ::SwapInterface::max_price(), + false, + true, + ); + + assert_ok!(&result); + + let result = result.unwrap(); + + // we don't want to have silent 0 comparisons in tests + assert!(result.amount_paid_out > AlphaCurrency::ZERO); + + (result.amount_paid_out, result.fee_paid.into()) +} + +#[allow(dead_code)] +pub(crate) fn swap_alpha_to_tao_ext( + netuid: NetUid, + alpha: AlphaCurrency, + drop_fees: bool, +) -> (TaoCurrency, u64) { + if netuid.is_root() { + return (alpha.to_u64().into(), 0); + } + + println!( + "::SwapInterface::min_price() = {:?}", + ::SwapInterface::min_price::() + ); + + let order = GetTaoForAlpha::::with_amount(alpha); + let result = ::SwapInterface::swap( + netuid.into(), + order, + ::SwapInterface::min_price(), + drop_fees, + true, + ); + + assert_ok!(&result); + + let result = result.unwrap(); + + // we don't want to have silent 0 comparisons in tests + assert!(!result.amount_paid_out.is_zero()); + + (result.amount_paid_out, result.fee_paid.into()) +} + +#[allow(dead_code)] +pub(crate) fn swap_alpha_to_tao(netuid: NetUid, alpha: AlphaCurrency) -> (TaoCurrency, u64) { + swap_alpha_to_tao_ext(netuid, alpha, false) +} + +#[allow(dead_code)] +pub(crate) fn last_event() -> RuntimeEvent { + System::events().pop().expect("RuntimeEvent expected").event +} + +#[allow(dead_code)] +pub fn assert_last_event( + generic_event: ::RuntimeEvent, +) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +} + +#[allow(dead_code)] +pub fn commit_dummy(who: U256, netuid: NetUid) { + SubtensorModule::set_weights_set_rate_limit(netuid, 0); + + // any 32‑byte value is fine; hash is never opened + let hash = sp_core::H256::from_low_u64_be(0xDEAD_BEEF); + assert_ok!(SubtensorModule::do_commit_weights( + RuntimeOrigin::signed(who), + netuid, + hash + )); +} diff --git a/chain-extensions/src/tests.rs b/chain-extensions/src/tests.rs new file mode 100644 index 000000000..a59c4aa5a --- /dev/null +++ b/chain-extensions/src/tests.rs @@ -0,0 +1,901 @@ +#![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); + }); +} + +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); + }); +} From 8e55f846d4f59ea49803abcbd1f60448df3d8183 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 16 Oct 2025 15:16:45 +0200 Subject: [PATCH 28/47] refactor(runtime): replace chain extension with subtensor-chain-extensions crate --- Cargo.lock | 1 + runtime/Cargo.toml | 2 ++ runtime/src/chain_extension.rs | 52 ---------------------------------- runtime/src/lib.rs | 3 +- 4 files changed, 4 insertions(+), 54 deletions(-) delete mode 100644 runtime/src/chain_extension.rs diff --git a/Cargo.lock b/Cargo.lock index f58e912ad..36d231664 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8347,6 +8347,7 @@ dependencies = [ "sp-version", "substrate-fixed", "substrate-wasm-builder", + "subtensor-chain-extensions", "subtensor-custom-rpc-runtime-api", "subtensor-macros", "subtensor-precompiles", diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 113d63d39..138216f5a 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -100,6 +100,7 @@ runtime-common.workspace = true # Wasm smart contracts support pallet-contracts.workspace = true +subtensor-chain-extensions.workspace = true # NPoS frame-election-provider-support = { workspace = true } @@ -274,6 +275,7 @@ std = [ "pallet-subtensor-swap-runtime-api/std", "subtensor-swap-interface/std", "pallet-contracts/std", + "subtensor-chain-extensions/std", "ethereum/std", ] runtime-benchmarks = [ diff --git a/runtime/src/chain_extension.rs b/runtime/src/chain_extension.rs deleted file mode 100644 index 801bc83fa..000000000 --- a/runtime/src/chain_extension.rs +++ /dev/null @@ -1,52 +0,0 @@ -use codec::Encode; -use pallet_contracts::chain_extension::{ - ChainExtension, Environment, Ext, InitState, RetVal, SysConfig, -}; -use sp_runtime::{AccountId32, DispatchError}; -use subtensor_runtime_common::NetUid; - -use crate::{Runtime, SubtensorModule}; - -#[derive(Default)] -pub struct SubtensorChainExtension; - -impl ChainExtension for SubtensorChainExtension { - fn call(&mut self, env: Environment) -> Result - where - E::T: SysConfig, - { - let func_id = env.func_id(); - - match func_id { - // Function ID 1001: get_stake_info_for_hotkey_coldkey_netuid - 1001 => { - let mut env = env.buf_in_buf_out(); - - let input: (AccountId32, AccountId32, NetUid) = env - .read_as() - .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; - - let (hotkey, coldkey, netuid) = input; - - let stake_info = SubtensorModule::get_stake_info_for_hotkey_coldkey_netuid( - hotkey, coldkey, netuid, - ); - - let encoded_result = stake_info.encode(); - - env.write(&encoded_result, false, None) - .map_err(|_| DispatchError::Other("Failed to write output"))?; - - Ok(RetVal::Converging(0)) - } - _ => { - log::error!("Called an unregistered chain extension function: {func_id}",); - Err(DispatchError::Other("Unimplemented function ID")) - } - } - } - - fn enabled() -> bool { - true - } -} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index ba21b9321..486b88b70 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -10,7 +10,6 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); use core::num::NonZeroU64; -pub mod chain_extension; pub mod check_nonce; mod migrations; pub mod transaction_payment_wrapper; @@ -1704,7 +1703,7 @@ impl pallet_contracts::Config for Runtime { type CallStack = [pallet_contracts::Frame; 5]; type WeightPrice = pallet_transaction_payment::Pallet; type WeightInfo = pallet_contracts::weights::SubstrateWeight; - type ChainExtension = chain_extension::SubtensorChainExtension; + type ChainExtension = subtensor_chain_extensions::SubtensorChainExtension; type Schedule = ContractsSchedule; type AddressGenerator = pallet_contracts::DefaultAddressGenerator; type MaxCodeLen = ConstU32<{ 128 * 1024 }>; From 8dd3c5c6c79d5c49d3274586e2d91b0bed297043 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 16 Oct 2025 15:24:21 +0200 Subject: [PATCH 29/47] refactor(contracts): remove SubtensorModule calls from whitelist --- docs/contracts.md | 12 ------------ runtime/src/lib.rs | 15 --------------- 2 files changed, 27 deletions(-) diff --git a/docs/contracts.md b/docs/contracts.md index 068b9d566..b6d3f297a 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -43,18 +43,6 @@ pub trait SubtensorExtension { For security, contracts can only dispatch a limited set of runtime calls: **Whitelisted Calls:** -- `SubtensorModule::add_stake` - Delegate stake from a coldkey to a hotkey -- `SubtensorModule::remove_stake` - Withdraw stake from a hotkey back to the caller -- `SubtensorModule::unstake_all` - Unstake all funds associated with a hotkey -- `SubtensorModule::unstake_all_alpha` - Unstake all alpha stake from a hotkey -- `SubtensorModule::move_stake` - Move stake between hotkeys -- `SubtensorModule::transfer_stake` - Transfer stake between coldkeys (optionally across subnets) -- `SubtensorModule::swap_stake` - Swap stake allocations between subnets -- `SubtensorModule::add_stake_limit` - Delegate stake with a price limit -- `SubtensorModule::remove_stake_limit` - Withdraw staked funds with a price limit -- `SubtensorModule::swap_stake_limit` - Swap stake between subnets with a price limit -- `SubtensorModule::remove_stake_full_limit` - Fully withdraw stake subject to a price limit -- `SubtensorModule::set_coldkey_auto_stake_hotkey` - Configure the automatic stake destination for a coldkey - `Proxy::proxy` - Execute proxy calls - `Proxy::add_proxy` - Add a proxy relationship diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 486b88b70..a0bed93d3 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1666,21 +1666,6 @@ pub struct ContractCallFilter; impl Contains for ContractCallFilter { fn contains(call: &RuntimeCall) -> bool { match call { - RuntimeCall::SubtensorModule(inner) => matches!( - inner, - pallet_subtensor::Call::add_stake { .. } - | pallet_subtensor::Call::remove_stake { .. } - | pallet_subtensor::Call::unstake_all { .. } - | pallet_subtensor::Call::unstake_all_alpha { .. } - | pallet_subtensor::Call::move_stake { .. } - | pallet_subtensor::Call::transfer_stake { .. } - | pallet_subtensor::Call::swap_stake { .. } - | pallet_subtensor::Call::add_stake_limit { .. } - | pallet_subtensor::Call::remove_stake_limit { .. } - | pallet_subtensor::Call::swap_stake_limit { .. } - | pallet_subtensor::Call::remove_stake_full_limit { .. } - | pallet_subtensor::Call::set_coldkey_auto_stake_hotkey { .. } - ), RuntimeCall::Proxy(inner) => matches!( inner, pallet_proxy::Call::proxy { .. } | pallet_proxy::Call::add_proxy { .. } From 4e7e9130bbfb1a6d8fd3f68a1d64cf69f3a2a943 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 16 Oct 2025 15:34:38 +0200 Subject: [PATCH 30/47] docs(contracts): document all chain extension functions and error codes --- docs/contracts.md | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/docs/contracts.md b/docs/contracts.md index b6d3f297a..5dd6ce45f 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -21,7 +21,19 @@ Subtensor provides a custom chain extension that allows smart contracts to inter | Function ID | Name | Description | Parameters | Returns | |------------|------|-------------|------------|---------| -| 1001 | `get_stake_info_for_hotkey_coldkey_netuid` | Query stake information | `(AccountId32, AccountId32, NetUid)` | Stake information | +| 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 | Example usage in your ink! contract: ```rust @@ -29,7 +41,7 @@ Example usage in your ink! contract: pub trait SubtensorExtension { type ErrorCode = SubtensorError; - #[ink(function = 1001)] + #[ink(function = 0)] fn get_stake_info( hotkey: AccountId, coldkey: AccountId, @@ -38,6 +50,29 @@ pub trait SubtensorExtension { } ``` +#### 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 | + ### Call Filter For security, contracts can only dispatch a limited set of runtime calls: From 68b89f8c061a065453206d54c75f5f6bb20c3ef7 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Fri, 17 Oct 2025 10:25:50 +0200 Subject: [PATCH 31/47] fix(chain-extensions): make types module public --- chain-extensions/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chain-extensions/src/lib.rs b/chain-extensions/src/lib.rs index dcdebeb9d..24f5580dc 100644 --- a/chain-extensions/src/lib.rs +++ b/chain-extensions/src/lib.rs @@ -3,7 +3,7 @@ mod mock; #[cfg(test)] mod tests; -mod types; +pub mod types; use crate::types::{FunctionId, Output}; use codec::{Decode, Encode, MaxEncodedLen}; From 61219e779ec9c36c98bc888f762e7bfeda7d88dc Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Fri, 17 Oct 2025 11:32:19 +0200 Subject: [PATCH 32/47] fix(chain-extensions): return correct success code --- chain-extensions/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chain-extensions/src/lib.rs b/chain-extensions/src/lib.rs index 24f5580dc..02aa3bc7f 100644 --- a/chain-extensions/src/lib.rs +++ b/chain-extensions/src/lib.rs @@ -76,7 +76,7 @@ where env.write_output(&encoded_result) .map_err(|_| DispatchError::Other("Failed to write output"))?; - Ok(RetVal::Converging(0)) + Ok(RetVal::Converging(Output::Success as u32)) } FunctionId::AddStakeV1 => { let weight = Weight::from_parts(340_800_000, 0) From 673d8c425b82aa5ee04aa00228f8f577c789c50e Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Fri, 17 Oct 2025 11:33:48 +0200 Subject: [PATCH 33/47] refactor(mock): clean up unused imports --- chain-extensions/src/mock.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs index cc4e54e69..134a152ef 100644 --- a/chain-extensions/src/mock.rs +++ b/chain-extensions/src/mock.rs @@ -6,7 +6,7 @@ use core::num::NonZeroU64; -use frame_support::dispatch::{DispatchResult, DispatchResultWithPostInfo}; +use frame_support::dispatch::DispatchResult; use frame_support::traits::{Contains, Everything, InherentBuilder, InsideBoth}; use frame_support::weights::Weight; use frame_support::weights::constants::RocksDbWeight; @@ -16,7 +16,7 @@ use frame_support::{ traits::{Hooks, PrivilegeCmp}, }; use frame_system as system; -use frame_system::{EnsureNever, EnsureRoot, RawOrigin, limits, offchain::CreateTransactionBase}; +use frame_system::{EnsureRoot, RawOrigin, limits, offchain::CreateTransactionBase}; use pallet_contracts::HoldReason as ContractsHoldReason; use pallet_subtensor::utils::rate_limiting::TransactionType; use pallet_subtensor::*; From e542ed5904a89622d233b3960930c9e9d29e8428 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Fri, 17 Oct 2025 12:34:09 +0200 Subject: [PATCH 34/47] chore(chain-extensions): add no_std support --- chain-extensions/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chain-extensions/src/lib.rs b/chain-extensions/src/lib.rs index 02aa3bc7f..b25c39e12 100644 --- a/chain-extensions/src/lib.rs +++ b/chain-extensions/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg_attr(not(feature = "std"), no_std)] + #[cfg(test)] mod mock; #[cfg(test)] From 0fd04bf29185b1ca48f2ec4bae164116fedf0598 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Fri, 17 Oct 2025 18:16:09 +0200 Subject: [PATCH 35/47] refactor(chain-extensions): remove unused mock utilities --- chain-extensions/src/mock.rs | 339 +---------------------------------- 1 file changed, 3 insertions(+), 336 deletions(-) diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs index 134a152ef..da0005102 100644 --- a/chain-extensions/src/mock.rs +++ b/chain-extensions/src/mock.rs @@ -11,26 +11,20 @@ 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::{Hooks, PrivilegeCmp}, -}; +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::utils::rate_limiting::TransactionType; use pallet_subtensor::*; use pallet_subtensor_utility as pallet_utility; -use sp_core::{ConstU64, Get, H256, U256, offchain::KeyTypeId}; +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::Currency as CurrencyTrait; use subtensor_runtime_common::{AlphaCurrency, NetUid, TaoCurrency}; -use subtensor_swap_interface::{Order, SwapHandler}; type Block = frame_system::mocking::MockBlock; @@ -51,27 +45,14 @@ frame_support::construct_runtime!( } ); -#[allow(dead_code)] -pub type SubtensorCall = pallet_subtensor::Call; - -#[allow(dead_code)] -pub type SubtensorEvent = pallet_subtensor::Event; - -#[allow(dead_code)] -pub type BalanceCall = pallet_balances::Call; +pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"test"); #[allow(dead_code)] pub type TestRuntimeCall = frame_system::Call; -pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"test"); - #[allow(dead_code)] pub type AccountId = U256; -// The address format for describing accounts. -#[allow(dead_code)] -pub type Address = AccountId; - // Balance of an account. #[allow(dead_code)] pub type Balance = u64; @@ -600,120 +581,6 @@ pub fn new_test_ext(block_number: BlockNumber) -> sp_io::TestExternalities { ext } -#[allow(dead_code)] -pub fn test_ext_with_balances(balances: Vec<(U256, u128)>) -> sp_io::TestExternalities { - init_logs_for_tests(); - let mut t = frame_system::GenesisConfig::::default() - .build_storage() - .unwrap(); - - pallet_balances::GenesisConfig:: { - balances: balances - .iter() - .map(|(a, b)| (*a, *b as u64)) - .collect::>(), - dev_accounts: None, - } - .assimilate_storage(&mut t) - .unwrap(); - - t.into() -} - -#[allow(dead_code)] -pub(crate) fn step_block(n: u16) { - for _ in 0..n { - Scheduler::on_finalize(System::block_number()); - SubtensorModule::on_finalize(System::block_number()); - System::on_finalize(System::block_number()); - System::set_block_number(System::block_number() + 1); - System::on_initialize(System::block_number()); - SubtensorModule::on_initialize(System::block_number()); - Scheduler::on_initialize(System::block_number()); - } -} - -#[allow(dead_code)] -pub(crate) fn run_to_block(n: u64) { - run_to_block_ext(n, false) -} - -#[allow(dead_code)] -pub(crate) fn run_to_block_ext(n: u64, enable_events: bool) { - while System::block_number() < n { - Scheduler::on_finalize(System::block_number()); - SubtensorModule::on_finalize(System::block_number()); - System::on_finalize(System::block_number()); - System::set_block_number(System::block_number() + 1); - System::on_initialize(System::block_number()); - if !enable_events { - System::events().iter().for_each(|event| { - log::info!("Event: {:?}", event.event); - }); - System::reset_events(); - } - SubtensorModule::on_initialize(System::block_number()); - Scheduler::on_initialize(System::block_number()); - } -} - -#[allow(dead_code)] -pub(crate) fn next_block_no_epoch(netuid: NetUid) -> u64 { - // high tempo to skip automatic epochs in on_initialize - let high_tempo: u16 = u16::MAX - 1; - let old_tempo: u16 = SubtensorModule::get_tempo(netuid); - - SubtensorModule::set_tempo(netuid, high_tempo); - let new_block = next_block(); - SubtensorModule::set_tempo(netuid, old_tempo); - - new_block -} - -#[allow(dead_code)] -pub(crate) fn run_to_block_no_epoch(netuid: NetUid, n: u64) { - // high tempo to skip automatic epochs in on_initialize - let high_tempo: u16 = u16::MAX - 1; - let old_tempo: u16 = SubtensorModule::get_tempo(netuid); - - SubtensorModule::set_tempo(netuid, high_tempo); - run_to_block(n); - SubtensorModule::set_tempo(netuid, old_tempo); -} - -#[allow(dead_code)] -pub(crate) fn step_epochs(count: u16, netuid: NetUid) { - for _ in 0..count { - let blocks_to_next_epoch = SubtensorModule::blocks_until_next_epoch( - netuid, - SubtensorModule::get_tempo(netuid), - SubtensorModule::get_current_block_as_u64(), - ); - log::info!("Blocks to next epoch: {blocks_to_next_epoch:?}"); - step_block(blocks_to_next_epoch as u16); - - assert!(SubtensorModule::should_run_epoch( - netuid, - SubtensorModule::get_current_block_as_u64() - )); - step_block(1); - } -} - -/// Increments current block by 1, running all hooks associated with doing so, and asserts -/// that the block number was in fact incremented. -/// -/// Returns the new block number. -#[allow(dead_code)] -#[cfg(test)] -pub(crate) fn next_block() -> u64 { - let mut block = System::block_number(); - block += 1; - run_to_block(block); - assert_eq!(System::block_number(), block); - block -} - #[allow(dead_code)] pub fn register_ok_neuron( netuid: NetUid, @@ -743,30 +610,6 @@ pub fn register_ok_neuron( ); } -#[allow(dead_code)] -pub fn add_network(netuid: NetUid, tempo: u16, _modality: u16) { - SubtensorModule::init_new_network(netuid, tempo); - SubtensorModule::set_network_registration_allowed(netuid, true); - SubtensorModule::set_network_pow_registration_allowed(netuid, true); - FirstEmissionBlockNumber::::insert(netuid, 1); - SubtokenEnabled::::insert(netuid, true); -} - -#[allow(dead_code)] -pub fn add_network_without_emission_block(netuid: NetUid, tempo: u16, _modality: u16) { - SubtensorModule::init_new_network(netuid, tempo); - SubtensorModule::set_network_registration_allowed(netuid, true); - SubtensorModule::set_network_pow_registration_allowed(netuid, true); -} - -#[allow(dead_code)] -pub fn add_network_disable_subtoken(netuid: NetUid, tempo: u16, _modality: u16) { - SubtensorModule::init_new_network(netuid, tempo); - SubtensorModule::set_network_registration_allowed(netuid, true); - SubtensorModule::set_network_pow_registration_allowed(netuid, true); - SubtokenEnabled::::insert(netuid, false); -} - #[allow(dead_code)] pub fn add_dynamic_network(hotkey: &U256, coldkey: &U256) -> NetUid { let netuid = SubtensorModule::get_next_netuid(); @@ -784,93 +627,6 @@ pub fn add_dynamic_network(hotkey: &U256, coldkey: &U256) -> NetUid { netuid } -#[allow(dead_code)] -pub fn add_dynamic_network_without_emission_block(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); - netuid -} - -#[allow(dead_code)] -pub fn add_dynamic_network_disable_commit_reveal(hotkey: &U256, coldkey: &U256) -> NetUid { - let netuid = add_dynamic_network(hotkey, coldkey); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); - netuid -} - -#[allow(dead_code)] -pub fn add_network_disable_commit_reveal(netuid: NetUid, tempo: u16, _modality: u16) { - add_network(netuid, tempo, _modality); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, false); -} - -#[allow(dead_code)] -pub fn wait_set_pending_children_cooldown(netuid: NetUid) { - let cooldown = DefaultPendingCooldown::::get(); - step_block(cooldown as u16); // Wait for cooldown to pass - step_epochs(1, netuid); // Run next epoch -} - -#[allow(dead_code)] -pub fn wait_and_set_pending_children(netuid: NetUid) { - let original_block = System::block_number(); - wait_set_pending_children_cooldown(netuid); - SubtensorModule::do_set_pending_children(netuid); - System::set_block_number(original_block); -} - -#[allow(dead_code)] -pub fn mock_schedule_children( - coldkey: &U256, - parent: &U256, - netuid: NetUid, - child_vec: &[(u64, U256)], -) { - // Set minimum stake for setting children - StakeThreshold::::put(0); - - // Set initial parent-child relationship - assert_ok!(SubtensorModule::do_schedule_children( - RuntimeOrigin::signed(*coldkey), - *parent, - netuid, - child_vec.to_vec() - )); -} - -#[allow(dead_code)] -pub fn mock_set_children(coldkey: &U256, parent: &U256, netuid: NetUid, child_vec: &[(u64, U256)]) { - mock_schedule_children(coldkey, parent, netuid, child_vec); - wait_and_set_pending_children(netuid); -} - -#[allow(dead_code)] -pub fn mock_set_children_no_epochs(netuid: NetUid, parent: &U256, child_vec: &[(u64, U256)]) { - let backup_block = SubtensorModule::get_current_block_as_u64(); - PendingChildKeys::::insert(netuid, parent, (child_vec, 0)); - System::set_block_number(1); - SubtensorModule::do_set_pending_children(netuid); - System::set_block_number(backup_block); -} - -// Helper function to wait for the rate limit -#[allow(dead_code)] -pub fn step_rate_limit(transaction_type: &TransactionType, netuid: NetUid) { - // Check rate limit - let limit = transaction_type.rate_limit_on_subnet::(netuid); - - // Step that many blocks - step_block(limit as u16); -} - #[allow(dead_code)] pub(crate) fn remove_stake_rate_limit_for_tests(hotkey: &U256, coldkey: &U256, netuid: NetUid) { StakingOperationRateLimiter::::remove((hotkey, coldkey, netuid)); @@ -881,92 +637,3 @@ pub(crate) fn setup_reserves(netuid: NetUid, tao: TaoCurrency, alpha: AlphaCurre SubnetTAO::::set(netuid, tao); SubnetAlphaIn::::set(netuid, alpha); } - -#[allow(dead_code)] -pub(crate) fn swap_tao_to_alpha(netuid: NetUid, tao: TaoCurrency) -> (AlphaCurrency, u64) { - if netuid.is_root() { - return (tao.to_u64().into(), 0); - } - - let order = GetAlphaForTao::::with_amount(tao); - let result = ::SwapInterface::swap( - netuid.into(), - order, - ::SwapInterface::max_price(), - false, - true, - ); - - assert_ok!(&result); - - let result = result.unwrap(); - - // we don't want to have silent 0 comparisons in tests - assert!(result.amount_paid_out > AlphaCurrency::ZERO); - - (result.amount_paid_out, result.fee_paid.into()) -} - -#[allow(dead_code)] -pub(crate) fn swap_alpha_to_tao_ext( - netuid: NetUid, - alpha: AlphaCurrency, - drop_fees: bool, -) -> (TaoCurrency, u64) { - if netuid.is_root() { - return (alpha.to_u64().into(), 0); - } - - println!( - "::SwapInterface::min_price() = {:?}", - ::SwapInterface::min_price::() - ); - - let order = GetTaoForAlpha::::with_amount(alpha); - let result = ::SwapInterface::swap( - netuid.into(), - order, - ::SwapInterface::min_price(), - drop_fees, - true, - ); - - assert_ok!(&result); - - let result = result.unwrap(); - - // we don't want to have silent 0 comparisons in tests - assert!(!result.amount_paid_out.is_zero()); - - (result.amount_paid_out, result.fee_paid.into()) -} - -#[allow(dead_code)] -pub(crate) fn swap_alpha_to_tao(netuid: NetUid, alpha: AlphaCurrency) -> (TaoCurrency, u64) { - swap_alpha_to_tao_ext(netuid, alpha, false) -} - -#[allow(dead_code)] -pub(crate) fn last_event() -> RuntimeEvent { - System::events().pop().expect("RuntimeEvent expected").event -} - -#[allow(dead_code)] -pub fn assert_last_event( - generic_event: ::RuntimeEvent, -) { - frame_system::Pallet::::assert_last_event(generic_event.into()); -} - -#[allow(dead_code)] -pub fn commit_dummy(who: U256, netuid: NetUid) { - SubtensorModule::set_weights_set_rate_limit(netuid, 0); - - // any 32‑byte value is fine; hash is never opened - let hash = sp_core::H256::from_low_u64_be(0xDEAD_BEEF); - assert_ok!(SubtensorModule::do_commit_weights( - RuntimeOrigin::signed(who), - netuid, - hash - )); -} From 6a607501a194e21639ac3b4559da7de28eb394a2 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Sat, 18 Oct 2025 12:25:37 +0200 Subject: [PATCH 36/47] feat(chain-extensions): add AddProxyV1 function ID and error codes Add FunctionId::AddProxyV1 (ID 13) for proxy management and three new error codes for proxy-specific failures: ProxyTooMany, ProxyDuplicate, and ProxyNoSelfProxy. --- chain-extensions/src/types.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/chain-extensions/src/types.rs b/chain-extensions/src/types.rs index aa7154867..9566247f5 100644 --- a/chain-extensions/src/types.rs +++ b/chain-extensions/src/types.rs @@ -18,6 +18,7 @@ pub enum FunctionId { SwapStakeLimitV1 = 10, RemoveStakeFullLimitV1 = 11, SetColdkeyAutoStakeHotkeyV1 = 12, + AddProxyV1 = 13, } #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug)] @@ -55,6 +56,12 @@ pub enum Output { InsufficientLiquidity = 14, /// Same netuid SameNetuid = 15, + /// Too many proxies registered + ProxyTooMany = 16, + /// Proxy already exists + ProxyDuplicate = 17, + /// Cannot add self as proxy + ProxyNoSelfProxy = 18, } impl From for Output { @@ -78,6 +85,9 @@ impl From for Output { Some("AmountTooLow") => Output::AmountTooLow, Some("InsufficientLiquidity") => Output::InsufficientLiquidity, Some("SameNetuid") => Output::SameNetuid, + Some("TooMany") => Output::ProxyTooMany, + Some("Duplicate") => Output::ProxyDuplicate, + Some("NoSelfProxy") => Output::ProxyNoSelfProxy, _ => Output::RuntimeError, } } From 2d975b76341b396b15cbd2b17ba6e8817c31f3ec Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Sat, 18 Oct 2025 12:25:59 +0200 Subject: [PATCH 37/47] build(chain-extensions): add pallet-subtensor-proxy dependency Add pallet-subtensor-proxy to dependencies for proxy chain extension support. --- chain-extensions/Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chain-extensions/Cargo.toml b/chain-extensions/Cargo.toml index babe461bf..903c82367 100644 --- a/chain-extensions/Cargo.toml +++ b/chain-extensions/Cargo.toml @@ -30,6 +30,7 @@ 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 @@ -59,6 +60,7 @@ std = [ "pallet-timestamp/std", "pallet-crowdloan/std", "pallet-subtensor-utility/std", + "pallet-subtensor-proxy/std", "pallet-drand/std", "subtensor-swap-interface/std", ] From ea31d42720d9e768f9ad38b2df62d74119bd0639 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Sat, 18 Oct 2025 12:26:20 +0200 Subject: [PATCH 38/47] feat(chain-extensions): implement AddProxyV1 chain extension Add AddProxyV1 chain extension handler that calls pallet_proxy::add_proxy with ProxyType::Staking and zero delay, matching precompile behavior. --- chain-extensions/src/lib.rs | 41 ++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/chain-extensions/src/lib.rs b/chain-extensions/src/lib.rs index b25c39e12..d16f484db 100644 --- a/chain-extensions/src/lib.rs +++ b/chain-extensions/src/lib.rs @@ -14,9 +14,11 @@ 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, TaoCurrency}; +use subtensor_runtime_common::{AlphaCurrency, NetUid, ProxyType, TaoCurrency}; #[derive(DebugNoBound)] pub struct SubtensorChainExtension(PhantomData); @@ -29,7 +31,9 @@ impl Default for SubtensorChainExtension { impl ChainExtension for SubtensorChainExtension where - T: pallet_subtensor::Config + pallet_contracts::Config, + T: pallet_subtensor::Config + + pallet_contracts::Config + + pallet_proxy::Config, T::AccountId: Clone, <::Lookup as StaticLookup>::Source: From<::AccountId>, { @@ -48,7 +52,9 @@ where impl SubtensorChainExtension where - T: pallet_subtensor::Config + pallet_contracts::Config, + T: pallet_subtensor::Config + + pallet_contracts::Config + + pallet_proxy::Config, T::AccountId: Clone, { fn dispatch(env: &mut Env) -> Result @@ -434,6 +440,35 @@ where 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) => { From 7e89630ba02aeb8b0eef571c56fcfaa828684a17 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Sat, 18 Oct 2025 12:27:17 +0200 Subject: [PATCH 39/47] test(chain-extensions): add proxy pallet to test runtime Configure pallet_proxy in mock runtime with ProxyType InstanceFilter implementation for chain extension tests. --- chain-extensions/src/mock.rs | 62 ++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs index da0005102..38521df62 100644 --- a/chain-extensions/src/mock.rs +++ b/chain-extensions/src/mock.rs @@ -16,6 +16,7 @@ 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; @@ -42,6 +43,7 @@ frame_support::construct_runtime!( 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, } ); @@ -132,6 +134,57 @@ impl pallet_contracts::Config for Test { 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 { @@ -200,6 +253,15 @@ parameter_types! { 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; From 07243495d09da4229128676a394eb6f6c0149265 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Sat, 18 Oct 2025 12:27:33 +0200 Subject: [PATCH 40/47] test(chain-extensions): add test for AddProxyV1 Add test verifying AddProxyV1 creates proxy relationship with correct delegate, proxy type (Staking), and delay (0). --- chain-extensions/src/tests.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/chain-extensions/src/tests.rs b/chain-extensions/src/tests.rs index a59c4aa5a..f3437f7ce 100644 --- a/chain-extensions/src/tests.rs +++ b/chain-extensions/src/tests.rs @@ -659,6 +659,40 @@ fn unstake_all_alpha_success_moves_stake_to_root() { }); } +#[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); + }); +} + impl MockEnv { fn new(func_id: FunctionId, caller: AccountId, input: Vec) -> Self { Self { From 00dc850edf6346b20d9a917c5b685c67f59d5c38 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Sat, 18 Oct 2025 12:28:00 +0200 Subject: [PATCH 41/47] docs: add AddProxyV1 to chain extension documentation Document AddProxyV1 function (ID 13) and associated error codes in contracts documentation. --- docs/contracts.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/contracts.md b/docs/contracts.md index 5dd6ce45f..28b6ce6e8 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -34,6 +34,7 @@ Subtensor provides a custom chain extension that allows smart contracts to inter | 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 | Example usage in your ink! contract: ```rust @@ -72,6 +73,9 @@ Chain extension functions that modify state return error codes as `u32` values. | 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 | ### Call Filter @@ -79,7 +83,6 @@ For security, contracts can only dispatch a limited set of runtime calls: **Whitelisted Calls:** - `Proxy::proxy` - Execute proxy calls -- `Proxy::add_proxy` - Add a proxy relationship All other runtime calls are restricted and cannot be dispatched from contracts. From 61b55413ae1c3a9a319aea55615f03e45626176e Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Sat, 18 Oct 2025 12:28:25 +0200 Subject: [PATCH 42/47] refactor(runtime): remove add_proxy from contract call filter Remove add_proxy from whitelisted contract calls as it should be called via chain extension instead of direct runtime call. --- runtime/src/lib.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 543a415b5..3ca067414 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1491,10 +1491,7 @@ pub struct ContractCallFilter; impl Contains for ContractCallFilter { fn contains(call: &RuntimeCall) -> bool { match call { - RuntimeCall::Proxy(inner) => matches!( - inner, - pallet_proxy::Call::proxy { .. } | pallet_proxy::Call::add_proxy { .. } - ), + RuntimeCall::Proxy(inner) => matches!(inner, pallet_proxy::Call::proxy { .. }), _ => false, } } From 24e87c6522758a0e14534dad2be5a71afb342e76 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Sat, 18 Oct 2025 12:28:31 +0200 Subject: [PATCH 43/47] chore: update Cargo.lock --- Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.lock b/Cargo.lock index ea4f9c18b..f8e793a4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17970,6 +17970,7 @@ dependencies = [ "pallet-preimage", "pallet-scheduler", "pallet-subtensor", + "pallet-subtensor-proxy", "pallet-subtensor-swap", "pallet-subtensor-utility", "pallet-timestamp", From 73e9387fa0f32651b5e7d455f53bb3a5f11a27bf Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Sat, 18 Oct 2025 12:40:00 +0200 Subject: [PATCH 44/47] feat(chain-extensions): add RemoveProxyV1 function ID and error code Add FunctionId::RemoveProxyV1 (ID 14) and ProxyNotFound error code for proxy removal failures. --- chain-extensions/src/types.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/chain-extensions/src/types.rs b/chain-extensions/src/types.rs index 9566247f5..7c0bfe420 100644 --- a/chain-extensions/src/types.rs +++ b/chain-extensions/src/types.rs @@ -19,6 +19,7 @@ pub enum FunctionId { RemoveStakeFullLimitV1 = 11, SetColdkeyAutoStakeHotkeyV1 = 12, AddProxyV1 = 13, + RemoveProxyV1 = 14, } #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug)] @@ -62,6 +63,8 @@ pub enum Output { ProxyDuplicate = 17, /// Cannot add self as proxy ProxyNoSelfProxy = 18, + /// Proxy relationship not found + ProxyNotFound = 19, } impl From for Output { @@ -88,6 +91,7 @@ impl From for Output { Some("TooMany") => Output::ProxyTooMany, Some("Duplicate") => Output::ProxyDuplicate, Some("NoSelfProxy") => Output::ProxyNoSelfProxy, + Some("NotFound") => Output::ProxyNotFound, _ => Output::RuntimeError, } } From f919287a9d0311cd9eda8581dd65a6416a52c0aa Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Sat, 18 Oct 2025 12:40:31 +0200 Subject: [PATCH 45/47] feat(chain-extensions): implement RemoveProxyV1 chain extension Add RemoveProxyV1 handler that calls pallet_proxy::remove_proxy with ProxyType::Staking and zero delay, matching add_proxy behavior. --- chain-extensions/src/lib.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/chain-extensions/src/lib.rs b/chain-extensions/src/lib.rs index d16f484db..e53fac765 100644 --- a/chain-extensions/src/lib.rs +++ b/chain-extensions/src/lib.rs @@ -469,6 +469,35 @@ where 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) => { From 0a362f2eb1b0dec4648edbbb7164e9bef2f0d27a Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Sat, 18 Oct 2025 12:41:16 +0200 Subject: [PATCH 46/47] test(chain-extensions): add test for RemoveProxyV1 Add test verifying RemoveProxyV1 successfully removes proxy relationship after it was created with AddProxyV1. --- chain-extensions/src/tests.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/chain-extensions/src/tests.rs b/chain-extensions/src/tests.rs index f3437f7ce..f4c301388 100644 --- a/chain-extensions/src/tests.rs +++ b/chain-extensions/src/tests.rs @@ -693,6 +693,33 @@ fn add_proxy_success_creates_proxy_relationship() { }); } +#[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 { From 49b07bdfdb76332f69acfbda901d9091d0f10a0d Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Sat, 18 Oct 2025 12:41:28 +0200 Subject: [PATCH 47/47] docs: add RemoveProxyV1 to chain extension documentation Document RemoveProxyV1 function (ID 14) and ProxyNotFound error code. --- docs/contracts.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/contracts.md b/docs/contracts.md index 28b6ce6e8..e0b4456cf 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -35,6 +35,7 @@ Subtensor provides a custom chain extension that allows smart contracts to inter | 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 @@ -76,6 +77,7 @@ Chain extension functions that modify state return error codes as `u32` values. | 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