diff --git a/.github/workflows/eco-tests-indexer-notify.yml b/.github/workflows/eco-tests-indexer-notify.yml new file mode 100644 index 0000000000..e55bbdf859 --- /dev/null +++ b/.github/workflows/eco-tests-indexer-notify.yml @@ -0,0 +1,140 @@ +name: on eco-tests change notification + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + paths: + - 'eco-tests/**' + +permissions: + contents: read + pull-requests: write + issues: write + +concurrency: + group: eco-tests-indexer-notify-${{ github.ref }} + cancel-in-progress: true + +env: + ECO_TESTS_REVIEWERS: "evgeny-s" + +jobs: + notify: + name: Notify indexer reviewer + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: List changed files under eco-tests/ + id: changes + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + changed=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- 'eco-tests/' || true) + { + echo "files<> "$GITHUB_OUTPUT" + + - name: Post or update sticky review-request comment + if: steps.changes.outputs.files != '' + uses: actions/github-script@v7 + env: + CHANGED_FILES: ${{ steps.changes.outputs.files }} + REVIEWERS: ${{ env.ECO_TESTS_REVIEWERS }} + with: + script: | + const marker = ''; + + const reviewers = (process.env.REVIEWERS || '') + .split(',') + .map(s => s.trim()) + .filter(Boolean); + const ccLine = reviewers.length + ? reviewers.map(u => `@${u}`).join(' ') + : '_(no reviewers configured — set ECO_TESTS_REVIEWERS in the workflow)_'; + + const changed = (process.env.CHANGED_FILES || '').trim(); + const fileList = changed + .split('\n') + .filter(Boolean) + .map(f => `- \`${f}\``) + .join('\n'); + + const body = [ + marker, + '### eco-tests changed — indexer review required', + '', + 'This PR modifies files under `eco-tests/`. and may affect downstream indexing.', + `**cc ${ccLine}** — please review manually`, + '', + '
Changed files', + '', + fileList, + '', + '
', + ].join('\n'); + + const { owner, repo } = context.repo; + const issue_number = context.issue.number; + + const comments = await github.paginate( + github.rest.issues.listComments, + { owner, repo, issue_number, per_page: 100 } + ); + const existing = comments.find(c => c.body && c.body.includes(marker)); + + if (existing) { + if (existing.body !== body) { + await github.rest.issues.updateComment({ + owner, repo, comment_id: existing.id, body, + }); + } + } else { + await github.rest.issues.createComment({ + owner, repo, issue_number, body, + }); + } + + - name: Request reviews from configured reviewers + if: steps.changes.outputs.files != '' + uses: actions/github-script@v7 + env: + REVIEWERS: ${{ env.ECO_TESTS_REVIEWERS }} + with: + script: | + const reviewers = (process.env.REVIEWERS || '') + .split(',') + .map(s => s.trim()) + .filter(Boolean); + if (reviewers.length === 0) { + core.info('ECO_TESTS_REVIEWERS is empty — skipping review request.'); + return; + } + + const { owner, repo } = context.repo; + const pull_number = context.issue.number; + const pr = await github.rest.pulls.get({ owner, repo, pull_number }); + + // GitHub rejects requesting a review from the PR author. + const author = pr.data.user && pr.data.user.login; + const filtered = reviewers.filter(u => u !== author); + if (filtered.length === 0) { + core.info(`All configured reviewers are the PR author (${author}) — skipping.`); + return; + } + + try { + await github.rest.pulls.requestReviewers({ + owner, repo, pull_number, + reviewers: filtered, + }); + } catch (e) { + core.warning(`requestReviewers failed: ${e.message}`); + } diff --git a/Cargo.lock b/Cargo.lock index 32d4c7655d..bcf52ff339 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,6 +71,17 @@ dependencies = [ "subtle 2.6.1", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.12" @@ -497,7 +508,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" dependencies = [ - "ahash", + "ahash 0.8.12", "ark-ff 0.5.0", "ark-poly 0.5.0", "ark-serialize 0.5.0", @@ -732,7 +743,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" dependencies = [ - "ahash", + "ahash 0.8.12", "ark-ff 0.5.0", "ark-serialize 0.5.0", "ark-std 0.5.0", @@ -1669,6 +1680,29 @@ dependencies = [ "piper", ] +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive", + "cfg_aliases 0.2.1", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "bounded-collections" version = "0.1.9" @@ -1955,6 +1989,28 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytemuck" version = "1.24.0" @@ -4085,6 +4141,16 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "endian-cast" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f7a506e5de77a3db9e56fdbed17fa6f3b8d27ede81545dde96107c3d6a1d2" +dependencies = [ + "generic-array 1.3.5", + "typenum", +] + [[package]] name = "enum-as-inner" version = "0.6.1" @@ -5524,6 +5590,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "generic-array" +version = "1.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf57c49a95fd1fe24b90b3033bee6dc7e8f1288d51494cb44e627c295e38542" +dependencies = [ + "rustversion", + "typenum", +] + [[package]] name = "gethostname" version = "0.2.3" @@ -5737,6 +5813,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" @@ -5744,7 +5823,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash", + "ahash 0.8.12", ] [[package]] @@ -5753,7 +5832,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.12", "allocator-api2", "serde", ] @@ -6934,6 +7013,33 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "lencode" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83dc280ed78264020f986b2539e6a44e0720f98f66c99a48a2f52e4a441e99d8" +dependencies = [ + "endian-cast", + "generic-array 1.3.5", + "hashbrown 0.12.3", + "lencode-macros", + "newt-hype", + "ruint", + "zstd-safe 7.2.4", +] + +[[package]] +name = "lencode-macros" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c57df14b9005d1e4e8e56436e922e2c046ad0be55d7cfb062a303714857508" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "libc" version = "0.2.176" @@ -8236,6 +8342,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "newt-hype" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8b7b69b0eafaa88ec8dc9fe7c3860af0a147517e5207cfbd0ecd21cd7cde18" + [[package]] name = "nix" version = "0.26.4" @@ -8416,12 +8528,12 @@ dependencies = [ "pallet-grandpa", "pallet-hotfix-sufficients", "pallet-insecure-randomness-collective-flip", + "pallet-limit-orders", "pallet-multisig", "pallet-nomination-pools", "pallet-nomination-pools-runtime-api", "pallet-offences", "pallet-preimage", - "pallet-registry", "pallet-safe-mode", "pallet-scheduler", "pallet-session", @@ -8459,6 +8571,7 @@ dependencies = [ "sp-genesis-builder", "sp-inherents", "sp-io", + "sp-keyring", "sp-npos-elections", "sp-offchain", "sp-runtime", @@ -9984,6 +10097,28 @@ dependencies = [ "scale-info", ] +[[package]] +name = "pallet-limit-orders" +version = "0.1.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-keyring", + "sp-keystore", + "sp-runtime", + "sp-std", + "substrate-fixed", + "subtensor-macros", + "subtensor-runtime-common", + "subtensor-swap-interface", +] + [[package]] name = "pallet-lottery" version = "41.0.0" @@ -10095,6 +10230,23 @@ dependencies = [ "sp-mmr-primitives", ] +[[package]] +name = "pallet-multi-collective" +version = "1.0.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "impl-trait-for-tuples", + "num-traits", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "subtensor-runtime-common", +] + [[package]] name = "pallet-multisig" version = "41.0.0" @@ -10379,25 +10531,6 @@ dependencies = [ "sp-runtime", ] -[[package]] -name = "pallet-registry" -version = "4.0.0-dev" -dependencies = [ - "enumflags2", - "frame-benchmarking", - "frame-support", - "frame-system", - "pallet-balances", - "parity-scale-codec", - "scale-info", - "sp-core", - "sp-io", - "sp-runtime", - "sp-std", - "subtensor-macros", - "subtensor-runtime-common", -] - [[package]] name = "pallet-remark" version = "41.0.0" @@ -10902,6 +11035,9 @@ dependencies = [ "log", "pallet-subtensor-swap-runtime-api", "parity-scale-codec", + "rand 0.8.5", + "rayon", + "safe-bigmath", "safe-math", "scale-info", "serde", @@ -10938,6 +11074,7 @@ dependencies = [ "frame-support", "parity-scale-codec", "scale-info", + "serde", "sp-api", "sp-std", "subtensor-macros", @@ -13546,6 +13683,26 @@ dependencies = [ "cc", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quanta" version = "0.12.6" @@ -13654,6 +13811,29 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoth" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d9da82a5dc3ff2fb2eee43d2b434fb197a9bf6a2a243850505b61584f888d2" +dependencies = [ + "quoth-macros", + "regex", + "rust_decimal", + "safe-string", +] + +[[package]] +name = "quoth-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58547202bec9896e773db7ef04b4d47c444f9c97bc4386f36e55718c347db440" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "r-efi" version = "5.3.0" @@ -13949,6 +14129,15 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "resolv-conf" version = "0.7.5" @@ -14003,6 +14192,35 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rlp" version = "0.5.2" @@ -14238,6 +14456,22 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" +[[package]] +name = "rust_decimal" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" +dependencies = [ + "arrayvec 0.7.6", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -14493,6 +14727,18 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "safe-bigmath" +version = "0.4.1" +source = "git+https://github.com/sam0x17/safe-bigmath?rev=013c49984910e1c9a23289e8c85e7a856e263a02#013c49984910e1c9a23289e8c85e7a856e263a02" +dependencies = [ + "lencode", + "num-bigint", + "num-integer", + "num-traits", + "quoth", +] + [[package]] name = "safe-math" version = "0.1.0" @@ -14512,6 +14758,12 @@ dependencies = [ "rustc_version 0.2.3", ] +[[package]] +name = "safe-string" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fc51f1e562058dee569383bfdb5a58752bfeb7fa7f0823f5c07c4c45381b5a" + [[package]] name = "safe_arch" version = "0.7.4" @@ -14934,7 +15186,7 @@ name = "sc-consensus-grandpa" version = "0.36.0" source = "git+https://github.com/opentensor/polkadot-sdk.git?rev=7cc54bf2d50ae3921d718736dfeb0de9468539c7#7cc54bf2d50ae3921d718736dfeb0de9468539c7" dependencies = [ - "ahash", + "ahash 0.8.12", "array-bytes 6.2.3", "async-trait", "dyn-clone", @@ -15237,7 +15489,7 @@ name = "sc-network-gossip" version = "0.51.0" source = "git+https://github.com/opentensor/polkadot-sdk.git?rev=7cc54bf2d50ae3921d718736dfeb0de9468539c7#7cc54bf2d50ae3921d718736dfeb0de9468539c7" dependencies = [ - "ahash", + "ahash 0.8.12", "futures", "futures-timer", "log", @@ -15950,7 +16202,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "356285bbf17bea63d9e52e96bd18f039672ac92b55b8cb997d6162a2a37d1649" dependencies = [ - "ahash", + "ahash 0.8.12", "cfg-if", "hashbrown 0.13.2", ] @@ -16014,6 +16266,12 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "sec1" version = "0.7.3" @@ -16454,6 +16712,12 @@ dependencies = [ "wide", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "similar" version = "2.7.0" @@ -17554,7 +17818,7 @@ name = "sp-trie" version = "40.0.0" source = "git+https://github.com/opentensor/polkadot-sdk.git?rev=7cc54bf2d50ae3921d718736dfeb0de9468539c7#7cc54bf2d50ae3921d718736dfeb0de9468539c7" dependencies = [ - "ahash", + "ahash 0.8.12", "foldhash 0.1.5", "hash-db", "hashbrown 0.15.5", @@ -18237,7 +18501,7 @@ dependencies = [ name = "subtensor-macros" version = "0.1.0" dependencies = [ - "ahash", + "ahash 0.8.12", "proc-macro2", "quote", "syn 2.0.106", @@ -18290,6 +18554,7 @@ dependencies = [ "approx", "environmental", "frame-support", + "impl-trait-for-tuples", "num-traits", "parity-scale-codec", "polkadot-runtime-common", @@ -19886,7 +20151,7 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c128c039340ffd50d4195c3f8ce31aac357f06804cfc494c8b9508d4b30dca4" dependencies = [ - "ahash", + "ahash 0.8.12", "hashbrown 0.14.5", "string-interner", ] @@ -21196,11 +21461,18 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "git+https://github.com/gztensor/zstd-safe?rev=42cc34ef6abe5d35d982f6afefb5d7e4e69f5f18#42cc34ef6abe5d35d982f6afefb5d7e4e69f5f18" +dependencies = [ + "zstd-sys", +] + [[package]] name = "zstd-sys" version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +source = "git+https://github.com/gztensor/zstd-sys#01e299b6ce8d08af5a3429f7ceb956f8355cf1aa" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 14ded6a4f9..e66ecc3a06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,14 +57,16 @@ useless_conversion = "allow" # until polkadot is patched pallet-alpha-assets = { path = "pallets/alpha-assets", default-features = false } node-subtensor-runtime = { path = "runtime", default-features = false } pallet-admin-utils = { path = "pallets/admin-utils", default-features = false } +pallet-limit-orders = { path = "pallets/limit-orders", default-features = false } pallet-commitments = { path = "pallets/commitments", default-features = false } -pallet-registry = { path = "pallets/registry", default-features = false } pallet-crowdloan = { path = "pallets/crowdloan", default-features = false } pallet-subtensor = { path = "pallets/subtensor", default-features = false } pallet-subtensor-swap = { path = "pallets/swap", default-features = false } pallet-subtensor-swap-runtime-api = { path = "pallets/swap/runtime-api", default-features = false } pallet-subtensor-swap-rpc = { path = "pallets/swap/rpc", default-features = false } +pallet-multi-collective = { path = "pallets/multi-collective", default-features = false } procedural-fork = { path = "support/procedural-fork", default-features = false } +safe-bigmath = { package = "safe-bigmath", default-features = false, git = "https://github.com/sam0x17/safe-bigmath", rev = "013c49984910e1c9a23289e8c85e7a856e263a02" } safe-math = { path = "primitives/safe-math", default-features = false } share-pool = { path = "primitives/share-pool", default-features = false } subtensor-macros = { path = "support/macros", default-features = false } @@ -72,7 +74,7 @@ subtensor-custom-rpc = { default-features = false, path = "pallets/subtensor/rpc subtensor-custom-rpc-runtime-api = { default-features = false, path = "pallets/subtensor/runtime-api" } 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-swap-interface = { default-features = false, path = "primitives/swap-interface" } subtensor-transaction-fee = { default-features = false, path = "pallets/transaction-fee" } subtensor-chain-extensions = { default-features = false, path = "chain-extensions" } stp-shield = { git = "https://github.com/opentensor/polkadot-sdk.git", rev = "7cc54bf2d50ae3921d718736dfeb0de9468539c7", default-features = false } @@ -87,6 +89,7 @@ enumflags2 = "0.7.9" futures = "0.3.30" hex = { version = "0.4", default-features = false } hex-literal = "0.4.1" +impl-trait-for-tuples = "0.2.3" jsonrpsee = { version = "0.24.9", default-features = false } libsecp256k1 = { version = "0.7.2", default-features = false } lencode = "0.1.6" @@ -117,7 +120,7 @@ toml_edit = "0.22" derive-syn-parse = "0.2" Inflector = "0.11" cfg-expr = "0.15" -itertools = "0.10" +itertools = { version = "0.10", default-features = false } macro_magic = { version = "0.5", default-features = false } frame-support-procedural-tools = { version = "10.0.0", default-features = false } proc-macro-warning = { version = "1", default-features = false } @@ -319,3 +322,5 @@ pow-faucet = [] [patch.crates-io] w3f-bls = { git = "https://github.com/opentensor/bls", branch = "fix-no-std" } +zstd-sys = { git = "https://github.com/gztensor/zstd-sys" } +zstd-safe = { git = "https://github.com/gztensor/zstd-safe", rev = "42cc34ef6abe5d35d982f6afefb5d7e4e69f5f18" } diff --git a/build.rs b/build.rs index 854778873e..f382604525 100644 --- a/build.rs +++ b/build.rs @@ -29,45 +29,52 @@ fn main() { // as we process each Rust file let (tx, rx) = channel(); - // Parse each rust file with syn and run the linting suite on it in parallel - rust_files.par_iter().for_each_with(tx.clone(), |tx, file| { - let is_test = file.display().to_string().contains("test"); - let Ok(content) = fs::read_to_string(file) else { - return; - }; - let Ok(parsed_tokens) = proc_macro2::TokenStream::from_str(&content) else { - return; - }; - let Ok(parsed_file) = syn::parse2::(parsed_tokens) else { - return; - }; + let pool = rayon::ThreadPoolBuilder::new() + .stack_size(64 * 1024 * 1024) + .build() + .expect("build script lint thread pool can be created"); - let track_lint = |result: Result| { - let Err(errors) = result else { + pool.install(|| { + // Parse each rust file with syn and run the linting suite on it in parallel. + rust_files.par_iter().for_each_with(tx.clone(), |tx, file| { + let is_test = file.display().to_string().contains("test"); + let Ok(content) = fs::read_to_string(file) else { return; }; - let relative_path = file.strip_prefix(workspace_root).unwrap_or(file.as_path()); - for error in errors { - let loc = error.span().start(); - let file_path = relative_path.display(); - // note that spans can't go across thread boundaries without losing their location - // info so we we serialize here and send a String - tx.send(format!( - "cargo:warning={}:{}:{}: {}", - file_path, loc.line, loc.column, error, - )) - .unwrap(); - } - }; + let Ok(parsed_tokens) = proc_macro2::TokenStream::from_str(&content) else { + return; + }; + let Ok(parsed_file) = syn::parse2::(parsed_tokens) else { + return; + }; + + let track_lint = |result: Result| { + let Err(errors) = result else { + return; + }; + let relative_path = file.strip_prefix(workspace_root).unwrap_or(file.as_path()); + for error in errors { + let loc = error.span().start(); + let file_path = relative_path.display(); + // note that spans can't go across thread boundaries without losing their location + // info so we we serialize here and send a String + tx.send(format!( + "cargo:warning={}:{}:{}: {}", + file_path, loc.line, loc.column, error, + )) + .unwrap(); + } + }; - track_lint(ForbidAsPrimitiveConversion::lint(&parsed_file)); - track_lint(ForbidKeysRemoveCall::lint(&parsed_file)); - track_lint(RequireFreezeStruct::lint(&parsed_file)); - track_lint(RequireExplicitPalletIndex::lint(&parsed_file)); + track_lint(ForbidAsPrimitiveConversion::lint(&parsed_file)); + track_lint(ForbidKeysRemoveCall::lint(&parsed_file)); + track_lint(RequireFreezeStruct::lint(&parsed_file)); + track_lint(RequireExplicitPalletIndex::lint(&parsed_file)); - if is_test { - track_lint(ForbidSaturatingMath::lint(&parsed_file)); - } + if is_test { + track_lint(ForbidSaturatingMath::lint(&parsed_file)); + } + }); }); // Collect and print all errors after the parallel processing is done diff --git a/chain-extensions/Cargo.toml b/chain-extensions/Cargo.toml index ecc30878b5..74fa71e78a 100644 --- a/chain-extensions/Cargo.toml +++ b/chain-extensions/Cargo.toml @@ -84,5 +84,6 @@ runtime-benchmarks = [ "pallet-subtensor-proxy/runtime-benchmarks", "pallet-subtensor-utility/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", - "subtensor-runtime-common/runtime-benchmarks" + "subtensor-runtime-common/runtime-benchmarks", + "subtensor-swap-interface/runtime-benchmarks" ] diff --git a/chain-extensions/src/lib.rs b/chain-extensions/src/lib.rs index 1aa1a2f966..0e842fe7d1 100644 --- a/chain-extensions/src/lib.rs +++ b/chain-extensions/src/lib.rs @@ -19,7 +19,7 @@ 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 substrate_fixed::types::U96F32; +use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaBalance, NetUid, ProxyType, TaoBalance}; use subtensor_swap_interface::SwapHandler; @@ -719,7 +719,7 @@ where netuid.into(), ); - let price = current_alpha_price.saturating_mul(U96F32::from_num(1_000_000_000)); + let price = current_alpha_price.saturating_mul(U64F64::from_num(1_000_000_000)); let price: u64 = price.saturating_to_num(); let encoded_result = price.encode(); diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs index 37c6d4fb47..a910f7c517 100644 --- a/chain-extensions/src/mock.rs +++ b/chain-extensions/src/mock.rs @@ -8,13 +8,13 @@ use core::num::NonZeroU64; use frame_support::dispatch::DispatchResult; use frame_support::pallet_prelude::Zero; -use frame_support::traits::{Contains, Everything, InherentBuilder, InsideBoth}; +use frame_support::traits::{Contains, Everything, InsideBoth}; use frame_support::weights::Weight; use frame_support::weights::constants::RocksDbWeight; use frame_support::{PalletId, derive_impl}; use frame_support::{assert_ok, parameter_types, traits::PrivilegeCmp}; use frame_system as system; -use frame_system::{EnsureRoot, RawOrigin, limits, offchain::CreateTransactionBase}; +use frame_system::{EnsureRoot, RawOrigin, limits}; use pallet_contracts::HoldReason as ContractsHoldReason; use pallet_subtensor::*; use pallet_subtensor_proxy as pallet_proxy; @@ -26,9 +26,7 @@ use sp_runtime::{ traits::{BlakeTwo256, Convert, IdentityLookup}, }; use sp_std::{cell::RefCell, cmp::Ordering, sync::OnceLock}; -use subtensor_runtime_common::{ - AlphaBalance, AuthorshipInfo, NetUid, Saturating, TaoBalance, Token, -}; +use subtensor_runtime_common::{AlphaBalance, AuthorshipInfo, NetUid, Saturating, TaoBalance}; type Block = frame_system::mocking::MockBlock; @@ -315,6 +313,10 @@ parameter_types! { pub const InitialMaxBurn: u64 = 1_000_000_000; pub const MinBurnUpperBound: TaoBalance = TaoBalance::new(1_000_000_000); // 1 TAO pub const MaxBurnLowerBound: TaoBalance = TaoBalance::new(100_000_000); // 0.1 TAO + pub const MinTempo: u16 = pallet_subtensor::MIN_TEMPO; + pub const MaxTempo: u16 = pallet_subtensor::MAX_TEMPO; + pub const MinActivityCutoffFactorMilli: u32 = pallet_subtensor::MIN_ACTIVITY_CUTOFF_FACTOR_MILLI; + pub const MaxActivityCutoffFactorMilli: u32 = pallet_subtensor::MAX_ACTIVITY_CUTOFF_FACTOR_MILLI; pub const InitialValidatorPruneLen: u64 = 0; pub const InitialScalingLawPower: u16 = 50; pub const InitialMaxAllowedValidators: u16 = 100; @@ -354,6 +356,7 @@ parameter_types! { pub const EvmKeyAssociateRateLimit: u64 = 10; pub const SubtensorPalletId: PalletId = PalletId(*b"subtensr"); pub const BurnAccountId: PalletId = PalletId(*b"burntnsr"); + pub const MaxEpochsPerBlock: u32 = 32; } impl pallet_subtensor::Config for Test { @@ -402,6 +405,10 @@ impl pallet_subtensor::Config for Test { type InitialMinStake = InitialMinStake; type MinBurnUpperBound = MinBurnUpperBound; type MaxBurnLowerBound = MaxBurnLowerBound; + type MinTempo = MinTempo; + type MaxTempo = MaxTempo; + type MinActivityCutoffFactorMilli = MinActivityCutoffFactorMilli; + type MaxActivityCutoffFactorMilli = MaxActivityCutoffFactorMilli; type InitialRAORecycledForRegistration = InitialRAORecycledForRegistration; type InitialNetworkImmunityPeriod = InitialNetworkImmunityPeriod; type InitialNetworkMinLockCost = InitialNetworkMinLockCost; @@ -433,6 +440,7 @@ impl pallet_subtensor::Config for Test { type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; type BurnAccountId = BurnAccountId; + type MaxEpochsPerBlock = MaxEpochsPerBlock; type WeightInfo = (); } @@ -440,7 +448,6 @@ impl pallet_subtensor::Config for Test { 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(); } @@ -452,7 +459,6 @@ impl pallet_subtensor_swap::Config for Test { type TaoReserve = TaoBalanceReserve; type AlphaReserve = AlphaBalanceReserve; type MaxFeeRate = SwapMaxFeeRate; - type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; type MinimumReserve = SwapMinimumReserve; type WeightInfo = (); @@ -620,28 +626,12 @@ where type RuntimeCall = RuntimeCall; } -impl frame_system::offchain::CreateInherent for Test +impl frame_system::offchain::CreateBare 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(), (), ())) + UncheckedExtrinsic::new_bare(call) } } @@ -676,8 +666,7 @@ pub fn register_ok_neuron( // Ensure reserves exist for swap/burn path, but do NOT clobber reserves if the test already set them. let reserve: u64 = 1_000_000_000_000; let tao_reserve = SubnetTAO::::get(netuid); - let alpha_reserve = SubnetAlphaIn::::get(netuid) - .saturating_add(SubnetAlphaInProvided::::get(netuid)); + let alpha_reserve = SubnetAlphaIn::::get(netuid); if tao_reserve.is_zero() && alpha_reserve.is_zero() { setup_reserves(netuid, reserve.into(), reserve.into()); @@ -765,11 +754,6 @@ pub fn add_dynamic_network(hotkey: &U256, coldkey: &U256) -> NetUid { netuid } -#[allow(dead_code)] -pub(crate) fn remove_stake_rate_limit_for_tests(hotkey: &U256, coldkey: &U256, netuid: NetUid) { - StakingOperationRateLimiter::::remove((hotkey, coldkey, netuid)); -} - #[allow(dead_code)] pub(crate) fn setup_reserves(netuid: NetUid, tao: TaoBalance, alpha: AlphaBalance) { SubnetTAO::::set(netuid, tao); diff --git a/chain-extensions/src/tests.rs b/chain-extensions/src/tests.rs index 08a9e17f77..deba3e5bd8 100644 --- a/chain-extensions/src/tests.rs +++ b/chain-extensions/src/tests.rs @@ -12,7 +12,7 @@ use pallet_subtensor::weights::WeightInfo as SubtensorWeightInfo; use sp_core::Get; use sp_core::U256; use sp_runtime::DispatchError; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; use subtensor_swap_interface::SwapHandler; @@ -106,8 +106,6 @@ fn remove_stake_full_limit_success_with_limit_price() { stake_amount_raw.into(), )); - mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::remove_stake_full_limit(); let balance_before = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); @@ -170,8 +168,6 @@ fn swap_stake_limit_with_tight_price_returns_slippage_error() { 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, @@ -241,8 +237,6 @@ fn remove_stake_limit_success_respects_price_limit() { 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, @@ -382,8 +376,6 @@ fn swap_stake_success_moves_between_subnets() { 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, @@ -454,8 +446,6 @@ fn transfer_stake_success_moves_between_coldkeys() { 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, @@ -533,8 +523,6 @@ fn move_stake_success_moves_alpha_between_hotkeys() { 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, @@ -608,8 +596,6 @@ fn unstake_all_alpha_success_moves_stake_to_root() { stake_amount_raw.into(), )); - mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::unstake_all_alpha(); let mut env = MockEnv::new(FunctionId::UnstakeAllAlphaV1, coldkey, hotkey.encode()) @@ -1579,8 +1565,6 @@ fn unstake_all_success_unstakes_balance() { stake_amount_raw.into(), )); - mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::unstake_all(); let pre_balance = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); @@ -1622,7 +1606,7 @@ fn get_alpha_price_returns_encoded_price() { as SwapHandler>::current_alpha_price( netuid.into(), ); - let expected_price_scaled = expected_price.saturating_mul(U96F32::from_num(1_000_000_000)); + let expected_price_scaled = expected_price.saturating_mul(U64F64::from_num(1_000_000_000)); let expected_price_u64: u64 = expected_price_scaled.saturating_to_num(); let mut env = MockEnv::new(FunctionId::GetAlphaPriceV1, caller, netuid.encode()); @@ -1752,8 +1736,6 @@ mod caller_dispatch_tests { stake_amount_raw.into(), )); - mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::unstake_all(); let pre_balance = pallet_subtensor::Pallet::::get_coldkey_balance(&coldkey); @@ -1806,8 +1788,6 @@ mod caller_dispatch_tests { stake_amount_raw.into(), )); - mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::unstake_all_alpha(); let mut env = MockEnv::new( @@ -1870,8 +1850,6 @@ mod caller_dispatch_tests { 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, @@ -1950,8 +1928,6 @@ mod caller_dispatch_tests { 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, @@ -2039,8 +2015,6 @@ mod caller_dispatch_tests { 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, @@ -2171,8 +2145,6 @@ mod caller_dispatch_tests { 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, @@ -2251,8 +2223,6 @@ mod caller_dispatch_tests { 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, @@ -2321,8 +2291,6 @@ mod caller_dispatch_tests { stake_amount_raw.into(), )); - mock::remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); - let expected_weight = <::WeightInfo as SubtensorWeightInfo>::remove_stake_full_limit(); let balance_before = diff --git a/common/Cargo.toml b/common/Cargo.toml index 9fa9bd1856..e225657b8c 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -25,6 +25,7 @@ substrate-fixed.workspace = true subtensor-macros.workspace = true runtime-common.workspace = true approx = { workspace = true, optional = true } +impl-trait-for-tuples.workspace = true [lints] workspace = true diff --git a/common/src/lib.rs b/common/src/lib.rs index ad29f123b2..92095b29b3 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -16,10 +16,12 @@ use subtensor_macros::freeze_struct; pub use currency::*; pub use evm_context::*; +pub use traits::*; pub use transaction_error::*; mod currency; mod evm_context; +mod traits; mod transaction_error; /// Balance of an account. diff --git a/common/src/traits.rs b/common/src/traits.rs new file mode 100644 index 0000000000..349d387fa5 --- /dev/null +++ b/common/src/traits.rs @@ -0,0 +1,34 @@ +use frame_support::pallet_prelude::*; + +/// Handler for when the members of a collective have changed. +pub trait OnMembersChanged { + /// A collective's members have changed, `incoming` members have joined and + /// `outgoing` members have left. + fn on_members_changed( + collective_id: CollectiveId, + incoming: &[AccountId], + outgoing: &[AccountId], + ); + /// Worst-case upper bound on `on_members_changed`'s weight. The + /// implementation is responsible for bounding its own iteration over + /// `incoming`/`outgoing` against the relevant `MaxMembers` constant. + fn weight() -> Weight; +} + +#[impl_trait_for_tuples::impl_for_tuples(10)] +impl OnMembersChanged for Tuple { + fn on_members_changed( + collective_id: CollectiveId, + incoming: &[AccountId], + outgoing: &[AccountId], + ) { + for_tuples!( #( Tuple::on_members_changed(collective_id.clone(), incoming, outgoing); )* ); + } + + fn weight() -> Weight { + #[allow(clippy::let_and_return)] + let mut weight = Weight::zero(); + for_tuples!( #( weight.saturating_accrue(Tuple::weight()); )* ); + weight + } +} diff --git a/contract-tests/src/contracts/precompileWrapper.sol b/contract-tests/src/contracts/precompileWrapper.sol index 9f5fe242c1..57ee067ff5 100644 --- a/contract-tests/src/contracts/precompileWrapper.sol +++ b/contract-tests/src/contracts/precompileWrapper.sol @@ -35,6 +35,7 @@ interface ISubnet { string memory additional ) external payable; function getServingRateLimit(uint16 netuid) external view returns (uint64); + function getNetworkRegistrationBlock(uint16 netuid) external view returns (uint64); } interface INeuron { @@ -223,6 +224,10 @@ contract PrecompileWrapper { return subnet.getServingRateLimit(netuid); } + function getNetworkRegistrationBlock(uint16 netuid) external view returns (uint64) { + return subnet.getNetworkRegistrationBlock(netuid); + } + // ============ Neuron Functions ============ function burnedRegister(uint16 netuid, bytes32 hotkey) external payable { diff --git a/contract-tests/src/contracts/precompileWrapper.ts b/contract-tests/src/contracts/precompileWrapper.ts index 9916b735e9..3e41c5aa02 100644 --- a/contract-tests/src/contracts/precompileWrapper.ts +++ b/contract-tests/src/contracts/precompileWrapper.ts @@ -413,6 +413,25 @@ export const PRECOMPILE_WRAPPER_ABI = [ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "netuid", + "type": "uint16" + } + ], + "name": "getNetworkRegistrationBlock", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -711,4 +730,4 @@ export const PRECOMPILE_WRAPPER_ABI = [ } ]; -export const PRECOMPILE_WRAPPER_BYTECODE = "6080604052348015600e575f5ffd5b50612bb98061001c5f395ff3fe6080604052600436106101d6575f3560e01c80637d691e3011610101578063b1f789ef11610094578063d75e3e0d11610063578063d75e3e0d146106a9578063db1d0fd5146106d3578063ec556889146106fd578063fc6679fb14610727576101d6565b8063b1f789ef146105fd578063bfe252a214610639578063caf2ebf214610663578063cd6f4eb11461068d576101d6565b80639f246f6f116100d05780639f246f6f14610551578063a21762761461058d578063ac3166bf146105b7578063afed65f9146105e1576101d6565b80637d691e30146104815780638bba466c1461049d57806394e3ac6f146104d9578063998538c414610515576101d6565b80634c378a96116101795780635e25f3f8116101485780635e25f3f8146103d157806369e38bc3146103ed57806371214e27146104295780637444dadc14610445576101d6565b80634c378a96146103175780634cf088d9146103415780635b53ddde1461036b5780635b7210c514610395576101d6565b80631f193572116101b55780631f193572146102665780631fc9b141146102a25780633175bd98146102be5780634054ecca146102fb576101d6565b80620ae759146101da5780630494cd9a146102025780630cadeda51461023e575b5f5ffd5b3480156101e5575f5ffd5b5061020060048036038101906101fb91906113ab565b610751565b005b34801561020d575f5ffd5b506102286004803603810190610223919061148d565b6107c1565b60405161023591906114c7565b60405180910390f35b348015610249575f5ffd5b50610264600480360381019061025f9190611519565b610843565b005b348015610271575f5ffd5b5061028c600480360381019061028791906115a0565b6108b4565b60405161029991906115da565b60405180910390f35b6102bc60048036038101906102b79190611626565b610936565b005b3480156102c9575f5ffd5b506102e460048036038101906102df9190611676565b6109a7565b6040516102f29291906116de565b60405180910390f35b61031560048036038101906103109190611705565b610a2f565b005b348015610322575f5ffd5b5061032b610a9d565b604051610338919061179e565b60405180910390f35b34801561034c575f5ffd5b50610355610aa3565b60405161036291906117d7565b60405180910390f35b348015610376575f5ffd5b5061037f610aa9565b60405161038c9190611810565b60405180910390f35b3480156103a0575f5ffd5b506103bb60048036038101906103b69190611676565b610aaf565b6040516103c8919061184b565b60405180910390f35b6103eb60048036038101906103e69190611914565b610b34565b005b3480156103f8575f5ffd5b50610413600480360381019061040e91906115a0565b610bb4565b6040516104209190611a98565b60405180910390f35b610443600480360381019061043e9190611adb565b610c36565b005b348015610450575f5ffd5b5061046b600480360381019061046691906115a0565b610cad565b604051610478919061184b565b60405180910390f35b61049b60048036038101906104969190611626565b610d2f565b005b3480156104a8575f5ffd5b506104c360048036038101906104be9190611b52565b610da0565b6040516104d09190611ca3565b60405180910390f35b3480156104e4575f5ffd5b506104ff60048036038101906104fa9190611cbd565b610e2a565b60405161050c9190611ddf565b60405180910390f35b348015610520575f5ffd5b5061053b60048036038101906105369190611cbd565b610eb0565b6040516105489190611a98565b60405180910390f35b34801561055c575f5ffd5b5061057760048036038101906105729190611cbd565b610f32565b6040516105849190611a98565b60405180910390f35b348015610598575f5ffd5b506105a1610fb4565b6040516105ae9190611e1f565b60405180910390f35b3480156105c2575f5ffd5b506105cb610fba565b6040516105d89190611e58565b60405180910390f35b6105fb60048036038101906105f69190611e9b565b610fc0565b005b348015610608575f5ffd5b50610623600480360381019061061e9190611f38565b61103d565b604051610630919061206c565b60405180910390f35b348015610644575f5ffd5b5061064d6110c9565b60405161065a91906120ac565b60405180910390f35b34801561066e575f5ffd5b506106776110cf565b60405161068491906120e5565b60405180910390f35b6106a760048036038101906106a29190611cbd565b6110d5565b005b3480156106b4575f5ffd5b506106bd611142565b6040516106ca919061211e565b60405180910390f35b3480156106de575f5ffd5b506106e7611148565b6040516106f49190612157565b60405180910390f35b348015610708575f5ffd5b5061071161114e565b60405161071e9190612190565b60405180910390f35b348015610732575f5ffd5b5061073b611154565b60405161074891906121c9565b60405180910390f35b61080b73ffffffffffffffffffffffffffffffffffffffff16620ae7598484846040518463ffffffff1660e01b815260040161078f93929190612299565b5f604051808303815f87803b1580156107a6575f5ffd5b505af11580156107b8573d5f5f3e3d5ffd5b50505050505050565b5f61080c73ffffffffffffffffffffffffffffffffffffffff16630494cd9a836040518263ffffffff1660e01b81526004016107fd91906122eb565b602060405180830381865afa158015610818573d5f5f3e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061083c9190612318565b9050919050565b61080b73ffffffffffffffffffffffffffffffffffffffff16630cadeda58484846040518463ffffffff1660e01b815260040161088293929190612361565b5f604051808303815f87803b158015610899575f5ffd5b505af11580156108ab573d5f5f3e3d5ffd5b50505050505050565b5f61080273ffffffffffffffffffffffffffffffffffffffff16631f193572836040518263ffffffff1660e01b81526004016108f091906115da565b602060405180830381865afa15801561090b573d5f5f3e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061092f91906123aa565b9050919050565b61080573ffffffffffffffffffffffffffffffffffffffff16631fc9b1418484846040518463ffffffff1660e01b8152600401610975939291906123d5565b5f604051808303815f87803b15801561098c575f5ffd5b505af115801561099e573d5f5f3e3d5ffd5b50505050505050565b5f5f61080a73ffffffffffffffffffffffffffffffffffffffff16633175bd9885856040518363ffffffff1660e01b81526004016109e692919061240a565b6040805180830381865afa158015610a00573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610a24919061245b565b915091509250929050565b61080473ffffffffffffffffffffffffffffffffffffffff16634054ecca83836040518363ffffffff1660e01b8152600401610a6c929190612499565b5f604051808303815f87803b158015610a83575f5ffd5b505af1158015610a95573d5f5f3e3d5ffd5b505050505050565b61080481565b61080581565b61080a81565b5f61080973ffffffffffffffffffffffffffffffffffffffff16635b7210c584846040518363ffffffff1660e01b8152600401610aed92919061240a565b602060405180830381865afa158015610b08573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610b2c91906124d4565b905092915050565b61080373ffffffffffffffffffffffffffffffffffffffff16631cf98c6b89898989898989896040518963ffffffff1660e01b8152600401610b7d98979695949392919061255f565b5f604051808303815f87803b158015610b94575f5ffd5b505af1158015610ba6573d5f5f3e3d5ffd5b505050505050505050505050565b5f61080873ffffffffffffffffffffffffffffffffffffffff166369e38bc3836040518263ffffffff1660e01b8152600401610bf091906115da565b602060405180830381865afa158015610c0b573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610c2f9190612620565b9050919050565b61080973ffffffffffffffffffffffffffffffffffffffff1663127e1adb86868686866040518663ffffffff1660e01b8152600401610c7995949392919061264b565b5f604051808303815f87803b158015610c90575f5ffd5b505af1158015610ca2573d5f5f3e3d5ffd5b505050505050505050565b5f61080373ffffffffffffffffffffffffffffffffffffffff16637444dadc836040518263ffffffff1660e01b8152600401610ce991906115da565b602060405180830381865afa158015610d04573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610d2891906124d4565b9050919050565b61080573ffffffffffffffffffffffffffffffffffffffff16637d691e308484846040518463ffffffff1660e01b8152600401610d6e939291906123d5565b5f604051808303815f87803b158015610d85575f5ffd5b505af1158015610d97573d5f5f3e3d5ffd5b50505050505050565b610da861115a565b61080973ffffffffffffffffffffffffffffffffffffffff16638bba466c836040518263ffffffff1660e01b8152600401610de3919061269c565b61016060405180830381865afa158015610dff573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610e2391906127ea565b9050919050565b606061080b73ffffffffffffffffffffffffffffffffffffffff166394e3ac6f836040518263ffffffff1660e01b8152600401610e6791906114c7565b5f60405180830381865afa158015610e81573d5f5f3e3d5ffd5b505050506040513d5f823e3d601f19601f82011682018060405250810190610ea99190612937565b9050919050565b5f61080573ffffffffffffffffffffffffffffffffffffffff1663998538c4836040518263ffffffff1660e01b8152600401610eec91906114c7565b602060405180830381865afa158015610f07573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610f2b9190612620565b9050919050565b5f61080573ffffffffffffffffffffffffffffffffffffffff16639f246f6f836040518263ffffffff1660e01b8152600401610f6e91906114c7565b602060405180830381865afa158015610f89573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610fad9190612620565b9050919050565b61080681565b61080c81565b61080a73ffffffffffffffffffffffffffffffffffffffff1663afed65f9888888888888886040518863ffffffff1660e01b8152600401611007979695949392919061298d565b5f604051808303815f87803b15801561101e575f5ffd5b505af1158015611030573d5f5f3e3d5ffd5b5050505050505050505050565b606061080673ffffffffffffffffffffffffffffffffffffffff1663b1f789ef8585856040518463ffffffff1660e01b815260040161107e939291906129fa565b5f60405180830381865afa158015611098573d5f5f3e3d5ffd5b505050506040513d5f823e3d601f19601f820116820180604052508101906110c09190612b3c565b90509392505050565b61080981565b61080381565b61080073ffffffffffffffffffffffffffffffffffffffff1663cd6f4eb134836040518363ffffffff1660e01b815260040161111191906114c7565b5f604051808303818588803b158015611128575f5ffd5b505af115801561113a573d5f5f3e3d5ffd5b505050505050565b61080081565b61080881565b61080b81565b61080281565b6040518061016001604052805f81526020015f67ffffffffffffffff1681526020015f67ffffffffffffffff1681526020015f63ffffffff1681526020015f67ffffffffffffffff1681526020015f81526020015f67ffffffffffffffff1681526020015f151581526020015f81526020015f151581526020015f63ffffffff1681525090565b5f604051905090565b5f5ffd5b5f5ffd5b5f819050919050565b611204816111f2565b811461120e575f5ffd5b50565b5f8135905061121f816111fb565b92915050565b5f5ffd5b5f601f19601f8301169050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b61126f82611229565b810181811067ffffffffffffffff8211171561128e5761128d611239565b5b80604052505050565b5f6112a06111e1565b90506112ac8282611266565b919050565b5f67ffffffffffffffff8211156112cb576112ca611239565b5b602082029050602081019050919050565b5f5ffd5b5f60ff82169050919050565b6112f5816112e0565b81146112ff575f5ffd5b50565b5f81359050611310816112ec565b92915050565b5f611328611323846112b1565b611297565b9050808382526020820190506020840283018581111561134b5761134a6112dc565b5b835b8181101561137457806113608882611302565b84526020840193505060208101905061134d565b5050509392505050565b5f82601f83011261139257611391611225565b5b81356113a2848260208601611316565b91505092915050565b5f5f5f606084860312156113c2576113c16111ea565b5b5f6113cf86828701611211565b935050602084013567ffffffffffffffff8111156113f0576113ef6111ee565b5b6113fc8682870161137e565b925050604084013567ffffffffffffffff81111561141d5761141c6111ee565b5b6114298682870161137e565b9150509250925092565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f61145c82611433565b9050919050565b61146c81611452565b8114611476575f5ffd5b50565b5f8135905061148781611463565b92915050565b5f602082840312156114a2576114a16111ea565b5b5f6114af84828501611479565b91505092915050565b6114c1816111f2565b82525050565b5f6020820190506114da5f8301846114b8565b92915050565b5f63ffffffff82169050919050565b6114f8816114e0565b8114611502575f5ffd5b50565b5f81359050611513816114ef565b92915050565b5f5f5f606084860312156115305761152f6111ea565b5b5f61153d86828701611211565b935050602061154e86828701611302565b925050604061155f86828701611505565b9150509250925092565b5f61ffff82169050919050565b61157f81611569565b8114611589575f5ffd5b50565b5f8135905061159a81611576565b92915050565b5f602082840312156115b5576115b46111ea565b5b5f6115c28482850161158c565b91505092915050565b6115d481611569565b82525050565b5f6020820190506115ed5f8301846115cb565b92915050565b5f819050919050565b611605816115f3565b811461160f575f5ffd5b50565b5f81359050611620816115fc565b92915050565b5f5f5f6060848603121561163d5761163c6111ea565b5b5f61164a86828701611211565b935050602061165b86828701611612565b925050604061166c86828701611612565b9150509250925092565b5f5f6040838503121561168c5761168b6111ea565b5b5f61169985828601611505565b92505060206116aa85828601611211565b9150509250929050565b5f6fffffffffffffffffffffffffffffffff82169050919050565b6116d8816116b4565b82525050565b5f6040820190506116f15f8301856116cf565b6116fe60208301846116cf565b9392505050565b5f5f6040838503121561171b5761171a6111ea565b5b5f6117288582860161158c565b925050602061173985828601611211565b9150509250929050565b5f819050919050565b5f61176661176161175c84611433565b611743565b611433565b9050919050565b5f6117778261174c565b9050919050565b5f6117888261176d565b9050919050565b6117988161177e565b82525050565b5f6020820190506117b15f83018461178f565b92915050565b5f6117c18261176d565b9050919050565b6117d1816117b7565b82525050565b5f6020820190506117ea5f8301846117c8565b92915050565b5f6117fa8261176d565b9050919050565b61180a816117f0565b82525050565b5f6020820190506118235f830184611801565b92915050565b5f67ffffffffffffffff82169050919050565b61184581611829565b82525050565b5f60208201905061185e5f83018461183c565b92915050565b5f5ffd5b5f67ffffffffffffffff82111561188257611881611239565b5b61188b82611229565b9050602081019050919050565b828183375f83830152505050565b5f6118b86118b384611868565b611297565b9050828152602081018484840111156118d4576118d3611864565b5b6118df848285611898565b509392505050565b5f82601f8301126118fb576118fa611225565b5b813561190b8482602086016118a6565b91505092915050565b5f5f5f5f5f5f5f5f610100898b031215611931576119306111ea565b5b5f61193e8b828c01611211565b985050602089013567ffffffffffffffff81111561195f5761195e6111ee565b5b61196b8b828c016118e7565b975050604089013567ffffffffffffffff81111561198c5761198b6111ee565b5b6119988b828c016118e7565b965050606089013567ffffffffffffffff8111156119b9576119b86111ee565b5b6119c58b828c016118e7565b955050608089013567ffffffffffffffff8111156119e6576119e56111ee565b5b6119f28b828c016118e7565b94505060a089013567ffffffffffffffff811115611a1357611a126111ee565b5b611a1f8b828c016118e7565b93505060c089013567ffffffffffffffff811115611a4057611a3f6111ee565b5b611a4c8b828c016118e7565b92505060e089013567ffffffffffffffff811115611a6d57611a6c6111ee565b5b611a798b828c016118e7565b9150509295985092959890939650565b611a92816115f3565b82525050565b5f602082019050611aab5f830184611a89565b92915050565b611aba81611829565b8114611ac4575f5ffd5b50565b5f81359050611ad581611ab1565b92915050565b5f5f5f5f5f60a08688031215611af457611af36111ea565b5b5f611b0188828901611ac7565b9550506020611b1288828901611ac7565b9450506040611b2388828901611ac7565b9350506060611b3488828901611505565b9250506080611b4588828901611479565b9150509295509295909350565b5f60208284031215611b6757611b666111ea565b5b5f611b7484828501611505565b91505092915050565b611b86816111f2565b82525050565b611b9581611829565b82525050565b611ba4816114e0565b82525050565b5f8115159050919050565b611bbe81611baa565b82525050565b61016082015f820151611bd95f850182611b7d565b506020820151611bec6020850182611b8c565b506040820151611bff6040850182611b8c565b506060820151611c126060850182611b9b565b506080820151611c256080850182611b8c565b5060a0820151611c3860a0850182611b7d565b5060c0820151611c4b60c0850182611b8c565b5060e0820151611c5e60e0850182611bb5565b50610100820151611c73610100850182611b7d565b50610120820151611c88610120850182611bb5565b50610140820151611c9d610140850182611b9b565b50505050565b5f61016082019050611cb75f830184611bc4565b92915050565b5f60208284031215611cd257611cd16111ea565b5b5f611cdf84828501611211565b91505092915050565b5f81519050919050565b5f82825260208201905092915050565b5f819050602082019050919050565b611d1a816115f3565b82525050565b606082015f820151611d345f850182611b7d565b506020820151611d476020850182611d11565b506040820151611d5a6040850182611d11565b50505050565b5f611d6b8383611d20565b60608301905092915050565b5f602082019050919050565b5f611d8d82611ce8565b611d978185611cf2565b9350611da283611d02565b805f5b83811015611dd2578151611db98882611d60565b9750611dc483611d77565b925050600181019050611da5565b5085935050505092915050565b5f6020820190508181035f830152611df78184611d83565b905092915050565b5f611e098261176d565b9050919050565b611e1981611dff565b82525050565b5f602082019050611e325f830184611e10565b92915050565b5f611e428261176d565b9050919050565b611e5281611e38565b82525050565b5f602082019050611e6b5f830184611e49565b92915050565b611e7a81611baa565b8114611e84575f5ffd5b50565b5f81359050611e9581611e71565b92915050565b5f5f5f5f5f5f5f60e0888a031215611eb657611eb56111ea565b5b5f611ec38a828b01611ac7565b9750506020611ed48a828b01611ac7565b9650506040611ee58a828b01611ac7565b9550506060611ef68a828b01611505565b9450506080611f078a828b01611302565b93505060a0611f188a828b01611e87565b92505060c0611f298a828b01611505565b91505092959891949750929550565b5f5f5f60608486031215611f4f57611f4e6111ea565b5b5f611f5c8682870161158c565b9350506020611f6d86828701611479565b9250506040611f7e8682870161158c565b9150509250925092565b5f81519050919050565b5f82825260208201905092915050565b5f819050602082019050919050565b611fba81611569565b82525050565b604082015f820151611fd45f850182611fb1565b506020820151611fe76020850182611b8c565b50505050565b5f611ff88383611fc0565b60408301905092915050565b5f602082019050919050565b5f61201a82611f88565b6120248185611f92565b935061202f83611fa2565b805f5b8381101561205f5781516120468882611fed565b975061205183612004565b925050600181019050612032565b5085935050505092915050565b5f6020820190508181035f8301526120848184612010565b905092915050565b5f6120968261176d565b9050919050565b6120a68161208c565b82525050565b5f6020820190506120bf5f83018461209d565b92915050565b5f6120cf8261176d565b9050919050565b6120df816120c5565b82525050565b5f6020820190506120f85f8301846120d6565b92915050565b5f6121088261176d565b9050919050565b612118816120fe565b82525050565b5f6020820190506121315f83018461210f565b92915050565b5f6121418261176d565b9050919050565b61215181612137565b82525050565b5f60208201905061216a5f830184612148565b92915050565b5f61217a8261176d565b9050919050565b61218a81612170565b82525050565b5f6020820190506121a35f830184612181565b92915050565b5f6121b38261176d565b9050919050565b6121c3816121a9565b82525050565b5f6020820190506121dc5f8301846121ba565b92915050565b5f81519050919050565b5f82825260208201905092915050565b5f819050602082019050919050565b612214816112e0565b82525050565b5f612225838361220b565b60208301905092915050565b5f602082019050919050565b5f612247826121e2565b61225181856121ec565b935061225c836121fc565b805f5b8381101561228c578151612273888261221a565b975061227e83612231565b92505060018101905061225f565b5085935050505092915050565b5f6060820190506122ac5f8301866114b8565b81810360208301526122be818561223d565b905081810360408301526122d2818461223d565b9050949350505050565b6122e581611452565b82525050565b5f6020820190506122fe5f8301846122dc565b92915050565b5f81519050612312816111fb565b92915050565b5f6020828403121561232d5761232c6111ea565b5b5f61233a84828501612304565b91505092915050565b61234c816112e0565b82525050565b61235b816114e0565b82525050565b5f6060820190506123745f8301866114b8565b6123816020830185612343565b61238e6040830184612352565b949350505050565b5f815190506123a481611576565b92915050565b5f602082840312156123bf576123be6111ea565b5b5f6123cc84828501612396565b91505092915050565b5f6060820190506123e85f8301866114b8565b6123f56020830185611a89565b6124026040830184611a89565b949350505050565b5f60408201905061241d5f830185612352565b61242a60208301846114b8565b9392505050565b61243a816116b4565b8114612444575f5ffd5b50565b5f8151905061245581612431565b92915050565b5f5f60408385031215612471576124706111ea565b5b5f61247e85828601612447565b925050602061248f85828601612447565b9150509250929050565b5f6040820190506124ac5f8301856115cb565b6124b960208301846114b8565b9392505050565b5f815190506124ce81611ab1565b92915050565b5f602082840312156124e9576124e86111ea565b5b5f6124f6848285016124c0565b91505092915050565b5f81519050919050565b5f82825260208201905092915050565b8281835e5f83830152505050565b5f612531826124ff565b61253b8185612509565b935061254b818560208601612519565b61255481611229565b840191505092915050565b5f610100820190506125735f83018b6114b8565b8181036020830152612585818a612527565b905081810360408301526125998189612527565b905081810360608301526125ad8188612527565b905081810360808301526125c18187612527565b905081810360a08301526125d58186612527565b905081810360c08301526125e98185612527565b905081810360e08301526125fd8184612527565b90509998505050505050505050565b5f8151905061261a816115fc565b92915050565b5f60208284031215612635576126346111ea565b5b5f6126428482850161260c565b91505092915050565b5f60a08201905061265e5f83018861183c565b61266b602083018761183c565b612678604083018661183c565b6126856060830185612352565b61269260808301846122dc565b9695505050505050565b5f6020820190506126af5f830184612352565b92915050565b5f5ffd5b5f815190506126c7816114ef565b92915050565b5f815190506126db81611e71565b92915050565b5f61016082840312156126f7576126f66126b5565b5b612702610160611297565b90505f61271184828501612304565b5f830152506020612724848285016124c0565b6020830152506040612738848285016124c0565b604083015250606061274c848285016126b9565b6060830152506080612760848285016124c0565b60808301525060a061277484828501612304565b60a08301525060c0612788848285016124c0565b60c08301525060e061279c848285016126cd565b60e0830152506101006127b184828501612304565b610100830152506101206127c7848285016126cd565b610120830152506101406127dd848285016126b9565b6101408301525092915050565b5f6101608284031215612800576127ff6111ea565b5b5f61280d848285016126e1565b91505092915050565b5f67ffffffffffffffff8211156128305761282f611239565b5b602082029050602081019050919050565b5f60608284031215612856576128556126b5565b5b6128606060611297565b90505f61286f84828501612304565b5f8301525060206128828482850161260c565b60208301525060406128968482850161260c565b60408301525092915050565b5f6128b46128af84612816565b611297565b905080838252602082019050606084028301858111156128d7576128d66112dc565b5b835b8181101561290057806128ec8882612841565b8452602084019350506060810190506128d9565b5050509392505050565b5f82601f83011261291e5761291d611225565b5b815161292e8482602086016128a2565b91505092915050565b5f6020828403121561294c5761294b6111ea565b5b5f82015167ffffffffffffffff811115612969576129686111ee565b5b6129758482850161290a565b91505092915050565b61298781611baa565b82525050565b5f60e0820190506129a05f83018a61183c565b6129ad602083018961183c565b6129ba604083018861183c565b6129c76060830187612352565b6129d46080830186612343565b6129e160a083018561297e565b6129ee60c0830184612352565b98975050505050505050565b5f606082019050612a0d5f8301866115cb565b612a1a60208301856122dc565b612a2760408301846115cb565b949350505050565b5f67ffffffffffffffff821115612a4957612a48611239565b5b602082029050602081019050919050565b5f60408284031215612a6f57612a6e6126b5565b5b612a796040611297565b90505f612a8884828501612396565b5f830152506020612a9b848285016124c0565b60208301525092915050565b5f612ab9612ab484612a2f565b611297565b90508083825260208201905060408402830185811115612adc57612adb6112dc565b5b835b81811015612b055780612af18882612a5a565b845260208401935050604081019050612ade565b5050509392505050565b5f82601f830112612b2357612b22611225565b5b8151612b33848260208601612aa7565b91505092915050565b5f60208284031215612b5157612b506111ea565b5b5f82015167ffffffffffffffff811115612b6e57612b6d6111ee565b5b612b7a84828501612b0f565b9150509291505056fea2646970667358221220768c64014d2253c661e44d07f480f7a203eb9e422f680d00272498325a4f6ad964736f6c634300081e0033"; +export const PRECOMPILE_WRAPPER_BYTECODE = "6080604052348015600e575f5ffd5b506119368061001c5f395ff3fe6080604052600436106101da575f3560e01c80638bba466c116100fd578063b1f789ef11610092578063d75e3e0d11610062578063d75e3e0d14610547578063db1d0fd51461055c578063ec55688914610571578063fc6679fb14610586575f5ffd5b8063b1f789ef146104de578063bfe252a21461050a578063caf2ebf21461051f578063cd6f4eb114610534575f5ffd5b8063a2176276116100cd578063a217627614610482578063ac3166bf14610497578063afed65f9146104ac578063b0c751b0146104bf575f5ffd5b80638bba466c146103ec57806394e3ac6f14610418578063998538c4146104445780639f246f6f14610463575f5ffd5b80634cf088d91161017357806369e38bc31161014357806369e38bc31461038857806371214e27146103a75780637444dadc146103ba5780637d691e30146103d9575f5ffd5b80634cf088d9146103145780635b53ddde146103295780635b7210c51461033e5780635e25f3f814610375575f5ffd5b80631fc9b141116101ae5780631fc9b141146102825780633175bd98146102955780634054ecca146102d45780634c378a96146102e7575f5ffd5b80620ae759146101de5780630494cd9a146101ff5780630cadeda5146102315780631f19357214610250575b5f5ffd5b3480156101e9575f5ffd5b506101fd6101f8366004610e85565b61059b565b005b34801561020a575f5ffd5b5061021e610219366004610f06565b6105f4565b6040519081526020015b60405180910390f35b34801561023c575f5ffd5b506101fd61024b366004610f33565b610665565b34801561025b575f5ffd5b5061026f61026a366004610f7f565b6106a0565b60405161ffff9091168152602001610228565b6101fd610290366004610f9a565b610705565b3480156102a0575f5ffd5b506102b46102af366004610fc3565b610739565b604080516001600160801b03938416815292909116602083015201610228565b6101fd6102e2366004610fed565b6107b3565b3480156102f2575f5ffd5b506102fc61080481565b6040516001600160a01b039091168152602001610228565b34801561031f575f5ffd5b506102fc61080581565b348015610334575f5ffd5b506102fc61080a81565b348015610349575f5ffd5b5061035d610358366004610fc3565b6107f7565b6040516001600160401b039091168152602001610228565b6101fd610383366004611074565b61086c565b348015610393575f5ffd5b5061021e6103a2366004610f7f565b6108d6565b6101fd6103b53660046111c2565b610901565b3480156103c5575f5ffd5b5061035d6103d4366004610f7f565b610989565b6101fd6103e7366004610f9a565b6109ef565b3480156103f7575f5ffd5b5061040b61040636600461122b565b610a23565b6040516102289190611246565b348015610423575f5ffd5b50610437610432366004611334565b610add565b604051610228919061134b565b34801561044f575f5ffd5b5061021e61045e366004611334565b610b42565b34801561046e575f5ffd5b5061021e61047d366004611334565b610b6a565b34801561048d575f5ffd5b506102fc61080681565b3480156104a2575f5ffd5b506102fc61080c81565b6101fd6104ba3660046113b6565b610b92565b3480156104ca575f5ffd5b5061035d6104d9366004610f7f565b610c26565b3480156104e9575f5ffd5b506104fd6104f8366004611445565b610c51565b6040516102289190611480565b348015610515575f5ffd5b506102fc61080981565b34801561052a575f5ffd5b506102fc61080381565b6101fd610542366004611334565b610cd8565b348015610552575f5ffd5b506102fc61080081565b348015610567575f5ffd5b506102fc61080881565b34801561057c575f5ffd5b506102fc61080b81565b348015610591575f5ffd5b506102fc61080281565b604051620ae75960e01b815261080b90620ae759906105c29086908690869060040161150d565b5f604051808303815f87803b1580156105d9575f5ffd5b505af11580156105eb573d5f5f3e3d5ffd5b50505050505050565b60405163024a66cd60e11b81526001600160a01b03821660048201525f9061080c90630494cd9a906024015b602060405180830381865afa15801561063b573d5f5f3e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061065f9190611541565b92915050565b604051630cadeda560e01b81526004810184905260ff8316602482015263ffffffff8216604482015261080b90630cadeda5906064016105c2565b604051630f8c9ab960e11b815261ffff821660048201525f9061080290631f19357290602401602060405180830381865afa1580156106e1573d5f5f3e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061065f9190611558565b604051631fc9b14160e01b815260048101849052602481018390526044810182905261080590631fc9b141906064016105c2565b60405163062eb7b360e31b815263ffffffff83166004820152602481018290525f90819061080a90633175bd98906044016040805180830381865afa158015610784573d5f5f3e3d5ffd5b505050506040513d601f19601f820116820180604052508101906107a89190611589565b915091509250929050565b60405163202a766560e11b815261ffff831660048201526024810182905261080490634054ecca9034906044015f604051808303818588803b1580156105d9575f5ffd5b604051635b7210c560e01b815263ffffffff83166004820152602481018290525f9061080990635b7210c590604401602060405180830381865afa158015610841573d5f5f3e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061086591906115c5565b9392505050565b604051631cf98c6b60e01b815261080390631cf98c6b9061089f908b908b908b908b908b908b908b908b9060040161160e565b5f604051808303815f87803b1580156108b6575f5ffd5b505af11580156108c8573d5f5f3e3d5ffd5b505050505050505050505050565b6040516369e38bc360e01b815261ffff821660048201525f90610808906369e38bc390602401610620565b60405163127e1adb60e01b81526001600160401b03808716600483015280861660248301528416604482015263ffffffff831660648201526001600160a01b03821660848201526108099063127e1adb9060a4015f604051808303815f87803b15801561096c575f5ffd5b505af115801561097e573d5f5f3e3d5ffd5b505050505050505050565b604051631d1136b760e21b815261ffff821660048201525f9061080390637444dadc906024015b602060405180830381865afa1580156109cb573d5f5f3e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061065f91906115c5565b6040516307d691e360e41b815260048101849052602481018390526044810182905261080590637d691e30906064016105c2565b60408051610160810182525f80825260208201819052818301819052606082018190526080820181905260a0820181905260c0820181905260e082018190526101008201819052610120820181905261014082015290516322ee919b60e21b815263ffffffff8316600482015261080990638bba466c9060240161016060405180830381865afa158015610ab9573d5f5f3e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061065f91906116c3565b6040516394e3ac6f60e01b81526004810182905260609061080b906394e3ac6f906024015f60405180830381865afa158015610b1b573d5f5f3e3d5ffd5b505050506040513d5f823e601f3d908101601f1916820160405261065f919081019061178a565b6040516326614e3160e21b8152600481018290525f906108059063998538c490602401610620565b604051639f246f6f60e01b8152600481018290525f9061080590639f246f6f90602401610620565b60405163afed65f960e01b81526001600160401b03808916600483015280881660248301528616604482015263ffffffff808616606483015260ff8516608483015283151560a4830152821660c482015261080a9063afed65f99060e4015f604051808303815f87803b158015610c07575f5ffd5b505af1158015610c19573d5f5f3e3d5ffd5b5050505050505050505050565b604051630b0c751b60e41b815261ffff821660048201525f906108039063b0c751b0906024016109b0565b60405163b1f789ef60e01b815261ffff80851660048301526001600160a01b0384166024830152821660448201526060906108069063b1f789ef906064015f60405180830381865afa158015610ca9573d5f5f3e3d5ffd5b505050506040513d5f823e601f3d908101601f19168201604052610cd0919081019061183f565b949350505050565b60405163cd6f4eb160e01b8152600481018290526108009063cd6f4eb19034906024015f604051808303818588803b158015610d12575f5ffd5b505af1158015610d24573d5f5f3e3d5ffd5b505050505050565b634e487b7160e01b5f52604160045260245ffd5b60405161016081016001600160401b0381118282101715610d6357610d63610d2c565b60405290565b604051606081016001600160401b0381118282101715610d6357610d63610d2c565b604080519081016001600160401b0381118282101715610d6357610d63610d2c565b604051601f8201601f191681016001600160401b0381118282101715610dd557610dd5610d2c565b604052919050565b5f6001600160401b03821115610df557610df5610d2c565b5060051b60200190565b803560ff81168114610e0f575f5ffd5b919050565b5f82601f830112610e23575f5ffd5b8135610e36610e3182610ddd565b610dad565b8082825260208201915060208360051b860101925085831115610e57575f5ffd5b602085015b83811015610e7b57610e6d81610dff565b835260209283019201610e5c565b5095945050505050565b5f5f5f60608486031215610e97575f5ffd5b8335925060208401356001600160401b03811115610eb3575f5ffd5b610ebf86828701610e14565b92505060408401356001600160401b03811115610eda575f5ffd5b610ee686828701610e14565b9150509250925092565b80356001600160a01b0381168114610e0f575f5ffd5b5f60208284031215610f16575f5ffd5b61086582610ef0565b63ffffffff81168114610f30575f5ffd5b50565b5f5f5f60608486031215610f45575f5ffd5b83359250610f5560208501610dff565b91506040840135610f6581610f1f565b809150509250925092565b61ffff81168114610f30575f5ffd5b5f60208284031215610f8f575f5ffd5b813561086581610f70565b5f5f5f60608486031215610fac575f5ffd5b505081359360208301359350604090920135919050565b5f5f60408385031215610fd4575f5ffd5b8235610fdf81610f1f565b946020939093013593505050565b5f5f60408385031215610ffe575f5ffd5b8235610fdf81610f70565b5f82601f830112611018575f5ffd5b81356001600160401b0381111561103157611031610d2c565b611044601f8201601f1916602001610dad565b818152846020838601011115611058575f5ffd5b816020850160208301375f918101602001919091529392505050565b5f5f5f5f5f5f5f5f610100898b03121561108c575f5ffd5b8835975060208901356001600160401b038111156110a8575f5ffd5b6110b48b828c01611009565b97505060408901356001600160401b038111156110cf575f5ffd5b6110db8b828c01611009565b96505060608901356001600160401b038111156110f6575f5ffd5b6111028b828c01611009565b95505060808901356001600160401b0381111561111d575f5ffd5b6111298b828c01611009565b94505060a08901356001600160401b03811115611144575f5ffd5b6111508b828c01611009565b93505060c08901356001600160401b0381111561116b575f5ffd5b6111778b828c01611009565b92505060e08901356001600160401b03811115611192575f5ffd5b61119e8b828c01611009565b9150509295985092959890939650565b6001600160401b0381168114610f30575f5ffd5b5f5f5f5f5f60a086880312156111d6575f5ffd5b85356111e1816111ae565b945060208601356111f1816111ae565b93506040860135611201816111ae565b9250606086013561121181610f1f565b915061121f60808701610ef0565b90509295509295909350565b5f6020828403121561123b575f5ffd5b813561086581610f1f565b8151815260208083015161016083019161126a908401826001600160401b03169052565b50604083015161128560408401826001600160401b03169052565b50606083015161129d606084018263ffffffff169052565b5060808301516112b860808401826001600160401b03169052565b5060a083015160a083015260c08301516112dd60c08401826001600160401b03169052565b5060e08301516112f160e084018215159052565b5061010083015161010083015261012083015161131361012084018215159052565b5061014083015161132d61014084018263ffffffff169052565b5092915050565b5f60208284031215611344575f5ffd5b5035919050565b602080825282518282018190525f918401906040840190835b8181101561139e57835180518452602081015160208501526040810151604085015250606083019250602084019350600181019050611364565b509095945050505050565b8015158114610f30575f5ffd5b5f5f5f5f5f5f5f60e0888a0312156113cc575f5ffd5b87356113d7816111ae565b965060208801356113e7816111ae565b955060408801356113f7816111ae565b9450606088013561140781610f1f565b935061141560808901610dff565b925060a0880135611425816113a9565b915060c088013561143581610f1f565b8091505092959891949750929550565b5f5f5f60608486031215611457575f5ffd5b833561146281610f70565b925061147060208501610ef0565b91506040840135610f6581610f70565b602080825282518282018190525f918401906040840190835b8181101561139e578351805161ffff1684526020908101516001600160401b03168185015290930192604090920191600101611499565b5f8151808452602084019350602083015f5b8281101561150357815160ff168652602095860195909101906001016114e2565b5093949350505050565b838152606060208201525f61152560608301856114d0565b828103604084015261153781856114d0565b9695505050505050565b5f60208284031215611551575f5ffd5b5051919050565b5f60208284031215611568575f5ffd5b815161086581610f70565b80516001600160801b0381168114610e0f575f5ffd5b5f5f6040838503121561159a575f5ffd5b6115a383611573565b91506115b160208401611573565b90509250929050565b8051610e0f816111ae565b5f602082840312156115d5575f5ffd5b8151610865816111ae565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b88815261010060208201525f61162861010083018a6115e0565b828103604084015261163a818a6115e0565b9050828103606084015261164e81896115e0565b9050828103608084015261166281886115e0565b905082810360a084015261167681876115e0565b905082810360c084015261168a81866115e0565b905082810360e084015261169e81856115e0565b9b9a5050505050505050505050565b8051610e0f81610f1f565b8051610e0f816113a9565b5f6101608284031280156116d5575f5ffd5b506116de610d40565b825181526116ee602084016115ba565b60208201526116ff604084016115ba565b6040820152611710606084016116ad565b6060820152611721608084016115ba565b608082015260a0838101519082015261173c60c084016115ba565b60c082015261174d60e084016116b8565b60e0820152610100838101519082015261176a61012084016116b8565b61012082015261177d61014084016116ad565b6101408201529392505050565b5f6020828403121561179a575f5ffd5b81516001600160401b038111156117af575f5ffd5b8201601f810184136117bf575f5ffd5b80516117cd610e3182610ddd565b808282526020820191506020606084028501019250868311156117ee575f5ffd5b6020840193505b82841015611537576060848803121561180c575f5ffd5b611814610d69565b84518152602080860151818301526040808701519083015290835260609094019391909101906117f5565b5f6020828403121561184f575f5ffd5b81516001600160401b03811115611864575f5ffd5b8201601f81018413611874575f5ffd5b8051611882610e3182610ddd565b8082825260208201915060208360061b8501019250868311156118a3575f5ffd5b6020840193505b8284101561153757604084880312156118c1575f5ffd5b6118c9610d8b565b84516118d481610f70565b815260208501516118e4816111ae565b80602083015250808352506020820191506040840193506118aa56fea264697066735822122026460b0cf8f5e17c58e4083c1b1155431c8d2cb9962cd9d5f6105ce473df73ee64736f6c63430008230033"; diff --git a/contract-tests/src/contracts/subnet.ts b/contract-tests/src/contracts/subnet.ts index a55bd5030f..dd058dafe4 100644 --- a/contract-tests/src/contracts/subnet.ts +++ b/contract-tests/src/contracts/subnet.ts @@ -291,6 +291,25 @@ export const ISubnetABI = [ stateMutability: "view", type: "function", }, + { + inputs: [ + { + internalType: "uint16", + name: "netuid", + type: "uint16", + }, + ], + name: "getNetworkRegistrationBlock", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, { inputs: [ { diff --git a/contract-tests/test/precompileWrapper.direct-call.test.ts b/contract-tests/test/precompileWrapper.direct-call.test.ts index fa1354f3ce..53bc21c41f 100644 --- a/contract-tests/test/precompileWrapper.direct-call.test.ts +++ b/contract-tests/test/precompileWrapper.direct-call.test.ts @@ -88,6 +88,15 @@ describe("PrecompileWrapper - Direct Call Tests", () => { assert.ok(rateLimitViaWrapper !== undefined, "Rate limit should be not undefined"); }); + it("Should get network registered block via wrapper", async () => { + const onchainValue = await api.query.SubtensorModule.NetworkRegisteredAt.getValue(netuid); + + const valueViaWrapper = Number(await wrapperContract.getNetworkRegistrationBlock(netuid)); + + assert.ok(valueViaWrapper > 0, "Network registered block should be greater than 0"); + assert.equal(valueViaWrapper, onchainValue, "Network registered block should match on-chain value"); + }); + it("Should register network with details via wrapper", async () => { const newHotkey = getRandomSubstrateKeypair(); await forceSetBalanceToSs58Address(api, convertPublicKeyToSs58(newHotkey.publicKey)); diff --git a/eco-tests/Cargo.toml b/eco-tests/Cargo.toml index bf29e738fa..673b7d2f29 100644 --- a/eco-tests/Cargo.toml +++ b/eco-tests/Cargo.toml @@ -36,13 +36,17 @@ pallet-scheduler = { git = "https://github.com/opentensor/polkadot-sdk.git", rev pallet-preimage = { git = "https://github.com/opentensor/polkadot-sdk.git", rev = "7cc54bf2d50ae3921d718736dfeb0de9468539c7", default-features = false, features = ["std"] } pallet-drand = { path = "../pallets/drand", default-features = false, features = ["std"] } pallet-subtensor-swap = { path = "../pallets/swap", default-features = false, features = ["std"] } +pallet-subtensor-swap-runtime-api = { path = "../pallets/swap/runtime-api", default-features = false, features = ["std"] } +subtensor-custom-rpc-runtime-api = { path = "../pallets/subtensor/runtime-api", default-features = false, features = ["std"] } +sp-api = { git = "https://github.com/opentensor/polkadot-sdk.git", rev = "7cc54bf2d50ae3921d718736dfeb0de9468539c7", default-features = false, features = ["std"] } pallet-crowdloan = { path = "../pallets/crowdloan", default-features = false, features = ["std"] } pallet-subtensor-proxy = { path = "../pallets/proxy", default-features = false, features = ["std"] } pallet-subtensor-utility = { path = "../pallets/utility", default-features = false, features = ["std"] } pallet-shield = { path = "../pallets/shield", default-features = false, features = ["std"] } subtensor-runtime-common = { path = "../common", default-features = false, features = ["std"] } -subtensor-swap-interface = { path = "../pallets/swap-interface", default-features = false, features = ["std"] } +subtensor-swap-interface = { path = "../primitives/swap-interface", default-features = false, features = ["std"] } share-pool = { path = "../primitives/share-pool", default-features = false, features = ["std"] } +substrate-fixed = { git = "https://github.com/encointer/substrate-fixed.git", tag = "v0.6.0", default-features = false, features = ["std"] } safe-math = { path = "../primitives/safe-math", default-features = false, features = ["std"] } log = { version = "0.4.21", default-features = false, features = ["std"] } approx = "0.5" diff --git a/eco-tests/src/helpers.rs b/eco-tests/src/helpers.rs index c6fa0ec72d..1696e33634 100644 --- a/eco-tests/src/helpers.rs +++ b/eco-tests/src/helpers.rs @@ -87,9 +87,9 @@ pub fn next_block_no_epoch(netuid: NetUid) -> u64 { let high_tempo: u16 = u16::MAX - 1; let old_tempo: u16 = SubtensorModule::get_tempo(netuid); - SubtensorModule::set_tempo(netuid, high_tempo); + SubtensorModule::set_tempo_unchecked(netuid, high_tempo); let new_block = next_block(); - SubtensorModule::set_tempo(netuid, old_tempo); + SubtensorModule::set_tempo_unchecked(netuid, old_tempo); new_block } @@ -99,14 +99,14 @@ pub fn run_to_block_no_epoch(netuid: NetUid, n: u64) { let high_tempo: u16 = u16::MAX - 1; let old_tempo: u16 = SubtensorModule::get_tempo(netuid); - SubtensorModule::set_tempo(netuid, high_tempo); + SubtensorModule::set_tempo_unchecked(netuid, high_tempo); run_to_block(n); - SubtensorModule::set_tempo(netuid, old_tempo); + SubtensorModule::set_tempo_unchecked(netuid, old_tempo); } pub fn step_epochs(count: u16, netuid: NetUid) { for _ in 0..count { - let blocks_to_next_epoch = SubtensorModule::blocks_until_next_epoch( + let blocks_to_next_epoch = SubtensorModule::blocks_until_next_auto_epoch( netuid, SubtensorModule::get_tempo(netuid), SubtensorModule::get_current_block_as_u64(), @@ -322,10 +322,6 @@ pub fn increase_stake_on_hotkey_account(hotkey: &U256, increment: TaoBalance, ne ); } -pub fn remove_stake_rate_limit_for_tests(hotkey: &U256, coldkey: &U256, netuid: NetUid) { - StakingOperationRateLimiter::::remove((hotkey, coldkey, netuid)); -} - pub fn setup_reserves(netuid: NetUid, tao: TaoBalance, alpha: AlphaBalance) { SubnetTAO::::set(netuid, tao); SubnetAlphaIn::::set(netuid, alpha); diff --git a/eco-tests/src/lib.rs b/eco-tests/src/lib.rs index d7d60aca2b..980de0f3da 100644 --- a/eco-tests/src/lib.rs +++ b/eco-tests/src/lib.rs @@ -4,3 +4,6 @@ mod helpers; mod mock; #[cfg(test)] mod tests; + +#[cfg(test)] +mod tests_taocom_indexer; diff --git a/eco-tests/src/mock.rs b/eco-tests/src/mock.rs index aba98da9b5..e4fc547789 100644 --- a/eco-tests/src/mock.rs +++ b/eco-tests/src/mock.rs @@ -8,13 +8,13 @@ use core::num::NonZeroU64; use frame_support::dispatch::DispatchResult; -use frame_support::traits::{Contains, Everything, InherentBuilder, InsideBoth, InstanceFilter}; +use frame_support::traits::{Contains, Everything, InsideBoth, InstanceFilter}; use frame_support::weights::Weight; use frame_support::weights::constants::RocksDbWeight; use frame_support::{PalletId, derive_impl}; use frame_support::{parameter_types, traits::PrivilegeCmp}; use frame_system as system; -use frame_system::{EnsureRoot, limits, offchain::CreateTransactionBase}; +use frame_system::{EnsureRoot, limits}; use pallet_subtensor::*; use pallet_subtensor_proxy as pallet_proxy; use pallet_subtensor_utility as pallet_utility; @@ -28,7 +28,8 @@ use sp_std::{cell::RefCell, cmp::Ordering, sync::OnceLock}; use sp_tracing::tracing_subscriber; use subtensor_runtime_common::{AuthorshipInfo, NetUid, TaoBalance}; use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; -type Block = frame_system::mocking::MockBlock; +pub type Block = frame_system::mocking::MockBlock; +pub use api_mocks::MockApi; // Configure a mock runtime to test the pallet. frame_support::construct_runtime!( @@ -198,6 +199,12 @@ parameter_types! { pub const InitialMaxBurn: u64 = 1_000_000_000; pub const MinBurnUpperBound: TaoBalance = TaoBalance::new(1_000_000_000); // 1 TAO pub const MaxBurnLowerBound: TaoBalance = TaoBalance::new(100_000_000); // 0.1 TAO + pub const MinTempo: u16 = pallet_subtensor::MIN_TEMPO; + pub const MaxTempo: u16 = pallet_subtensor::MAX_TEMPO; + pub const MinActivityCutoffFactorMilli: u32 = + pallet_subtensor::MIN_ACTIVITY_CUTOFF_FACTOR_MILLI; + pub const MaxActivityCutoffFactorMilli: u32 = + pallet_subtensor::MAX_ACTIVITY_CUTOFF_FACTOR_MILLI; pub const InitialValidatorPruneLen: u64 = 0; pub const InitialScalingLawPower: u16 = 50; pub const InitialMaxAllowedValidators: u16 = 100; @@ -237,6 +244,7 @@ parameter_types! { pub const EvmKeyAssociateRateLimit: u64 = 10; pub const SubtensorPalletId: PalletId = PalletId(*b"subtensr"); pub const BurnAccountId: PalletId = PalletId(*b"burntnsr"); + pub const MaxEpochsPerBlock: u32 = 32; } impl pallet_subtensor::Config for Test { @@ -285,6 +293,10 @@ impl pallet_subtensor::Config for Test { type InitialMinStake = InitialMinStake; type MinBurnUpperBound = MinBurnUpperBound; type MaxBurnLowerBound = MaxBurnLowerBound; + type MinTempo = MinTempo; + type MaxTempo = MaxTempo; + type MinActivityCutoffFactorMilli = MinActivityCutoffFactorMilli; + type MaxActivityCutoffFactorMilli = MaxActivityCutoffFactorMilli; type InitialRAORecycledForRegistration = InitialRAORecycledForRegistration; type InitialNetworkImmunityPeriod = InitialNetworkImmunityPeriod; type InitialNetworkMinLockCost = InitialNetworkMinLockCost; @@ -315,6 +327,7 @@ impl pallet_subtensor::Config for Test { type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; type BurnAccountId = BurnAccountId; + type MaxEpochsPerBlock = MaxEpochsPerBlock; type WeightInfo = (); type AlphaAssets = AlphaAssets; } @@ -323,7 +336,6 @@ impl pallet_subtensor::Config for Test { 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(); } @@ -335,7 +347,6 @@ impl pallet_subtensor_swap::Config for Test { type TaoReserve = TaoBalanceReserve; type AlphaReserve = AlphaBalanceReserve; type MaxFeeRate = SwapMaxFeeRate; - type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; type MinimumReserve = SwapMinimumReserve; type WeightInfo = (); @@ -548,28 +559,12 @@ where type RuntimeCall = RuntimeCall; } -impl frame_system::offchain::CreateInherent for Test +impl frame_system::offchain::CreateBare 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(), (), ())) + UncheckedExtrinsic::new_bare(call) } } @@ -606,3 +601,81 @@ pub fn add_balance_to_coldkey_account(coldkey: &U256, tao: TaoBalance) { let credit = SubtensorModule::mint_tao(tao); let _ = SubtensorModule::spend_tao(coldkey, credit, tao).unwrap(); } + +mod api_mocks { + use codec::Compact; + use pallet_subtensor::rpc_info::delegate_info::DelegateInfo; + use pallet_subtensor::rpc_info::stake_info::StakeInfo; + use pallet_subtensor_swap_runtime_api::{SimSwapResult, SubnetPrice, SwapRuntimeApi}; + use sp_runtime::AccountId32; + use subtensor_custom_rpc_runtime_api::{DelegateInfoRuntimeApi, StakeInfoRuntimeApi}; + use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance}; + + use super::Block; + + pub struct MockApi; + + sp_api::mock_impl_runtime_apis! { + impl DelegateInfoRuntimeApi for MockApi { + fn get_delegates() -> Vec> { Vec::new() } + fn get_delegate(_delegate_account: AccountId32) -> Option> { None } + fn get_delegated( + _delegatee_account: AccountId32, + ) -> Vec<(DelegateInfo, (Compact, Compact))> { + Vec::new() + } + } + + impl StakeInfoRuntimeApi for MockApi { + fn get_stake_info_for_coldkey(_coldkey_account: AccountId32) -> Vec> { + Vec::new() + } + fn get_stake_info_for_coldkeys( + _coldkey_accounts: Vec, + ) -> Vec<(AccountId32, Vec>)> { + Vec::new() + } + fn get_stake_info_for_hotkey_coldkey_netuid( + _hotkey_account: AccountId32, + _coldkey_account: AccountId32, + _netuid: NetUid, + ) -> Option> { + None + } + fn get_stake_fee( + _origin: Option<(AccountId32, NetUid)>, + _origin_coldkey_account: AccountId32, + _destination: Option<(AccountId32, NetUid)>, + _destination_coldkey_account: AccountId32, + _amount: u64, + ) -> u64 { + 0 + } + } + + impl SwapRuntimeApi for MockApi { + fn current_alpha_price(_netuid: NetUid) -> u64 { 0 } + fn current_alpha_price_all() -> Vec { Vec::new() } + fn sim_swap_tao_for_alpha(_netuid: NetUid, _tao: TaoBalance) -> SimSwapResult { + SimSwapResult { + tao_amount: 0u64.into(), + alpha_amount: 0u64.into(), + tao_fee: 0u64.into(), + alpha_fee: 0u64.into(), + tao_slippage: 0u64.into(), + alpha_slippage: 0u64.into(), + } + } + fn sim_swap_alpha_for_tao(_netuid: NetUid, _alpha: AlphaBalance) -> SimSwapResult { + SimSwapResult { + tao_amount: 0u64.into(), + alpha_amount: 0u64.into(), + tao_fee: 0u64.into(), + alpha_fee: 0u64.into(), + tao_slippage: 0u64.into(), + alpha_slippage: 0u64.into(), + } + } + } + } +} diff --git a/eco-tests/src/tests_taocom_indexer.rs b/eco-tests/src/tests_taocom_indexer.rs new file mode 100644 index 0000000000..d79e05f9b2 --- /dev/null +++ b/eco-tests/src/tests_taocom_indexer.rs @@ -0,0 +1,191 @@ +//! Indexer-contract tests for the TAO.com / ecosystem indexer. +//! Any modification in these tests will notify the member responsible +//! for the communication between protocol and the indexer team. + +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] + +use pallet_subtensor::rpc_info::delegate_info::DelegateInfo; +use pallet_subtensor::rpc_info::stake_info::StakeInfo; +use pallet_subtensor::*; +use pallet_subtensor_swap_runtime_api::SwapRuntimeApi; +use share_pool::SafeFloat; +use sp_core::U256; +use sp_runtime::AccountId32; +use sp_runtime::traits::Block as BlockT; +use substrate_fixed::types::{I96F32, U64F64}; +use subtensor_custom_rpc_runtime_api::{DelegateInfoRuntimeApi, StakeInfoRuntimeApi}; +use subtensor_runtime_common::{AlphaBalance, MechId, NetUid, NetUidStorageIndex, TaoBalance}; + +use super::helpers::*; +use super::mock::*; + +#[test] +fn indexer_neuron_per_subnet_vectors() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let netuid_idx = NetUidStorageIndex::from(netuid); + + let _: Vec = Active::::get(netuid); + let _: Vec = Consensus::::get(netuid); + let _: Vec = Dividends::::get(netuid); + let _: Vec = Incentive::::get(netuid_idx); + let _: Vec = LastUpdate::::get(netuid_idx); + let _: Vec = ValidatorPermit::::get(netuid); + let _: Vec = ValidatorTrust::::get(netuid); + let _: Vec = Emission::::get(netuid); + }); +} + +#[test] +fn indexer_neuron_uid_maps() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let netuid_idx = NetUidStorageIndex::from(netuid); + let hotkey = U256::from(1); + let uid: u16 = 0; + + let _: Option = Uids::::get(netuid, hotkey); + let _: U256 = Keys::::get(netuid, uid); + let _: Vec<(u16, u16)> = Weights::::get(netuid_idx, uid); + let _: Vec<(u16, u16)> = Bonds::::get(netuid_idx, uid); + let _: Option = Axons::::get(netuid, hotkey); + }); +} + +#[test] +fn indexer_ownership_and_childkey_graph() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let key = U256::from(42); + + let _: U256 = Owner::::get(key); + let _: U256 = SubnetOwner::::get(netuid); + let _: U256 = SubnetOwnerHotkey::::get(netuid); + let _: Vec<(u64, U256)> = ChildKeys::::get(key, netuid); + let _: Vec<(u64, U256)> = ParentKeys::::get(key, netuid); + }); +} + +#[test] +fn indexer_stake_and_alpha_shares() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let hotkey = U256::from(1); + let coldkey = U256::from(2); + + let _: AlphaBalance = TotalHotkeyAlpha::::get(hotkey, netuid); + let _: U64F64 = TotalHotkeyShares::::get(hotkey, netuid); + let _: SafeFloat = TotalHotkeySharesV2::::get(hotkey, netuid); + let _: U64F64 = Alpha::::get((hotkey, coldkey, netuid)); + let _: SafeFloat = AlphaV2::::get((hotkey, coldkey, netuid)); + }); +} + +#[test] +fn indexer_subnet_metadata() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let coldkey = U256::from(7); + + let _: u16 = TotalNetworks::::get(); + let _: Vec = TokenSymbol::::get(netuid); + let _: Option = IdentitiesV2::::get(coldkey); + let _: Option = SubnetIdentitiesV3::::get(netuid); + let _: MechId = MechanismCountCurrent::::get(netuid); + let _: Option = FirstEmissionBlockNumber::::get(netuid); + }); +} + +#[test] +fn indexer_subnet_pool_and_emissions() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + + let _: I96F32 = SubnetMovingPrice::::get(netuid); + let _: u128 = SubnetVolume::::get(netuid); + let _: TaoBalance = SubnetTAO::::get(netuid); + let _: AlphaBalance = SubnetAlphaIn::::get(netuid); + let _: AlphaBalance = SubnetAlphaOut::::get(netuid); + let _: TaoBalance = SubnetTaoInEmission::::get(netuid); + let _: AlphaBalance = SubnetAlphaInEmission::::get(netuid); + let _: AlphaBalance = SubnetAlphaOutEmission::::get(netuid); + let _: AlphaBalance = PendingValidatorEmission::::get(netuid); + let _: AlphaBalance = PendingServerEmission::::get(netuid); + }); +} + +#[test] +fn indexer_subnet_hyperparams() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + + let _: u16 = Rho::::get(netuid); + let _: u16 = Kappa::::get(netuid); + let _: u16 = ImmunityPeriod::::get(netuid); + let _: u16 = MinAllowedWeights::::get(netuid); + let _: u16 = MaxWeightsLimit::::get(netuid); + let _: u16 = Tempo::::get(netuid); + let _: u64 = MinDifficulty::::get(netuid); + let _: u64 = MaxDifficulty::::get(netuid); + let _: u64 = WeightsVersionKey::::get(netuid); + let _: u64 = WeightsSetRateLimit::::get(netuid); + let _: u16 = AdjustmentInterval::::get(netuid); + let _: u16 = ActivityCutoff::::get(netuid); + let _: bool = NetworkRegistrationAllowed::::get(netuid); + let _: u16 = TargetRegistrationsPerInterval::::get(netuid); + let _: TaoBalance = MinBurn::::get(netuid); + let _: TaoBalance = MaxBurn::::get(netuid); + let _: u64 = BondsMovingAverage::::get(netuid); + let _: u16 = MaxRegistrationsPerBlock::::get(netuid); + let _: u64 = ServingRateLimit::::get(netuid); + let _: u16 = MaxAllowedValidators::::get(netuid); + let _: u64 = Difficulty::::get(netuid); + let _: u64 = AdjustmentAlpha::::get(netuid); + let _: u64 = RevealPeriodEpochs::::get(netuid); + let _: bool = CommitRevealWeightsEnabled::::get(netuid); + let _: bool = LiquidAlphaOn::::get(netuid); + let _: i16 = AlphaSigmoidSteepness::::get(netuid); + let _: bool = Yuma3On::::get(netuid); + let _: bool = BondsResetOn::::get(netuid); + let _: (u16, u16) = AlphaValues::::get(netuid); + let _: RecycleOrBurnEnum = RecycleOrBurn::::get(netuid); + }); +} + +#[test] +fn indexer_step_and_toggles() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + + let _: u64 = BlocksSinceLastStep::::get(netuid); + let _: u64 = LastMechansimStepBlock::::get(netuid); + let _: Option<(RateLimitKey, u64)> = LastRateLimitedBlock::::iter().next(); + let _: bool = TransferToggle::::get(netuid); + }); +} + +#[test] +fn indexer_network_economics() { + new_test_ext(1).execute_with(|| { + let _: TaoBalance = NetworkMinLockCost::::get(); + let _: TaoBalance = NetworkLastLockCost::::get(); + let _: u64 = NetworkLockReductionInterval::::get(); + let _: TaoBalance = TotalIssuance::::get(); + }); +} + +#[test] +fn indexer_runtime_api_signatures() { + let at = ::Hash::default(); + let netuid = NetUid::from(1u16); + let acct = AccountId32::new([0u8; 32]); + + let _: Option> = + DelegateInfoRuntimeApi::get_delegate(&MockApi, at, acct.clone()).unwrap(); + + let _: Vec<(AccountId32, Vec>)> = + StakeInfoRuntimeApi::get_stake_info_for_coldkeys(&MockApi, at, vec![acct.clone()]).unwrap(); + + let _: u64 = SwapRuntimeApi::current_alpha_price(&MockApi, at, netuid).unwrap(); +} diff --git a/node/src/dev_keystore.rs b/node/src/dev_keystore.rs new file mode 100644 index 0000000000..6011021bff --- /dev/null +++ b/node/src/dev_keystore.rs @@ -0,0 +1,56 @@ +use stc_shield::MemoryShieldKeystore; +use stp_shield::{Result as TraitResult, ShieldKeystore}; + +/// A fixed (non-rotating) shield keystore for single-validator dev/manual-seal nodes. +/// +/// Uses the same ML-KEM-768 keypair for both `next_enc_key()` and `current_dec_key()`, +/// bypassing the multi-validator key-rotation timing assumption. In a real multi-validator +/// AURA chain, each validator builds every Kth block (K≥3), so the keystore rolls at the +/// same cadence as the on-chain PendingKey pipeline (2-block delay). In single-validator +/// manual-seal mode the keystore would roll on every block, drifting 2 pairs ahead of +/// PendingKey. This keystore avoids that by keeping both keys from the same generated pair. +/// +/// Construction: capture `next_enc_key()` from a fresh `MemoryShieldKeystore`, roll once +/// so that key becomes current, then freeze. `current_dec_key()` delegates to the inner +/// store (which now holds the matching pair), and `roll_for_next_slot()` is a no-op. +pub struct DevShieldKeystore { + enc_key_bytes: Vec, + inner: MemoryShieldKeystore, +} + +impl DevShieldKeystore { + #[allow(clippy::expect_used)] + pub fn new() -> Self { + let inner = MemoryShieldKeystore::new(); + let enc_key_bytes = inner + .next_enc_key() + .expect("MemoryShieldKeystore always has a next key"); + inner + .roll_for_next_slot() + .expect("initial roll should not fail"); + Self { + enc_key_bytes, + inner, + } + } +} + +impl Default for DevShieldKeystore { + fn default() -> Self { + Self::new() + } +} + +impl ShieldKeystore for DevShieldKeystore { + fn roll_for_next_slot(&self) -> TraitResult<()> { + Ok(()) + } + + fn next_enc_key(&self) -> TraitResult> { + Ok(self.enc_key_bytes.clone()) + } + + fn current_dec_key(&self) -> TraitResult> { + self.inner.current_dec_key() + } +} diff --git a/node/src/lib.rs b/node/src/lib.rs index 4740155f5e..d269fe583d 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -4,6 +4,7 @@ pub mod client; pub mod clone_spec; pub mod conditional_evm_block_import; pub mod consensus; +pub mod dev_keystore; pub mod ethereum; pub mod rpc; pub mod service; diff --git a/node/src/main.rs b/node/src/main.rs index 2766b93054..a6aa15038f 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -10,6 +10,7 @@ mod clone_spec; mod command; mod conditional_evm_block_import; mod consensus; +mod dev_keystore; mod ethereum; mod rpc; mod service; diff --git a/node/src/service.rs b/node/src/service.rs index d07671f81f..624f63b968 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -544,10 +544,14 @@ where .await; if role.is_authority() { - let shield_keystore = Arc::new(MemoryShieldKeystore::new()); - - // manual-seal authorship + // manual-seal authorship — use a fixed keystore so the single-validator dev + // node doesn't drift: MemoryShieldKeystore rolls on every own-block import + // (every block in single-validator mode), advancing current_dec_key() 2 pairs + // ahead of PendingKey on-chain. DevShieldKeystore avoids this by keeping the + // same keypair for both next_enc_key() and current_dec_key(). if let Some(sealing) = sealing { + let dev_shield_keystore: stp_shield::ShieldKeystorePtr = + Arc::new(crate::dev_keystore::DevShieldKeystore::new()); run_manual_seal_authorship( sealing, client, @@ -558,12 +562,14 @@ where prometheus_registry.as_ref(), telemetry.as_ref(), commands_stream, - shield_keystore.clone(), + dev_shield_keystore, )?; log::info!("Manual Seal Ready"); return Ok(task_manager); } + let shield_keystore = Arc::new(MemoryShieldKeystore::new()); + stc_shield::spawn_key_rotation_on_own_import( &task_manager.spawn_handle(), client.clone(), @@ -749,7 +755,7 @@ fn run_manual_seal_authorship( transaction_pool.clone(), prometheus_registry, telemetry.as_ref().map(|x| x.handle()), - shield_keystore, + shield_keystore.clone(), ); thread_local!(static TIMESTAMP: RefCell = const { RefCell::new(0) }); @@ -781,8 +787,15 @@ fn run_manual_seal_authorship( } } - let create_inherent_data_providers = - move |_, ()| async move { Ok(MockTimestampInherentDataProvider) }; + let create_inherent_data_providers = move |_, ()| { + let keystore = shield_keystore.clone(); + async move { + Ok(( + MockTimestampInherentDataProvider, + stc_shield::InherentDataProvider::new(keystore), + )) + } + }; let aura_data_provider = sc_consensus_manual_seal::consensus::aura::AuraConsensusDataProvider::new(client.clone()); diff --git a/pallets/admin-utils/Cargo.toml b/pallets/admin-utils/Cargo.toml index 85236a425a..ae374b4938 100644 --- a/pallets/admin-utils/Cargo.toml +++ b/pallets/admin-utils/Cargo.toml @@ -92,6 +92,7 @@ runtime-benchmarks = [ "pallet-subtensor-swap/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "subtensor-runtime-common/runtime-benchmarks", + "subtensor-swap-interface/runtime-benchmarks", ] try-runtime = [ "frame-support/try-runtime", diff --git a/pallets/admin-utils/src/lib.rs b/pallets/admin-utils/src/lib.rs index f972facca6..ac9f239f3c 100644 --- a/pallets/admin-utils/src/lib.rs +++ b/pallets/admin-utils/src/lib.rs @@ -611,6 +611,9 @@ pub mod pallet { /// The extrinsic sets the activity cutoff for a subnet. /// It is only callable by the root account or subnet owner. /// The extrinsic will call the Subtensor pallet to set the activity cutoff. + // #[deprecated( + // note = "Please use set_activity_cutoff_factor instead. This extrinsic will be removed soon." + // )] #[pallet::call_index(18)] #[pallet::weight(::WeightInfo::sudo_set_activity_cutoff())] pub fn sudo_set_activity_cutoff( @@ -983,7 +986,7 @@ pub mod pallet { pallet_subtensor::Pallet::::if_subnet_exist(netuid), Error::::SubnetDoesNotExist ); - pallet_subtensor::Pallet::::set_tempo(netuid, tempo); + pallet_subtensor::Pallet::::apply_tempo_with_cycle_reset(netuid, tempo); log::debug!("TempoSet( netuid: {netuid:?} tempo: {tempo:?} ) "); Ok(()) } diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index 37b4e06aa5..2534c719f4 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -4,9 +4,9 @@ use core::num::NonZeroU64; use frame_support::{ PalletId, assert_ok, derive_impl, parameter_types, - traits::{Everything, Hooks, InherentBuilder, PrivilegeCmp}, + traits::{Everything, Hooks, PrivilegeCmp}, }; -use frame_system::{self as system, offchain::CreateTransactionBase}; +use frame_system::{self as system}; use frame_system::{EnsureRoot, limits}; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_consensus_grandpa::AuthorityList as GrandpaAuthorityList; @@ -123,6 +123,10 @@ parameter_types! { pub const InitialMaxBurn: TaoBalance = TaoBalance::new(1_000_000_000); pub const MinBurnUpperBound: TaoBalance = TaoBalance::new(1_000_000_000); // 1 TAO pub const MaxBurnLowerBound: TaoBalance = TaoBalance::new(100_000_000); // 0.1 TAO + pub const MinTempo: u16 = pallet_subtensor::MIN_TEMPO; + pub const MaxTempo: u16 = pallet_subtensor::MAX_TEMPO; + pub const MinActivityCutoffFactorMilli: u32 = pallet_subtensor::MIN_ACTIVITY_CUTOFF_FACTOR_MILLI; + pub const MaxActivityCutoffFactorMilli: u32 = pallet_subtensor::MAX_ACTIVITY_CUTOFF_FACTOR_MILLI; pub const InitialValidatorPruneLen: u64 = 0; pub const InitialScalingLawPower: u16 = 50; pub const InitialMaxAllowedValidators: u16 = 100; @@ -161,6 +165,7 @@ parameter_types! { pub const EvmKeyAssociateRateLimit: u64 = 0; pub const SubtensorPalletId: PalletId = PalletId(*b"subtensr"); pub const BurnAccountId: PalletId = PalletId(*b"burntnsr"); + pub const MaxEpochsPerBlock: u32 = 32; } impl pallet_subtensor::Config for Test { @@ -209,6 +214,10 @@ impl pallet_subtensor::Config for Test { type InitialMinStake = InitialMinStake; type MinBurnUpperBound = MinBurnUpperBound; type MaxBurnLowerBound = MaxBurnLowerBound; + type MinTempo = MinTempo; + type MaxTempo = MaxTempo; + type MinActivityCutoffFactorMilli = MinActivityCutoffFactorMilli; + type MaxActivityCutoffFactorMilli = MaxActivityCutoffFactorMilli; type InitialRAORecycledForRegistration = InitialRAORecycledForRegistration; type InitialNetworkImmunityPeriod = InitialNetworkImmunityPeriod; type InitialNetworkMinLockCost = InitialNetworkMinLockCost; @@ -240,6 +249,7 @@ impl pallet_subtensor::Config for Test { type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; type BurnAccountId = BurnAccountId; + type MaxEpochsPerBlock = MaxEpochsPerBlock; type WeightInfo = (); } @@ -343,7 +353,6 @@ impl pallet_alpha_assets::Config for Test {} 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(1_000_000).unwrap(); } @@ -355,7 +364,6 @@ impl pallet_subtensor_swap::Config for Test { type TaoReserve = pallet_subtensor::TaoBalanceReserve; type AlphaReserve = pallet_subtensor::AlphaBalanceReserve; type MaxFeeRate = SwapMaxFeeRate; - type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; type MinimumReserve = SwapMinimumReserve; type WeightInfo = (); @@ -471,28 +479,12 @@ where type RuntimeCall = RuntimeCall; } -impl frame_system::offchain::CreateInherent for Test +impl frame_system::offchain::CreateBare 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, (), ())) + UncheckedExtrinsic::new_bare(call) } } diff --git a/pallets/admin-utils/src/tests/mod.rs b/pallets/admin-utils/src/tests/mod.rs index 7e05acf54b..de49e7651d 100644 --- a/pallets/admin-utils/src/tests/mod.rs +++ b/pallets/admin-utils/src/tests/mod.rs @@ -2042,7 +2042,7 @@ fn test_sudo_set_admin_freeze_window_and_rate() { fn test_freeze_window_blocks_root_and_owner() { new_test_ext().execute_with(|| { let netuid = NetUid::from(1); - let tempo = 10; + let tempo: u16 = 10; // Create subnet with tempo 10 add_network(netuid, tempo); // Set freeze window to 3 blocks @@ -2050,8 +2050,12 @@ fn test_freeze_window_blocks_root_and_owner() { <::RuntimeOrigin>::root(), 3 )); - // Advance to a block where remaining < 3 - run_to_block((tempo - 2).into()); + // Pin the state-based scheduler so the next auto-epoch lands at + // `LastEpochBlock + tempo`. Freeze window covers blocks (next_auto - 3, next_auto]. + pallet_subtensor::LastEpochBlock::::insert(netuid, 0); + let next_auto = tempo as u64; + // Advance to a block inside the freeze window (remaining < 3). + run_to_block(next_auto - 2); // Root should be blocked during freeze window assert_noop!( @@ -2147,7 +2151,7 @@ fn test_owner_hyperparam_update_rate_limit_enforced() { SubnetOwner::::insert(netuid, owner); // Set tempo to 1 so owner hyperparam RL = 2 tempos = 2 blocks - SubtensorModule::set_tempo(netuid, 1); + SubtensorModule::set_tempo_unchecked(netuid, 1); // Disable admin freeze window to avoid blocking on small tempo assert_ok!(AdminUtils::sudo_set_admin_freeze_window( <::RuntimeOrigin>::root(), @@ -2202,7 +2206,7 @@ fn test_hyperparam_rate_limit_enforced_by_tempo() { SubnetOwner::::insert(netuid, owner); // Set tempo to 1 so RL = 2 blocks - SubtensorModule::set_tempo(netuid, 1); + SubtensorModule::set_tempo_unchecked(netuid, 1); // Disable admin freeze window to avoid blocking on small tempo assert_ok!(AdminUtils::sudo_set_admin_freeze_window( <::RuntimeOrigin>::root(), @@ -2250,7 +2254,7 @@ fn test_owner_hyperparam_rate_limit_independent_per_param() { SubnetOwner::::insert(netuid, owner); // Use small tempo to make RL short and deterministic (2 blocks when tempo=1) - SubtensorModule::set_tempo(netuid, 1); + SubtensorModule::set_tempo_unchecked(netuid, 1); // Disable admin freeze window so it doesn't interfere with small tempo assert_ok!(AdminUtils::sudo_set_admin_freeze_window( <::RuntimeOrigin>::root(), diff --git a/pallets/commitments/src/lib.rs b/pallets/commitments/src/lib.rs index 2bbf8ecaf1..b5f28a0968 100644 --- a/pallets/commitments/src/lib.rs +++ b/pallets/commitments/src/lib.rs @@ -406,6 +406,8 @@ impl Pallet { let original_fields = registration.info.fields.clone(); let mut remain_fields = Vec::new(); let mut revealed_fields = Vec::new(); + let mut saw_timelock = false; + let mut processed_timelock = false; for data in original_fields { match data { @@ -413,6 +415,7 @@ impl Pallet { encrypted, reveal_round, } => { + saw_timelock = true; total_weight = total_weight.saturating_add(T::DbWeight::get().reads(1)); let pulse = match pallet_drand::Pulses::::get(reveal_round) { Some(p) => p, @@ -425,6 +428,8 @@ impl Pallet { } }; + processed_timelock = true; + let signature_bytes = pulse .signature .strip_prefix(b"0x") @@ -478,6 +483,29 @@ impl Pallet { } } + if !saw_timelock { + TimelockedIndex::::mutate(|idx| { + idx.remove(&(netuid, who.clone())); + }); + total_weight = total_weight.saturating_add(T::DbWeight::get().reads_writes(1, 1)); + continue; + } + + // Do not rewrite CommitmentOf every block for entries whose reveal round is + // not yet available in the drand pulse storage. The hook has only performed + // the index, commitment, and pulse reads accounted above. + if !processed_timelock { + continue; + } + + let Ok(remaining_fields) = BoundedVec::try_from(remain_fields) else { + log::error!( + "Failed to build BoundedVec for remain_fields; this should be impossible \ + because remain_fields is a subset of the original commitment fields" + ); + continue; + }; + if !revealed_fields.is_empty() { let mut existing_reveals = RevealedCommitments::::get(netuid, &who).unwrap_or_default(); @@ -489,7 +517,6 @@ impl Pallet { // Push newly revealed items onto the tail of existing_reveals and emit the event for revealed_bytes in revealed_fields { existing_reveals.push((revealed_bytes, block_u64)); - Self::deposit_event(Event::CommitmentRevealed { netuid, who: who.clone(), @@ -506,8 +533,7 @@ impl Pallet { total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1)); } - registration.info.fields = BoundedVec::try_from(remain_fields) - .map_err(|_| "Failed to build BoundedVec for remain_fields")?; + registration.info.fields = remaining_fields; match registration.info.fields.is_empty() { true => { @@ -534,7 +560,6 @@ impl Pallet { TimelockedIndex::::mutate(|idx| { idx.remove(&(netuid, who.clone())); }); - total_weight = total_weight.saturating_add(T::DbWeight::get().reads_writes(1, 1)); } diff --git a/pallets/commitments/src/mock.rs b/pallets/commitments/src/mock.rs index 3db0e8f312..10bbf7bbb1 100644 --- a/pallets/commitments/src/mock.rs +++ b/pallets/commitments/src/mock.rs @@ -3,9 +3,8 @@ use crate as pallet_commitments; use frame_support::{ derive_impl, pallet_prelude::{Get, TypeInfo}, - traits::{ConstU32, ConstU64, InherentBuilder}, + traits::{ConstU32, ConstU64}, }; -use frame_system::offchain::CreateTransactionBase; use sp_core::H256; use sp_runtime::{ BuildStorage, @@ -169,37 +168,12 @@ where type RuntimeCall = RuntimeCall; } -impl frame_system::offchain::CreateInherent for Test +impl frame_system::offchain::CreateBare 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 { - // Create a dummy sr25519 signature from a raw byte array - let dummy_raw = [0u8; 64]; - let dummy_signature = sp_core::sr25519::Signature::from(dummy_raw); - let signature = test_crypto::Signature::from(dummy_signature); - Some(UncheckedExtrinsic::new_signed( - call, - nonce.into(), - signature, - (), - )) + UncheckedExtrinsic::new_bare(call) } } diff --git a/pallets/drand/src/lib.rs b/pallets/drand/src/lib.rs index 130f796a90..f9a281038a 100644 --- a/pallets/drand/src/lib.rs +++ b/pallets/drand/src/lib.rs @@ -43,8 +43,7 @@ use codec::Encode; use frame_support::{pallet_prelude::*, traits::Randomness}; use frame_system::{ offchain::{ - AppCrypto, CreateInherent, CreateSignedTransaction, SendUnsignedTransaction, SignedPayload, - Signer, SigningTypes, + AppCrypto, CreateBare, SendUnsignedTransaction, SignedPayload, Signer, SigningTypes, }, pallet_prelude::BlockNumberFor, }; @@ -162,9 +161,7 @@ pub mod pallet { pub struct Pallet(_); #[pallet::config] - pub trait Config: - CreateSignedTransaction> + CreateInherent> + frame_system::Config - { + pub trait Config: CreateBare> + SigningTypes + frame_system::Config { /// The identifier type for an offchain worker. type AuthorityId: AppCrypto; /// something that knows how to verify beacon pulses diff --git a/pallets/drand/src/mock.rs b/pallets/drand/src/mock.rs index 3be3a6a8d1..aa370292b1 100644 --- a/pallets/drand/src/mock.rs +++ b/pallets/drand/src/mock.rs @@ -3,14 +3,14 @@ use crate::verifier::*; use crate::*; use frame_support::{ derive_impl, parameter_types, - traits::{ConstU16, ConstU64, InherentBuilder}, + traits::{ConstU16, ConstU64}, }; use sp_core::{H256, sr25519::Signature}; use sp_keystore::{KeystoreExt, testing::MemoryKeystore}; use sp_runtime::{ BuildStorage, testing::TestXt, - traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, Verify}, + traits::{BlakeTwo256, IdentityLookup, Verify}, }; type Block = frame_system::mocking::MockBlock; @@ -52,7 +52,6 @@ impl frame_system::Config for Test { } type Extrinsic = TestXt; -type AccountId = <::Signer as IdentifyAccount>::AccountId; impl frame_system::offchain::SigningTypes for Test { type Public = ::Signer; @@ -67,28 +66,12 @@ where type Extrinsic = Extrinsic; } -impl frame_system::offchain::CreateInherent for Test +impl frame_system::offchain::CreateBare for Test where RuntimeCall: From, { fn create_bare(call: RuntimeCall) -> Self::Extrinsic { - Extrinsic::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: ::Signer, - _account: AccountId, - nonce: u64, - ) -> Option { - Some(Extrinsic::new_signed(call, nonce, (), ())) + Extrinsic::new_bare(call) } } diff --git a/pallets/limit-orders/Cargo.toml b/pallets/limit-orders/Cargo.toml new file mode 100644 index 0000000000..48ffc61dcb --- /dev/null +++ b/pallets/limit-orders/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "pallet-limit-orders" +version = "0.1.0" +edition.workspace = true + +[dependencies] +codec = { workspace = true, features = ["derive"] } +frame-benchmarking = { workspace = true, optional = true } +sp-io = { workspace = true, optional = true } +sp-keyring = { workspace = true, optional = true } +frame-support.workspace = true +frame-system.workspace = true +scale-info.workspace = true +sp-core.workspace = true +sp-runtime.workspace = true +sp-std.workspace = true +log.workspace = true +substrate-fixed.workspace = true +subtensor-runtime-common.workspace = true +subtensor-macros.workspace = true +subtensor-swap-interface.workspace = true + +[dev-dependencies] +sp-io.workspace = true +sp-keyring.workspace = true +sp-keystore.workspace = true + +[lints] +workspace = true + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "sp-core/std", + "sp-io?/std", + "sp-keyring?/std", + "sp-keystore/std", + "sp-runtime/std", + "sp-std/std", + "log/std", + "substrate-fixed/std", + "subtensor-runtime-common/std", + "subtensor-swap-interface/std", +] + +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "sp-io", + "subtensor-runtime-common/runtime-benchmarks", + "subtensor-swap-interface/runtime-benchmarks", +] \ No newline at end of file diff --git a/pallets/limit-orders/README.md b/pallets/limit-orders/README.md new file mode 100644 index 0000000000..669980739a --- /dev/null +++ b/pallets/limit-orders/README.md @@ -0,0 +1,273 @@ +# pallet-limit-orders + +A FRAME pallet for off-chain signed limit orders on Bittensor subnets. + +Users sign orders off-chain and submit them to a relayer. The relayer batches +orders targeting the same subnet and submits them via `execute_batched_orders`, +which nets the buy and sell sides, executes a single AMM pool swap for the +residual, and distributes outputs pro-rata to all participants. This minimises +price impact compared to executing each order independently against the pool. + +MEV protection is available for free: any caller can wrap `execute_orders` or +`execute_batched_orders` inside `pallet_shield::submit_encrypted` to hide the +batch contents from the mempool until the block is proposed. + +--- + +## Order lifecycle + +``` +User signs VersionedOrder::V1(Order) off-chain + │ + ▼ +Relayer submits via execute_orders Relayer submits via execute_batched_orders + (one-by-one, best-effort) (aggregated, atomic) + │ │ + ├─ Invalid / expired / ├─ Any order invalid / expired / + │ price-not-met → │ price-not-met / root netuid → + │ skipped, emits OrderSkipped │ entire batch fails (DispatchError) + │ with DispatchError reason │ + │ │ + └─ Valid → executed └─ All orders valid → net pool swap + │ → distribute pro-rata + └─ order_id written to Orders as Fulfilled + (prevents replay) + +User can cancel at any time via cancel_order + └─ order_id written to Orders as Cancelled +``` + +--- + +## Data structures + +### `VersionedOrder` + +Versioned wrapper around an order payload. Currently has one variant: + +| Variant | Description | +|---------|-------------| +| `V1(Order)` | First version of the order schema. | + +Versioning lets the pallet accept orders signed against different schemas +simultaneously. When a new variant is added (`V2`, etc.), old `V1` signed orders +remain valid because the `OrderId` and signature both cover the full +`VersionedOrder` encoding (including the version discriminant byte). + +### `Order` + +The payload that a user signs off-chain, wrapped inside `VersionedOrder`. Never +stored in full on-chain — only the `blake2_256` hash of the `VersionedOrder` +encoding (`OrderId`) is persisted. + +| Field | Type | Description | +|-----------------|-------------|-------------| +| `signer` | `AccountId` | Coldkey that authorises the order. For buy types: pays TAO. For sell types: owns the staked alpha. | +| `hotkey` | `AccountId` | Hotkey to stake to (buy types) or unstake from (sell types). | +| `netuid` | `NetUid` | Target subnet. | +| `order_type` | `OrderType` | One of `LimitBuy`, `TakeProfit`, or `StopLoss` (see table below). | +| `amount` | `u64` | Input amount in raw units. TAO for buy types; alpha for sell types. | +| `limit_price` | `u64` | Price threshold in TAO/alpha raw units. Trigger direction depends on `OrderType` (see table below). | +| `expiry` | `u64` | Unix timestamp in milliseconds. Order must not execute after this time. | +| `fee_rate` | `Perbill` | Per-order fee as a fraction of the input amount. `Perbill::zero()` = no fee. | +| `fee_recipient` | `AccountId` | Account that receives the fee collected for this order. | +| `relayer` | `Option` | If `Some`, restricts execution to a single designated relayer account. Any attempt by a different account to execute this order is rejected with `RelayerMissMatch`. `None` = any relayer may execute. | +| `max_slippage` | `Option` | Maximum acceptable slippage in parts per billion applied to `limit_price` at swap time. `None` = no slippage protection (execute at market). When `Some(p)`: Buy ceiling = `limit_price + limit_price * p`; Sell floor = `limit_price - limit_price * p`. Both saturate at `u64` bounds. | + +### `OrderType` + +| Variant | Action | Triggers when | Use case | +|--------------|---------------|-------------------------|----------| +| `LimitBuy` | Buy alpha | price ≤ `limit_price` | Enter a position at or below a target price. | +| `TakeProfit` | Sell alpha | price ≥ `limit_price` | Exit a position once price rises to a profit target. | +| `StopLoss` | Sell alpha | price ≤ `limit_price` | Exit a position to limit downside if price falls to a floor. | + +### `SignedOrder` + +Envelope submitted by the relayer: the `VersionedOrder` payload plus the user's +sr25519 signature over the SCALE encoding of the `VersionedOrder` (including the +version discriminant). Only sr25519 signatures are accepted. Signature +verification uses the inner `order.signer` as the expected public key. + +### `OrderStatus` + +Terminal state of a processed order, stored under its `OrderId`. + +| Variant | Meaning | +|-------------|---------| +| `Fulfilled` | Order was successfully executed. | +| `Cancelled` | User registered a cancellation intent before execution. | + +--- + +## Storage + +### `Orders: StorageMap` + +Maps an `OrderId` (blake2_256 of the SCALE-encoded `VersionedOrder`) to its +terminal `OrderStatus`. Absence means the order has never been seen and is still +executable (provided it is valid). Presence means it is permanently closed — +neither `Fulfilled` nor `Cancelled` orders can be re-executed. + +--- + +## Config + +| Item | Type | Description | +|-----------------------|---------------------------------------------------|-------------| +| `SwapInterface` | `OrderSwapInterface` | Full swap + balance execution interface. Implemented by `pallet_subtensor::Pallet`. Provides `buy_alpha`, `sell_alpha`, `transfer_tao`, `transfer_staked_alpha`, and `current_alpha_price`. | +| `TimeProvider` | `UnixTime` | Current wall-clock time for expiry checks. | +| `MaxOrdersPerBatch` | `Get` (constant) | Maximum number of orders accepted in a single `execute_orders` or `execute_batched_orders` call. Should equal `floor(max_block_weight / per_order_weight)`. | +| `PalletId` | `Get` (constant) | Used to derive the pallet intermediary account (`PalletId::into_account_truncating`). This account temporarily holds pooled TAO and staked alpha during `execute_batched_orders`. | +| `PalletHotkey` | `Get` (constant) | Hotkey the pallet intermediary account stakes to/from during batch execution. Must be a dedicated hotkey registered on every subnet the pallet may operate on. Operators should register it as a non-validator neuron. | +| `WeightInfo` | `weights::WeightInfo` | Benchmarked weight functions for each extrinsic. Use `weights::SubstrateWeight` in production and `()` in tests. | + +--- + +## Extrinsics + +### `execute_orders(orders)` — call index 0 + +**Origin:** any signed account (typically a relayer). + +Executes a list of signed limit orders one by one, each interacting with the +AMM pool independently. Orders that fail validation or whose price condition is +not met are silently skipped — a single bad order does not revert the batch. + +**Fee handling:** each order's `fee_rate` is deducted from the input amount and +forwarded to that order's `fee_recipient` after execution. + +**When to use:** suitable for small batches or when orders target different +subnets. Use `execute_batched_orders` for same-subnet batches to reduce price +impact. + +--- + +### `execute_batched_orders(netuid, orders)` — call index 1 + +**Origin:** any signed account (typically a relayer). + +Aggregates all valid orders targeting `netuid` into a single net pool +interaction: + +1. **Validate & classify** — if any order has the wrong netuid, an invalid + signature, an already-processed id, a past expiry, a price condition not met, + or targets the root netuid (0), the **entire call fails** with the + corresponding error. All orders must be valid for execution to proceed. Valid + orders are split into buy-side (`LimitBuy`) and sell-side (`TakeProfit`, + `StopLoss`) groups. For buy orders the net TAO (after fee) is pre-computed + here. Each order's `effective_swap_limit` (derived from `limit_price` and + `max_slippage`) is computed and stored for use in the pool swap. + +2. **Collect assets** — gross TAO is pulled from each buyer's free balance into + the pallet intermediary account. Gross alpha stake is moved from each seller's + `(coldkey, hotkey)` position to the pallet intermediary's `(pallet_account, + pallet_hotkey)` position. + +3. **Net pool swap** — buy TAO and sell alpha are converted to a common TAO + basis at the current spot price and offset against each other. Only the + residual amount touches the pool in a single swap: + - Buy-dominant: residual TAO is sent to the pool; pool returns alpha. Price ceiling = `min(effective_swap_limit)` across all buy orders. + - Sell-dominant: residual alpha is sent to the pool; pool returns TAO. Price floor = `max(effective_swap_limit)` across all sell orders. + - Perfectly offset: no pool interaction. + +4. **Distribute alpha pro-rata** — every buyer receives their share of the total + available alpha (pool output + seller passthrough alpha). Share is + proportional to each buyer's net TAO contribution. Integer division floors + each share; any remainder stays in the pallet intermediary account as dust. + +5. **Distribute TAO pro-rata** — every seller receives their share of the total + available TAO (pool output + buyer passthrough TAO), minus their order's + fee. Share is proportional to each seller's alpha valued at the current spot + price. Integer division floors each share; any remainder stays in the pallet + intermediary account as dust. + +6. **Collect fees** — buy-side fees (withheld from each order's TAO input) and + sell-side fees (withheld from each order's TAO output) are accumulated per + unique `fee_recipient` and forwarded in a single transfer per recipient. + +7. **Emit `GroupExecutionSummary`.** + +> **Note:** rounding dust (alpha and TAO) accumulates in the pallet intermediary +> account between batches. If an emission epoch fires while dust is present, the +> pallet earns emissions it never distributes. + +--- + +### `cancel_order(order)` — call index 2 + +**Origin:** the order's `signer` (coldkey). + +Registers a cancellation intent by writing the `OrderId` into `Orders` as +`Cancelled`. Once cancelled an order can never be executed. The full +`VersionedOrder` payload is required so the pallet can derive the `OrderId`. + +--- + +## Events + +| Event | Fields | Emitted when | +|-------|--------|--------------| +| `OrderExecuted` | `order_id`, `signer`, `netuid`, `side` | An individual order was successfully executed (by either extrinsic). | +| `OrderSkipped` | `order_id`, `reason` | An order was skipped by `execute_orders` (bad signature, expired, wrong netuid, already processed, price condition not met, or root netuid). `reason` is the `DispatchError` that caused the skip. Not emitted by `execute_batched_orders` — invalid orders there cause the whole call to fail. | +| `OrderCancelled` | `order_id`, `signer` | The signer registered a cancellation via `cancel_order`. | +| `GroupExecutionSummary` | `netuid`, `net_side`, `net_amount`, `actual_out`, `executed_count` | Emitted once per `execute_batched_orders` call summarising the net pool trade. `net_side` is `Buy` if TAO was sent to the pool, `Sell` if alpha was sent. `net_amount` and `actual_out` are zero when the two sides perfectly offset. | + +--- + +## Errors + +| Error | Cause | +|-------|-------| +| `InvalidSignature` | Signature does not match the order payload and signer. Also used as a catch-all for failed validation in `execute_orders`. | +| `OrderAlreadyProcessed` | The `OrderId` is already present in `Orders` (either `Fulfilled` or `Cancelled`). | +| `OrderExpired` | `now > order.expiry`. Only returned as a hard error by `execute_batched_orders`; silently skipped in `execute_orders`. | +| `PriceConditionNotMet` | Current spot price is beyond the order's `limit_price`. Only returned as a hard error by `execute_batched_orders`; silently skipped in `execute_orders`. | +| `OrderNetUidMismatch` | An order inside a `execute_batched_orders` call targets a different netuid than the batch parameter. | +| `RootNetUidNotAllowed` | The order or batch targets netuid 0 (root). Root uses a fixed 1:1 stable mechanism with no AMM — limit orders are not meaningful there. | +| `Unauthorized` | Caller of `cancel_order` is not the order's `signer`. | +| `SwapReturnedZero` | The pool swap returned zero output for a non-zero residual input. | +| `RelayerMissMatch` | The caller is not the relayer designated in the order's `relayer` field. Only raised when the field is `Some`. | + +--- + +## Fee model + +Fees are specified per-order via `fee_rate: Perbill` and `fee_recipient: +AccountId` fields on the `Order` struct. There is no global protocol fee or +admin key. + +All fees are collected in TAO regardless of order side. + +| Order type | Fee deducted from | Timing | +|-------------------------|-------------------|--------| +| `LimitBuy` | TAO input | Pre-computed in `validate_and_classify`, before pool swap. | +| `TakeProfit`, `StopLoss`| TAO output | Deducted in `distribute_tao_pro_rata`, after pool swap. | + +Fee formula: `fee = fee_rate * amount` (using `Perbill` multiplication, which +upcasts to u128 internally to avoid overflow). + +At the end of each batch, fees are accumulated per unique `fee_recipient` and +forwarded in a single transfer per recipient. If multiple orders share the same +`fee_recipient`, they result in exactly one transfer rather than one per order. + +--- + +## Known limitations + +### `max_slippage` is semantically inverted for `StopLoss` orders + +`StopLoss` sells are triggered when the spot price *falls* to `limit_price`. +`max_slippage` derives a sell floor as `limit_price - limit_price * slippage`, +which is computed from the (higher) trigger threshold. By the time the order +fires, the actual market price will typically be **below** `limit_price`, so +the derived floor will almost always exceed the real fill price, causing the +swap to be rejected. + +**Consequence:** Applying `max_slippage` to a `StopLoss` order will usually +prevent it from executing. In `execute_orders` the order is silently skipped; +in `execute_batched_orders` the entire batch fails. + +**Recommendation:** Relayers should set `max_slippage: None` on `StopLoss` +orders. If slippage protection is desired, apply it at the relayer layer by +choosing a conservative `limit_price` rather than relying on `max_slippage`. diff --git a/pallets/limit-orders/src/benchmarking.rs b/pallets/limit-orders/src/benchmarking.rs new file mode 100644 index 0000000000..79bc60f516 --- /dev/null +++ b/pallets/limit-orders/src/benchmarking.rs @@ -0,0 +1,186 @@ +//! Benchmarks for Limit Orders Pallet +#![allow( + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::unwrap_used +)] +use crate::{NetUid, OrderType, Orders}; +use frame_benchmarking::v2::*; +use frame_system::RawOrigin; +use sp_core::{Get, H256}; +use sp_runtime::{AccountId32, MultiSignature, Perbill, traits::AccountIdConversion}; +extern crate alloc; +use crate::{Call, Config, Pallet}; +use codec::Encode; + +/// Sign a versioned order using the runtime keystore (no `full_crypto` required). +/// +/// The key identified by `public` must already be registered in the keystore +/// (e.g. via `sp_io::crypto::sr25519_generate`) before calling this. +fn sign_order( + public: sp_core::sr25519::Public, + order: &crate::VersionedOrder, +) -> crate::SignedOrder { + let sig = sp_io::crypto::sr25519_sign( + sp_core::crypto::key_types::ACCOUNT, + &public, + &order.encode(), + ) + .unwrap(); + crate::SignedOrder { + order: order.clone(), + signature: MultiSignature::Sr25519(sig), + partial_fill: None, + } +} + +/// Generate a deterministic sr25519 key for benchmark index `i` and return its +/// public key. The key is inserted into the runtime keystore so it can sign. +fn benchmark_key(i: u32) -> (sp_core::sr25519::Public, AccountId32) { + let seed = alloc::format!("//BenchSigner{}", i).into_bytes(); + let public = sp_io::crypto::sr25519_generate(sp_core::crypto::key_types::ACCOUNT, Some(seed)); + let account = AccountId32::from(public); + (public, account) +} + +pub fn order_id(order: &crate::VersionedOrder) -> H256 { + crate::pallet::Pallet::::derive_order_id(order) +} + +/// Build `n` signed benchmark orders for `netuid`, one per distinct signer. +/// +/// For each index `i` in `0..n` the function: +/// - derives a deterministic sr25519 key via `benchmark_key(i)`, +/// - calls `T::SwapInterface::set_up_acc_for_benchmark` so the account has +/// sufficient balance / stake, +/// - constructs a worst-case `LimitBuy` order (amount = 1 TAO, price = u64::MAX, +/// expiry = u64::MAX, fee 1 %, distinct fee recipient), and +/// - signs it with the generated key. +fn make_benchmark_orders( + n: u32, + netuid: NetUid, +) -> alloc::vec::Vec> { + use subtensor_swap_interface::OrderSwapInterface; + + let mut orders = alloc::vec::Vec::new(); + + for i in 0..n { + let (public, account_id) = benchmark_key(i); + let account: T::AccountId = account_id.into(); + let fee_recipient: T::AccountId = frame_benchmarking::account("fee_recipient", i, 0); + + T::SwapInterface::set_up_acc_for_benchmark(&account, &account); + + let order = crate::VersionedOrder::V1(crate::Order { + signer: account.clone(), + hotkey: account.clone(), + netuid, + order_type: OrderType::LimitBuy, + amount: 1_000_000_000u64, + limit_price: u64::MAX, + expiry: u64::MAX, + fee_rate: Perbill::from_percent(1), + fee_recipient, + relayer: None, + max_slippage: None, + chain_id: T::ChainId::get(), + partial_fills_enabled: false, + }); + orders.push(sign_order::(public, &order)); + } + + orders +} + +#[benchmarks] +mod benchmarks { + use super::*; + use frame_support::traits::Get; + use subtensor_swap_interface::OrderSwapInterface; + + #[benchmark] + fn cancel_order() { + let (public, account_id) = benchmark_key(0); + let account: T::AccountId = account_id.into(); + + let order = crate::VersionedOrder::V1(crate::Order { + signer: account.clone(), + hotkey: account.clone(), + netuid: NetUid::from(1u16), + order_type: OrderType::LimitBuy, + amount: 1_000, + limit_price: 2_000_000_000, + expiry: 1_000_000_000, + fee_rate: Perbill::zero(), + fee_recipient: account.clone(), + relayer: None, + max_slippage: None, + chain_id: T::ChainId::get(), + partial_fills_enabled: false, + }); + let signed = sign_order::(public, &order); + + #[extrinsic_call] + _(RawOrigin::Signed(account.clone()), signed.order.clone()); + + let id = order_id::(&signed.order); + assert_eq!(Orders::::get(id), Some(crate::OrderStatus::Cancelled)); + } + + #[benchmark] + fn set_pallet_status() { + #[extrinsic_call] + _(RawOrigin::Root, false); + + assert_eq!(crate::LimitOrdersEnabled::::get(), false); + } + + /// Worst case: `n` orders each with a distinct signer (coldkey/hotkey) and a + /// distinct fee recipient, maximising per-order storage reads and fee transfers. + #[benchmark] + fn execute_orders(n: Linear<1, { T::MaxOrdersPerBatch::get() }>) { + let netuid = NetUid::from(1u16); + crate::LimitOrdersEnabled::::set(true); + T::SwapInterface::set_up_netuid_for_benchmark(netuid); + + let orders = make_benchmark_orders::(n, netuid); + + let bounded_orders: frame_support::BoundedVec<_, T::MaxOrdersPerBatch> = + frame_support::BoundedVec::try_from(orders).unwrap(); + let caller: T::AccountId = frame_benchmarking::account("caller", 0, 0); + + #[extrinsic_call] + _(RawOrigin::Signed(caller), bounded_orders, false); + } + + /// Worst case: `n` buy orders each with a distinct signer and fee recipient, + /// maximising asset-collection reads, pro-rata distribution writes, and the + /// number of unique fee-transfer recipients in `collect_fees`. + #[benchmark] + fn execute_batched_orders(n: Linear<1, { T::MaxOrdersPerBatch::get() }>) { + let netuid = NetUid::from(1u16); + crate::LimitOrdersEnabled::::set(true); + T::SwapInterface::set_up_netuid_for_benchmark(netuid); + + // Set up the pallet intermediary so the net pool swap and alpha + // distribution transfers succeed. + let pallet_acct: T::AccountId = T::PalletId::get().into_account_truncating(); + let pallet_hotkey: T::AccountId = T::PalletHotkey::get(); + T::SwapInterface::set_up_acc_for_benchmark(&pallet_hotkey, &pallet_acct); + + let orders = make_benchmark_orders::(n, netuid); + + let bounded_orders: frame_support::BoundedVec<_, T::MaxOrdersPerBatch> = + frame_support::BoundedVec::try_from(orders).unwrap(); + let caller: T::AccountId = frame_benchmarking::account("caller", 0, 0); + + #[extrinsic_call] + _(RawOrigin::Signed(caller), netuid, bounded_orders); + } + + impl_benchmark_test_suite!( + Pallet, + crate::tests::mock::new_test_ext(), + crate::tests::mock::Test + ); +} diff --git a/pallets/limit-orders/src/lib.rs b/pallets/limit-orders/src/lib.rs new file mode 100644 index 0000000000..6cea157bd8 --- /dev/null +++ b/pallets/limit-orders/src/lib.rs @@ -0,0 +1,1265 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +pub use pallet::*; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; +pub(crate) mod migrations; +#[cfg(test)] +mod tests; +pub mod weights; + +type MigrationKeyMaxLen = frame_support::traits::ConstU32<128>; + +use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use frame_support::{BoundedVec, traits::ConstU32}; +use scale_info::TypeInfo; +use sp_core::H256; +use sp_runtime::{ + AccountId32, MultiSignature, Perbill, + traits::{ConstBool, Verify}, +}; +use substrate_fixed::types::U64F64; +use subtensor_macros::freeze_struct; +use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; +use subtensor_swap_interface::OrderSwapInterface; + +// ── Data structures ────────────────────────────────────────────────────────── + +/// Internal direction of a net pool trade. Used only for `GroupExecutionSummary` +/// and pool-swap bookkeeping; not part of the public order payload. +#[derive( + Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, Clone, PartialEq, Eq, Debug, +)] +pub enum OrderSide { + Buy, + Sell, +} + +/// The user-facing order type. Each variant encodes both the execution action +/// (buy alpha / sell alpha) and the price-trigger direction. +/// +/// | Variant | Action | Triggers when | +/// |--------------|--------|---------------------| +/// | `LimitBuy` | Buy | price ≤ limit_price | +/// | `TakeProfit` | Sell | price ≥ limit_price | +/// | `StopLoss` | Sell | price ≤ limit_price | +#[derive( + Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, Clone, PartialEq, Eq, Debug, +)] +pub enum OrderType { + LimitBuy, + TakeProfit, + StopLoss, +} + +impl OrderType { + /// `true` if this order results in buying alpha (staking into subnet). + pub fn is_buy(&self) -> bool { + matches!(self, OrderType::LimitBuy) + } +} + +/// The canonical order payload that users sign off-chain. +/// Only its H256 hash is stored on-chain; the full struct is submitted by the +/// admin at execution time (or by the user at cancellation time). +#[allow(clippy::multiple_bound_locations)] // bounds on AccountId required by FRAME derives +#[freeze_struct("27c7eedb92261456")] +#[derive( + Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, Clone, PartialEq, Eq, Debug, +)] +pub struct Order { + /// The coldkey that authorised this order (pays TAO for buys; owns the + /// staked alpha for sells). + pub signer: AccountId, + /// The hotkey to stake to (buy) or unstake from (sell). + pub hotkey: AccountId, + /// Target subnet. + pub netuid: NetUid, + /// Order type (LimitBuy, TakeProfit, or StopLoss). + pub order_type: OrderType, + /// Input amount: TAO (raw) for Buy, alpha (raw) for Sell. + pub amount: u64, + /// Price threshold in ×10⁹ scale (same as the `current_alpha_price` RPC endpoint). + /// A value of `1_000_000_000` represents a price of 1.0 TAO/alpha. + /// Sub-unity prices (e.g. 0.5 TAO/alpha) are expressed as `500_000_000`. + /// Buy: maximum acceptable price. Sell: minimum acceptable price. + /// `u64::MAX` means no ceiling (buy at any price); `0` means no floor (sell at any price). + pub limit_price: u64, + /// Unix timestamp in milliseconds after which this order must not be executed. + pub expiry: u64, + /// Fee rate applied to this order's TAO amount (input for buys, output for sells). + pub fee_rate: Perbill, + /// Account that receives the fee collected from this order. + pub fee_recipient: AccountId, + /// Accounts authorized to relay this order. When set, only an account present + /// in this list may submit the execution transaction. Supports up to 10 relayers. + pub relayer: Option>>, + /// Maximum slippage tolerance in parts per billion applied to `limit_price` + /// at execution time. `None` = no protection (execute at market). + /// - Buy: effective price ceiling = `limit_price + limit_price * max_slippage` + /// - Sell: effective price floor = `limit_price - limit_price * max_slippage` + pub max_slippage: Option, + /// EVM-compatible chain ID that this order is bound to. + /// Prevents replay of testnet-signed orders on mainnet and vice versa. + pub chain_id: u64, + /// Wether partial fills are enabled + pub partial_fills_enabled: bool, +} + +/// Versioned wrapper around an order payload. +/// +/// Adding a new variant in the future (e.g. `V2`) lets the pallet accept orders +/// signed against either schema simultaneously, preventing old signed orders from +/// being invalidated by a schema upgrade. +#[derive( + Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, Clone, PartialEq, Eq, Debug, +)] +pub enum VersionedOrder { + V1(Order), +} + +impl VersionedOrder { + /// Returns a reference to the inner order regardless of version. + pub fn inner(&self) -> &Order { + match self { + VersionedOrder::V1(order) => order, + } + } +} + +/// The envelope the admin submits on-chain: the versioned order payload plus +/// the user's signature over the SCALE-encoded `VersionedOrder`. +/// +/// Signature verification is performed against `order.inner().signer` (the AccountId) +/// directly. Only sr25519 signatures are accepted; ed25519 and ecdsa variants +/// of `MultiSignature` are rejected at validation time. +#[freeze_struct("9dd5a8ac812dc504")] +#[derive( + Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, Clone, PartialEq, Eq, Debug, +)] +pub struct SignedOrder { + pub order: VersionedOrder, + /// Sr25519 signature over `SCALE_ENCODE(VersionedOrder)`. + pub signature: MultiSignature, + /// Whether we want a partial fill for this order + pub partial_fill: Option, +} + +#[derive( + Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, Clone, PartialEq, Eq, Debug, +)] +pub enum OrderStatus { + /// The order was successfully executed. + Fulfilled, + /// The order was partially filled, with the amount already fulfilled in the enum + PartiallyFilled(u64), + /// The user registered a cancellation intent before execution. + Cancelled, +} + +/// Classified, fee-adjusted entry produced by `validate_and_classify`. +/// Used in every in-memory batch pipeline step; never stored on-chain. +#[derive(Debug, PartialEq)] +pub(crate) struct OrderEntry { + pub(crate) order_id: H256, + pub(crate) signer: AccountId, + pub(crate) hotkey: AccountId, + pub(crate) side: OrderType, + /// Actual input amount being processed this execution (partial or full, before fee). + pub(crate) gross: u64, + /// Full order amount as signed by the user. Used to determine terminal status. + pub(crate) order_amount: u64, + /// Net input amount (after fee). + /// For buys: `gross - fee_rate * gross`. For sells: equals `gross` (fee on TAO output). + pub(crate) net: u64, + /// Per-order fee rate. + pub(crate) fee_rate: Perbill, + /// Per-order fee recipient. + pub(crate) fee_recipient: AccountId, + /// Effective price limit passed to the pool swap. + /// For buys: ceiling (max TAO per alpha the pool may charge). + /// For sells: floor (min TAO per alpha the pool must return). + /// Derived from `limit_price` and `max_slippage` during classification. + pub(crate) effective_swap_limit: u64, + /// Present when this execution covers only part of the order. + pub(crate) partial_fill: Option, +} + +// ── Pallet ─────────────────────────────────────────────────────────────────── + +#[frame_support::pallet] +#[allow(clippy::expect_used)] +pub mod pallet { + use super::*; + use crate::weights::WeightInfo as _; + use frame_support::{ + PalletId, + pallet_prelude::*, + traits::{Get, UnixTime}, + transactional, + }; + use frame_system::pallet_prelude::*; + use sp_runtime::traits::AccountIdConversion; + use sp_std::collections::btree_set::BTreeSet; + use sp_std::vec::Vec; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// Full swap + balance execution interface (see [`OrderSwapInterface`]). + type SwapInterface: OrderSwapInterface; + + /// Time provider for expiry checks. + type TimeProvider: UnixTime; + + /// Maximum number of orders in a single `execute_orders` call. + /// Should equal `floor(max_block_weight / per_order_weight)`. + #[pallet::constant] + type MaxOrdersPerBatch: Get; + + /// PalletId used to derive the intermediary account for batch execution. + /// + /// The derived account temporarily holds pooled TAO and staked alpha + /// during `execute_batched_orders` before distributing to order signers. + #[pallet::constant] + type PalletId: Get; + + /// Hotkey registered in each subnet that the pallet's intermediary + /// account stakes to/from during batch execution. + /// + /// This must be a hotkey registered on every subnet the pallet may + /// operate on. Operators should register a dedicated hotkey and set + /// this in the runtime configuration. + #[pallet::constant] + type PalletHotkey: Get; + + /// Weight information for the pallet's extrinsics. + type WeightInfo: crate::weights::WeightInfo; + + /// EVM-compatible chain ID used to bind orders to a specific chain. + /// Wire to `pallet_evm_chain_id` in the runtime via `ConfigurableChainId`. + type ChainId: Get; + } + + // ── Storage ─────────────────────────────────────────────────────────────── + + /// Tracks the on-chain status of a known `OrderId`. + /// Absent ⇒ never seen (still executable if valid). + /// Present ⇒ Fulfilled or Cancelled (both are terminal). + #[pallet::storage] + pub type Orders = StorageMap<_, Blake2_128Concat, H256, OrderStatus, OptionQuery>; + + /// Switch to enable/disable the pallet. + /// Defaults to `false` so bare node deployments are safe; genesis sets it to `true`. + #[pallet::storage] + pub type LimitOrdersEnabled = StorageValue<_, bool, ValueQuery, ConstBool>; + + /// Tracks which named migrations have already been applied. + /// Keyed by a short migration name; value is always `true`. + #[pallet::storage] + pub type HasMigrationRun = + StorageMap<_, Identity, BoundedVec, bool, ValueQuery>; + + // ── Events ──────────────────────────────────────────────────────────────── + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A limit order was successfully executed. + OrderExecuted { + order_id: H256, + signer: T::AccountId, + netuid: NetUid, + order_type: OrderType, + /// Input amount: TAO (raw) for Buy orders, alpha (raw) for Sell orders. + amount_in: u64, + /// Output amount: alpha (raw) received for Buy orders, TAO (raw) received for Sell orders (after fee). + amount_out: u64, + }, + /// An order was skipped during execution. + OrderSkipped { + order_id: H256, + reason: sp_runtime::DispatchError, + }, + /// A user registered a cancellation intent for their order. + OrderCancelled { + order_id: H256, + signer: T::AccountId, + }, + /// Summary emitted once per `execute_batched_orders` call. + GroupExecutionSummary { + /// The subnet all orders in this batch belong to. + netuid: NetUid, + /// Direction of the net pool trade (Buy = net TAO into pool). + net_side: OrderSide, + /// Net amount sent to the pool (TAO for Buy, alpha for Sell). + /// Zero when buys and sells perfectly offset each other. + net_amount: u64, + /// Tokens received back from the pool. + /// Zero when `net_amount` is zero. + actual_out: u64, + /// Number of orders that were successfully executed. + executed_count: u32, + }, + /// Root has either enabled(true) or disabled(false) the pallet + LimitOrdersPalletStatusChanged { enabled: bool }, + } + + // ── Errors ──────────────────────────────────────────────────────────────── + + #[pallet::error] + pub enum Error { + /// The provided signature does not match the order payload and signer. + InvalidSignature, + /// The order has already been Fulfilled or Cancelled. + OrderAlreadyProcessed, + /// Order has been cancelled + OrderCancelled, + /// The order's expiry timestamp is in the past. + OrderExpired, + /// The current market price does not satisfy the order's limit price. + PriceConditionNotMet, + /// Caller is not the order signer (required for cancellation). + Unauthorized, + /// The pool swap returned zero output for a non-zero input. + SwapReturnedZero, + /// Root netuid (0) is not allowed for limit orders. + RootNetUidNotAllowed, + /// An order in the batch targets a different netuid than the batch netuid parameter. + OrderNetUidMismatch, + /// Limit orders are disabled + LimitOrdersDisabled, + /// Relayer not the same as specified in the order + RelayerMissMatch, + /// Partial fills not enabled for this order + PartialFillsNotEnabled, + /// Incorrect partial fill amount provided + IncorrectPartialFillAmount, + /// A relayer must be set on the order when using partial fills + RelayerRequiredForPartialFill, + /// The order's chain_id does not match the current chain. + ChainIdMismatch, + /// The pallet hotkey has not been registered to the pallet account. + /// Call on_runtime_upgrade or wait for genesis to complete registration + /// before enabling the pallet. + PalletHotkeyNotRegistered, + /// A TAO -> alpha conversion overflowed the fixed-point range. + ArithmeticOverflow, + /// The same order appears more than once in a single batch. + DuplicateOrderInBatch, + } + + // ── Hooks ───────────────────────────────────────────────────────────────── + + // ── Genesis ─────────────────────────────────────────────────────────────── + + #[pallet::genesis_config] + #[derive(frame_support::DefaultNoBound)] + pub struct GenesisConfig { + #[serde(skip)] + pub _phantom: core::marker::PhantomData, + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + let _ = T::SwapInterface::register_pallet_hotkey( + &Pallet::::pallet_account(), + &T::PalletHotkey::get(), + ); + // Enable the pallet on all networks that start from this genesis. + // The storage default is `false` (safe for bare upgrades); genesis + // explicitly opts new chains in. + LimitOrdersEnabled::::set(true); + } + } + + // ── Hooks ───────────────────────────────────────────────────────────────── + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_runtime_upgrade() -> frame_support::weights::Weight { + let mut weight = frame_support::weights::Weight::from_parts(0, 0); + + weight = weight.saturating_add(migrations::migrate_register_pallet_hotkey::()); + + weight + } + } + + // ── Extrinsics ──────────────────────────────────────────────────────────── + + #[pallet::call] + impl Pallet { + /// Execute a batch of signed limit orders. Admin-gated. + /// + /// The `should_fail` flag controls how individual order failures are + /// handled: + /// + /// - When `false` (best-effort): orders whose price condition is not yet + /// met are silently skipped so that a single stale order cannot block + /// the rest of the batch. Orders that fail for any other reason + /// (expired, bad signature, etc.) are also skipped; the admin is + /// expected to filter these off-chain. + /// - When `true` (all-or-nothing): the first order failure aborts the + /// whole batch by returning the underlying error, reverting any orders + /// already executed in this call. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::execute_orders(orders.len() as u32))] + pub fn execute_orders( + origin: OriginFor, + orders: BoundedVec, T::MaxOrdersPerBatch>, + should_fail: bool, + ) -> DispatchResult { + let relayer = ensure_signed(origin)?; + ensure!( + LimitOrdersEnabled::::get(), + Error::::LimitOrdersDisabled + ); + + for signed_order in orders { + let order_id = Self::derive_order_id(&signed_order.order); + if let Err(reason) = Self::try_execute_order(signed_order, order_id, &relayer) { + if should_fail { + // All-or-nothing: abort the batch, reverting prior orders. + return Err(reason); + } + // Best-effort: individual order failures do not revert the batch. + Self::deposit_event(Event::OrderSkipped { order_id, reason }); + } + } + + Ok(()) + } + + /// Execute a batch of signed limit orders for a single subnet using + /// aggregated (netted) pool interaction. + /// + /// Unlike `execute_orders`, which hits the pool once per order, this + /// extrinsic: + /// + /// 1. Validates all orders (bad signature / expired / already processed / + /// price-not-met orders are skipped and emit `OrderSkipped`). + /// 2. Fetches the current price once. + /// 3. Aggregates all valid buy inputs (TAO) and sell inputs (alpha). + /// 4. Nets the two sides: only the residual amount touches the pool in + /// a single swap, minimising price impact. + /// 5. Distributes outputs pro-rata: + /// - Dominant-side orders split the pool output proportionally to + /// their individual net amounts. + /// - Offset-side orders are filled internally at the current price + /// (no pool interaction for them). + /// 6. Collects protocol fees (TAO for buy orders, alpha → TAO for sell + /// orders) and routes them to `FeeCollector`. + /// + /// All orders in the batch must target `netuid`. Orders for a different + /// subnet are skipped. + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::execute_batched_orders(orders.len() as u32))] + pub fn execute_batched_orders( + origin: OriginFor, + netuid: NetUid, + orders: BoundedVec, T::MaxOrdersPerBatch>, + ) -> DispatchResult { + let relayer = ensure_signed(origin)?; + ensure!( + LimitOrdersEnabled::::get(), + Error::::LimitOrdersDisabled + ); + + Self::do_execute_batched_orders(netuid, orders, relayer) + } + + /// Register a cancellation intent for an order. + /// + /// Must be called by the order's signer. The full `Order` payload is + /// provided so the pallet can derive the `OrderId`. Once marked + /// Cancelled, the order can never be executed. + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::cancel_order())] + pub fn cancel_order( + origin: OriginFor, + order: VersionedOrder, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + ensure!(order.inner().signer == who, Error::::Unauthorized); + + let order_id = Self::derive_order_id(&order); + + ensure!( + Orders::::get(order_id).is_none(), + Error::::OrderAlreadyProcessed + ); + + Orders::::insert(order_id, OrderStatus::Cancelled); + Self::deposit_event(Event::OrderCancelled { + order_id, + signer: who, + }); + + Ok(()) + } + + /// Set a status for the limit orders pallet + /// + /// Must be called by root + /// It allows disabling or enabling the pallet + /// true means enabling, false means disabling + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::set_pallet_status())] + pub fn set_pallet_status(origin: OriginFor, enabled: bool) -> DispatchResult { + ensure_root(origin)?; + + if enabled { + ensure!( + T::SwapInterface::pallet_hotkey_registered( + &Self::pallet_account(), + &T::PalletHotkey::get(), + ), + Error::::PalletHotkeyNotRegistered + ); + } + + LimitOrdersEnabled::::set(enabled); + + Self::deposit_event(Event::LimitOrdersPalletStatusChanged { enabled }); + + Ok(()) + } + } + + // ── Internal helpers ────────────────────────────────────────────────────── + + impl Pallet { + /// Compute the effective price limit passed to the pool swap. + /// + /// - `None` slippage → no constraint: `u64::MAX` for buys (no ceiling), + /// `0` for sells (no floor). + /// - `Some(p)` → widens `limit_price` by the slippage fraction: + /// - Buy: ceiling = `limit_price + limit_price * p` (saturating) + /// - Sell: floor = `limit_price - limit_price * p` (saturating) + pub(crate) fn compute_effective_swap_limit( + is_buy: bool, + limit_price: u64, + max_slippage: Option, + ) -> u64 { + match max_slippage { + None => { + if is_buy { + u64::MAX + } else { + 0 + } + } + Some(slippage) => { + let delta = slippage.mul_floor(limit_price); + if is_buy { + limit_price.saturating_add(delta) + } else { + limit_price.saturating_sub(delta) + } + } + } + } + + /// Derive the on-chain `OrderId` as blake2_256 over the SCALE-encoded order. + pub fn derive_order_id(order: &VersionedOrder) -> H256 { + H256(sp_core::hashing::blake2_256(&order.encode())) + } + + /// Account derived from the pallet's `PalletId`. + pub(crate) fn pallet_account() -> T::AccountId { + T::PalletId::get().into_account_truncating() + } + + /// Transfer `fee_tao` from `signer` to `recipient`. + /// Returns an error if the transfer fails, causing the surrounding operation to revert. + /// Does nothing when `fee_tao` is zero. + fn forward_fee( + signer: &T::AccountId, + recipient: &T::AccountId, + fee_tao: TaoBalance, + ) -> DispatchResult { + if fee_tao.is_zero() { + return Ok(()); + } + T::SwapInterface::transfer_tao(signer, recipient, fee_tao) + } + + /// Validates all execution preconditions for a signed order. + /// Checks that the order's netuid is not root (0), that the signature is valid, + /// the order has not been processed, is not expired, and the price condition is met. + /// The batch netuid match (order.netuid == batch netuid) is checked separately by callers. + pub(crate) fn is_order_valid( + signed_order: &SignedOrder, + order_id: H256, + now_ms: u64, + current_price: U64F64, + relayer: &T::AccountId, + ) -> DispatchResult { + let order = signed_order.order.inner(); + ensure!(!order.netuid.is_root(), Error::::RootNetUidNotAllowed); + ensure!( + order.chain_id == T::ChainId::get(), + Error::::ChainIdMismatch + ); + ensure!( + matches!(signed_order.signature, MultiSignature::Sr25519(_)) + && signed_order + .signature + .verify(signed_order.order.encode().as_slice(), &order.signer), + Error::::InvalidSignature + ); + let order_status = Orders::::get(order_id); + ensure!( + order_status != Some(OrderStatus::Fulfilled), + Error::::OrderAlreadyProcessed + ); + ensure!( + order_status != Some(OrderStatus::Cancelled), + Error::::OrderCancelled + ); + ensure!(now_ms <= order.expiry, Error::::OrderExpired); + // Scale current_price to ×10⁹ to match the limit_price field, which is + // expressed in the same ×10⁹ scale as the `current_alpha_price` RPC endpoint. + // This allows sub-unity prices (e.g. 0.5 TAO/alpha = 500_000_000) to be + // represented and compared correctly. + let scaled_price = current_price + .saturating_mul(U64F64::from_num(1_000_000_000u64)) + .saturating_to_num::(); + ensure!( + match order.order_type { + OrderType::TakeProfit => scaled_price >= order.limit_price, + OrderType::StopLoss | OrderType::LimitBuy => scaled_price <= order.limit_price, + }, + Error::::PriceConditionNotMet + ); + if let Some(forced_relayers) = order.relayer.as_ref() { + ensure!( + forced_relayers.contains(relayer), + Error::::RelayerMissMatch + ); + } + if let Some(partial_fill) = signed_order.partial_fill { + ensure!( + order.relayer.is_some(), + Error::::RelayerRequiredForPartialFill + ); + ensure!( + order.partial_fills_enabled, + Error::::PartialFillsNotEnabled + ); + let max_fill = + if let Some(OrderStatus::PartiallyFilled(already_filled)) = order_status { + order.amount.saturating_sub(already_filled) + } else { + order.amount + }; + ensure!( + partial_fill > 0 && partial_fill <= max_fill, + Error::::IncorrectPartialFillAmount + ); + } + Ok(()) + } + + /// Compute the new `OrderStatus` to write after filling `fill_amount` of an order. + /// + /// Reads the current on-chain status to find any already-filled amount, adds + /// `fill_amount`, and returns `Fulfilled` when the total reaches `order_amount`. + /// Pass `None` for `fill_amount` when the order is being fully executed in one shot. + pub(crate) fn compute_order_status( + order_id: H256, + fill_amount: Option, + order_amount: u64, + ) -> OrderStatus { + let Some(fill) = fill_amount else { + return OrderStatus::Fulfilled; + }; + let already_filled = + if let Some(OrderStatus::PartiallyFilled(n)) = Orders::::get(order_id) { + n + } else { + 0 + }; + let new_total = already_filled.saturating_add(fill); + if new_total >= order_amount { + OrderStatus::Fulfilled + } else { + OrderStatus::PartiallyFilled(new_total) + } + } + + /// Attempt to execute one signed order. Returns an error on any + /// validation or execution failure without panicking. + /// + /// `#[transactional]` makes the whole body a single storage layer: the + /// swap (`buy_alpha`/`sell_alpha`, themselves transactional), the fee + /// transfer, and the `Orders::insert` either all commit together or all + /// roll back together. + #[transactional] + fn try_execute_order( + signed_order: SignedOrder, + order_id: H256, + relayer: &T::AccountId, + ) -> DispatchResult { + let order = signed_order.order.inner(); + let now_ms = T::TimeProvider::now().as_millis() as u64; + let current_price = T::SwapInterface::current_alpha_price(order.netuid); + + Self::is_order_valid(&signed_order, order_id, now_ms, current_price, relayer)?; + + let effective_swap_limit = Self::compute_effective_swap_limit( + order.order_type.is_buy(), + order.limit_price, + order.max_slippage, + ); + + // Execute the swap, taking the order's fee from the input (buys) or output (sells). + // `effective_swap_limit` enforces slippage protection: for buys it caps the price + // ceiling; for sells it sets a minimum floor. When `max_slippage` is None the + // limit is u64::MAX (buys) or 0 (sells), matching previous market-order behaviour. + let (amount_in, amount_out) = if order.order_type.is_buy() { + // partial fill validations have passed, it is safe here to do this + let tao_in = TaoBalance::from(signed_order.partial_fill.unwrap_or(order.amount)); + // Deduct fee from TAO input before swapping. + let fee_tao = TaoBalance::from(order.fee_rate.mul_floor(tao_in.to_u64())); + let tao_after_fee = tao_in.saturating_sub(fee_tao); + + let alpha_out = T::SwapInterface::buy_alpha( + &order.signer, + &order.hotkey, + order.netuid, + tao_after_fee, + TaoBalance::from(effective_swap_limit), + true, + )?; + + // Forward the fee TAO to the order's fee recipient. + Self::forward_fee(&order.signer, &order.fee_recipient, fee_tao)?; + (tao_after_fee.to_u64(), alpha_out.to_u64()) + } else { + // partial fill validations have passed, it is safe here to do this + let alpha_in = + AlphaBalance::from(signed_order.partial_fill.unwrap_or(order.amount)); + + // Sell the full alpha amount; fee is taken from the TAO output. + let tao_out = T::SwapInterface::sell_alpha( + &order.signer, + &order.hotkey, + order.netuid, + alpha_in, + TaoBalance::from(effective_swap_limit), + true, + )?; + + // Deduct fee from TAO output and forward to the order's fee recipient. + let fee_tao = TaoBalance::from(order.fee_rate.mul_floor(tao_out.to_u64())); + Self::forward_fee(&order.signer, &order.fee_recipient, fee_tao)?; + (alpha_in.to_u64(), tao_out.saturating_sub(fee_tao).to_u64()) + }; + + // Mark as fulfilled or partially filled and emit event. + let status = + Self::compute_order_status(order_id, signed_order.partial_fill, order.amount); + Orders::::insert(order_id, status); + Self::deposit_event(Event::OrderExecuted { + order_id, + signer: order.signer.clone(), + netuid: order.netuid, + order_type: order.order_type.clone(), + amount_in, + amount_out, + }); + + Ok(()) + } + + /// Thin orchestrator for `execute_batched_orders`. + fn do_execute_batched_orders( + netuid: NetUid, + orders: BoundedVec, T::MaxOrdersPerBatch>, + relayer: T::AccountId, + ) -> DispatchResult { + ensure!(!netuid.is_root(), Error::::RootNetUidNotAllowed); + + let now_ms = T::TimeProvider::now().as_millis() as u64; + let current_price = T::SwapInterface::current_alpha_price(netuid); + + // Validate all orders; any invalid order causes the entire batch to fail. + let (valid_buys, valid_sells) = + Self::validate_and_classify(netuid, &orders, now_ms, current_price, relayer)?; + + let executed_count = valid_buys.len().saturating_add(valid_sells.len()) as u32; + if executed_count == 0 { + return Ok(()); + } + + let total_buy_net: u128 = valid_buys.iter().map(|e| e.net as u128).sum(); + let total_sell_net: u128 = valid_sells.iter().map(|e| e.net as u128).sum(); + let total_sell_tao_equiv: u128 = Self::alpha_to_tao(total_sell_net, current_price); + + let pallet_acct = Self::pallet_account(); + let pallet_hotkey = T::PalletHotkey::get(); + + // Pull all input assets into the pallet intermediary before touching the pool. + Self::collect_assets( + &valid_buys, + &valid_sells, + &pallet_acct, + &pallet_hotkey, + netuid, + )?; + + // Derive the tightest slippage constraint from the dominant side: + // buy-dominant → min of all buy ceilings; sell-dominant → max of all sell floors. + let pool_price_limit = if total_buy_net >= total_sell_tao_equiv { + valid_buys + .iter() + .map(|e| e.effective_swap_limit) + .min() + .unwrap_or(u64::MAX) + } else { + valid_sells + .iter() + .map(|e| e.effective_swap_limit) + .max() + .unwrap_or(0) + }; + + // Execute a single pool swap for the residual (buy TAO minus sell TAO-equiv, or vice versa). + let (net_side, actual_out) = Self::net_pool_swap( + total_buy_net, + total_sell_net, + total_sell_tao_equiv, + current_price, + &pallet_acct, + &pallet_hotkey, + netuid, + pool_price_limit, + )?; + + // Give every buyer their pro-rata share of (pool alpha output + offset sell alpha). + Self::distribute_alpha_pro_rata( + &valid_buys, + actual_out, + total_buy_net, + total_sell_net, + &net_side, + current_price, + &pallet_acct, + &pallet_hotkey, + netuid, + )?; + + // Give every seller their pro-rata share of (pool TAO output + offset buy TAO), + // deducting per-order fees from each payout; returns accumulated sell fees by recipient. + let sell_fees = Self::distribute_tao_pro_rata( + &valid_sells, + actual_out, + total_buy_net, + total_sell_tao_equiv, + &net_side, + current_price, + &pallet_acct, + netuid, + )?; + + // Merge buy and sell fees by recipient and transfer once per unique recipient. + Self::collect_fees(&valid_buys, sell_fees, &pallet_acct)?; + + let net_amount = Self::net_amount_for_event( + &net_side, + total_buy_net, + total_sell_net, + total_sell_tao_equiv, + current_price, + )?; + Self::deposit_event(Event::GroupExecutionSummary { + netuid, + net_side, + net_amount, + actual_out: actual_out as u64, + executed_count, + }); + + Ok(()) + } + + /// Validate every order against `netuid`, signature, expiry, and price. + /// Valid orders are split into two BoundedVecs by side. + /// Each entry is `(order_id, signer, hotkey, gross, net, fee)`. + /// + /// Returns an error immediately if any order fails validation (wrong netuid, + /// invalid signature, expired, already processed, or price condition not met). + pub(crate) fn validate_and_classify( + netuid: NetUid, + orders: &BoundedVec, T::MaxOrdersPerBatch>, + now_ms: u64, + current_price: U64F64, + relayer: T::AccountId, + ) -> Result< + ( + BoundedVec, T::MaxOrdersPerBatch>, + BoundedVec, T::MaxOrdersPerBatch>, + ), + DispatchError, + > { + let mut buys = BoundedVec::new(); + let mut sells = BoundedVec::new(); + + // Track which order_ids we have already seen in this batch. A repeated + // order_id is never legitimate within a single batch. + let mut seen_order_ids: BTreeSet = BTreeSet::new(); + + for signed_order in orders.iter() { + let order_id = Self::derive_order_id(&signed_order.order); + + // Hard-fail on the first duplicate order_id in the batch (covers both + // buys and sells). BTreeSet::insert returns false if already present. + ensure!( + seen_order_ids.insert(order_id), + Error::::DuplicateOrderInBatch + ); + + let order = signed_order.order.inner(); + + // Hard-fail if the order targets a different subnet than the batch netuid. + ensure!(order.netuid == netuid, Error::::OrderNetUidMismatch); + + // Hard-fail on any per-order validation error (signature, expiry, price, root). + Self::is_order_valid(signed_order, order_id, now_ms, current_price, &relayer)?; + + let amount_in = signed_order.partial_fill.unwrap_or(order.amount); + let net = if order.order_type.is_buy() { + // Buy: fee on TAO input — net is the amount that reaches the pool. + amount_in.saturating_sub(order.fee_rate.mul_floor(amount_in)) + } else { + // Sell: fee on TAO output — full alpha enters the pool; the fee is + // deducted from the TAO payout later in `distribute_tao_pro_rata`. + amount_in + }; + + let effective_swap_limit = Self::compute_effective_swap_limit( + order.order_type.is_buy(), + order.limit_price, + order.max_slippage, + ); + + let entry = OrderEntry { + order_id, + signer: order.signer.clone(), + hotkey: order.hotkey.clone(), + side: order.order_type.clone(), + gross: amount_in, + order_amount: order.amount, + net, + fee_rate: order.fee_rate, + fee_recipient: order.fee_recipient.clone(), + effective_swap_limit, + partial_fill: signed_order.partial_fill, + }; + + // try_push cannot fail: both vecs share the same bound as `orders`. + if entry.side.is_buy() { + let _ = buys.try_push(entry); + } else { + let _ = sells.try_push(entry); + } + } + + Ok((buys, sells)) + } + + /// Pull gross TAO from each buyer and gross staked alpha from each seller + /// into the pallet intermediary account, bypassing the pool. + fn collect_assets( + buys: &BoundedVec, T::MaxOrdersPerBatch>, + sells: &BoundedVec, T::MaxOrdersPerBatch>, + pallet_acct: &T::AccountId, + pallet_hotkey: &T::AccountId, + netuid: NetUid, + ) -> DispatchResult { + for e in buys.iter() { + T::SwapInterface::transfer_tao(&e.signer, pallet_acct, TaoBalance::from(e.gross))?; + } + for e in sells.iter() { + T::SwapInterface::transfer_staked_alpha( + &e.signer, + &e.hotkey, + pallet_acct, + pallet_hotkey, + netuid, + AlphaBalance::from(e.gross), + true, // validate_sender: check user's rate limit, subnet, min stake + false, // set_receiver_limit: do not rate-limit the pallet intermediary + )?; + } + Ok(()) + } + + /// Execute a single pool swap for the net (residual) amount. + /// Returns `(net_side, actual_out)` where `actual_out` is in the output + /// token units (alpha for Buy, TAO for Sell). + /// + /// `price_limit` encodes the tightest slippage constraint across all dominant-side + /// orders: a ceiling for buy-dominant swaps, a floor for sell-dominant swaps. + #[allow(clippy::too_many_arguments)] + fn net_pool_swap( + total_buy_net: u128, + total_sell_net: u128, + total_sell_tao_equiv: u128, + current_price: U64F64, + pallet_acct: &T::AccountId, + pallet_hotkey: &T::AccountId, + netuid: NetUid, + price_limit: u64, + ) -> Result<(OrderSide, u128), DispatchError> { + if total_buy_net >= total_sell_tao_equiv { + let net_tao = (total_buy_net.saturating_sub(total_sell_tao_equiv)) as u64; + let actual_alpha = if net_tao > 0 { + let out = T::SwapInterface::buy_alpha( + pallet_acct, + pallet_hotkey, + netuid, + TaoBalance::from(net_tao), + TaoBalance::from(price_limit), + false, + )? + .to_u64() as u128; + ensure!(out > 0, Error::::SwapReturnedZero); + out + } else { + 0u128 + }; + Ok((OrderSide::Buy, actual_alpha)) + } else { + let total_buy_alpha_equiv = Self::tao_to_alpha(total_buy_net, current_price)?; + let net_alpha = (total_sell_net.saturating_sub(total_buy_alpha_equiv)) as u64; + let actual_tao = if net_alpha > 0 { + let out = T::SwapInterface::sell_alpha( + pallet_acct, + pallet_hotkey, + netuid, + AlphaBalance::from(net_alpha), + TaoBalance::from(price_limit), + false, + )? + .to_u64() as u128; + ensure!(out > 0, Error::::SwapReturnedZero); + out + } else { + 0u128 + }; + Ok((OrderSide::Sell, actual_tao)) + } + } + + /// Distribute alpha pro-rata to ALL buyers and mark their orders fulfilled. + /// + /// - Buy-dominant: total alpha = pool output + sell-side alpha (passed through). + /// - Sell-dominant: total alpha = buy-side TAO converted at `current_price`. + #[allow(clippy::too_many_arguments)] + pub(crate) fn distribute_alpha_pro_rata( + buys: &BoundedVec, T::MaxOrdersPerBatch>, + actual_out: u128, + total_buy_net: u128, + total_sell_net: u128, + net_side: &OrderSide, + current_price: U64F64, + pallet_acct: &T::AccountId, + pallet_hotkey: &T::AccountId, + netuid: NetUid, + ) -> DispatchResult { + let total_alpha: u128 = match net_side { + OrderSide::Buy => actual_out.saturating_add(total_sell_net), + OrderSide::Sell => Self::tao_to_alpha(total_buy_net, current_price)?, + }; + + for e in buys.iter() { + let share: u64 = if total_buy_net > 0 { + total_alpha + .saturating_mul(e.net as u128) + .checked_div(total_buy_net) + .unwrap_or(0) as u64 + } else { + 0 + }; + if share > 0 { + T::SwapInterface::transfer_staked_alpha( + pallet_acct, + pallet_hotkey, + &e.signer, + &e.hotkey, + netuid, + AlphaBalance::from(share), + false, // validate_sender: skip — pallet intermediary needs no validation + true, // set_receiver_limit: rate-limit the buyer after they receive stake + )?; + } + let status = Self::compute_order_status(e.order_id, e.partial_fill, e.order_amount); + Orders::::insert(e.order_id, status); + Self::deposit_event(Event::OrderExecuted { + order_id: e.order_id, + signer: e.signer.clone(), + netuid, + order_type: e.side.clone(), + amount_in: e.gross, + amount_out: share, + }); + } + Ok(()) + } + + /// Distribute TAO pro-rata to ALL sellers and mark their orders fulfilled. + /// + /// - Sell-dominant: total TAO = pool output + buy-side TAO (passed through). + /// - Buy-dominant: each seller receives their alpha valued at `current_price`. + /// + /// Fee on TAO output: `ppb(share)` is withheld from each seller's payout and + /// left in the pallet account. Returns the total sell-side fee TAO accumulated. + #[allow(clippy::too_many_arguments)] + pub(crate) fn distribute_tao_pro_rata( + sells: &BoundedVec, T::MaxOrdersPerBatch>, + actual_out: u128, + total_buy_net: u128, + total_sell_tao_equiv: u128, + net_side: &OrderSide, + current_price: U64F64, + pallet_acct: &T::AccountId, + netuid: NetUid, + ) -> Result, DispatchError> { + let total_tao: u128 = match net_side { + OrderSide::Sell => actual_out.saturating_add(total_buy_net), + OrderSide::Buy => total_sell_tao_equiv, + }; + + // Accumulate sell-side fees by recipient (one entry per unique recipient). + let mut sell_fees: Vec<(T::AccountId, u64)> = Vec::new(); + + for e in sells.iter() { + let sell_tao_equiv = Self::alpha_to_tao(e.net as u128, current_price); + let gross_share: u64 = if total_sell_tao_equiv > 0 { + total_tao + .saturating_mul(sell_tao_equiv) + .checked_div(total_sell_tao_equiv) + .unwrap_or(0) as u64 + } else { + 0u64 + }; + let fee = e.fee_rate.mul_floor(gross_share); + let net_share = gross_share.saturating_sub(fee); + + if fee > 0 { + if let Some(entry) = sell_fees.iter_mut().find(|(r, _)| r == &e.fee_recipient) { + entry.1 = entry.1.saturating_add(fee); + } else { + sell_fees.push((e.fee_recipient.clone(), fee)); + } + } + + T::SwapInterface::transfer_tao( + pallet_acct, + &e.signer, + TaoBalance::from(net_share), + )?; + let status = Self::compute_order_status(e.order_id, e.partial_fill, e.order_amount); + Orders::::insert(e.order_id, status); + Self::deposit_event(Event::OrderExecuted { + order_id: e.order_id, + signer: e.signer.clone(), + netuid, + order_type: e.side.clone(), + amount_in: e.gross, + amount_out: net_share, + }); + } + Ok(sell_fees) + } + + /// Forward accumulated fees to their respective recipients. + /// + /// Merges buy-side fees (withheld from TAO input) and sell-side fees + /// (withheld from TAO output, passed in as `sell_fees`) by recipient, + /// then performs one TAO transfer per unique `fee_recipient`. + /// All transfers are best-effort and do not revert the batch on failure. + pub(crate) fn collect_fees( + buys: &BoundedVec, T::MaxOrdersPerBatch>, + sell_fees: Vec<(T::AccountId, u64)>, + pallet_acct: &T::AccountId, + ) -> DispatchResult { + // Start with sell fees; fold in buy fees. + // Buy fee was already computed in `validate_and_classify` as `gross - net`, + // so we recover it here without recomputing. + let mut fees: Vec<(T::AccountId, u64)> = sell_fees; + for e in buys.iter() { + let fee = e.gross.saturating_sub(e.net); + if fee > 0 { + if let Some(entry) = fees.iter_mut().find(|(r, _)| r == &e.fee_recipient) { + entry.1 = entry.1.saturating_add(fee); + } else { + fees.push((e.fee_recipient.clone(), fee)); + } + } + } + + // One transfer per unique fee recipient. + for (recipient, amount) in fees { + Self::forward_fee(pallet_acct, &recipient, TaoBalance::from(amount))?; + } + + // TODO: sweep rounding dust and any emissions accrued on the pallet account. + // Pro-rata integer division leaves small alpha residuals in (pallet_account, + // pallet_hotkey) after each batch. Over time these accumulate and, if an + // emission epoch fires while the dust is present, the pallet earns emissions + // it never distributes. Fix: add `staked_alpha(coldkey, hotkey, netuid) -> + // AlphaBalance` to `OrderSwapInterface`, then sell the full remaining balance + // here and forward the TAO to `FeeCollector`. + Ok(()) + } + + /// Compute the net amount field for the `GroupExecutionSummary` event. + pub(crate) fn net_amount_for_event( + net_side: &OrderSide, + total_buy_net: u128, + total_sell_net: u128, + total_sell_tao_equiv: u128, + current_price: U64F64, + ) -> Result { + match net_side { + OrderSide::Buy => Ok((total_buy_net.saturating_sub(total_sell_tao_equiv)) as u64), + OrderSide::Sell => { + let buy_alpha_equiv = Self::tao_to_alpha(total_buy_net, current_price)? as u64; + Ok((total_sell_net as u64).saturating_sub(buy_alpha_equiv)) + } + } + } + + /// Convert a TAO amount to alpha at `price` (TAO/alpha). + /// + /// A zero `price` yields `Ok(0)` (no alpha is purchasable). A genuine + /// fixed-point overflow returns `Err(ArithmeticOverflow)` so the caller + /// aborts the batch. + fn tao_to_alpha(tao: u128, price: U64F64) -> Result { + if price == U64F64::from_num(0u32) { + return Ok(0u128); + } + U64F64::saturating_from_num(tao) + .checked_div(price) + .map(|alpha| alpha.saturating_to_num::()) + .ok_or(Error::::ArithmeticOverflow.into()) + } + + /// Convert an alpha amount to TAO at `price` (TAO/alpha). + fn alpha_to_tao(alpha: u128, price: U64F64) -> u128 { + price + .saturating_mul(U64F64::saturating_from_num(alpha)) + .saturating_to_num::() + } + } +} diff --git a/pallets/limit-orders/src/migrations/migrate_register_pallet_hotkey.rs b/pallets/limit-orders/src/migrations/migrate_register_pallet_hotkey.rs new file mode 100644 index 0000000000..f3d6b4ef3f --- /dev/null +++ b/pallets/limit-orders/src/migrations/migrate_register_pallet_hotkey.rs @@ -0,0 +1,52 @@ +use alloc::string::String; +use frame_support::{BoundedVec, traits::Get, weights::Weight}; + +use crate::*; + +fn migration_key() -> BoundedVec { + BoundedVec::truncate_from(b"migrate_register_pallet_hotkey".to_vec()) +} + +/// One-shot migration that disables the limit-orders pallet on first upgrade and +/// registers the pallet intermediary hotkey if it has not been registered yet. +/// +/// Guarded by `HasMigrationRun` so it is safe to include in every runtime upgrade: +/// subsequent calls return immediately after a single storage read. +pub fn migrate_register_pallet_hotkey() -> Weight { + let migration_name = migration_key(); + let mut weight = T::DbWeight::get().reads(1); + + if HasMigrationRun::::get(&migration_name) { + log::info!( + "Migration '{:?}' has already run. Skipping.", + String::from_utf8_lossy(&migration_name) + ); + return weight; + } + + log::info!( + "Running migration '{}'", + String::from_utf8_lossy(&migration_name) + ); + + // Register the pallet intermediary hotkey if it has not been registered yet. + let pallet_acct = Pallet::::pallet_account(); + let pallet_hotkey = T::PalletHotkey::get(); + weight = weight.saturating_add(T::DbWeight::get().reads(1)); + + if !T::SwapInterface::pallet_hotkey_registered(&pallet_acct, &pallet_hotkey) { + let _ = T::SwapInterface::register_pallet_hotkey(&pallet_acct, &pallet_hotkey); + // register_pallet_hotkey writes Owner, OwnedHotkeys, StakingHotkeys + weight = weight.saturating_add(T::DbWeight::get().writes(3)); + } + + HasMigrationRun::::insert(&migration_name, true); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + log::info!( + "Migration '{:?}' completed successfully.", + String::from_utf8_lossy(&migration_name) + ); + + weight +} diff --git a/pallets/limit-orders/src/migrations/mod.rs b/pallets/limit-orders/src/migrations/mod.rs new file mode 100644 index 0000000000..391730d481 --- /dev/null +++ b/pallets/limit-orders/src/migrations/mod.rs @@ -0,0 +1,2 @@ +mod migrate_register_pallet_hotkey; +pub use migrate_register_pallet_hotkey::*; diff --git a/pallets/limit-orders/src/tests/auxiliary.rs b/pallets/limit-orders/src/tests/auxiliary.rs new file mode 100644 index 0000000000..1049c84f74 --- /dev/null +++ b/pallets/limit-orders/src/tests/auxiliary.rs @@ -0,0 +1,1667 @@ +#![allow(clippy::expect_used, clippy::unwrap_used, clippy::indexing_slicing)] +//! Unit tests for the auxiliary helper functions in `pallet-limit-orders`. +//! +//! Extrinsics are NOT tested here. Each section focuses on one helper. + +use frame_support::{BoundedVec, assert_noop, assert_ok, traits::ConstU32}; +use sp_core::H256; +use sp_keyring::Sr25519Keyring as AccountKeyring; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::NetUid; + +use sp_runtime::Perbill; + +use crate::pallet::Pallet as LimitOrders; +use crate::{OrderEntry, OrderSide, OrderStatus, OrderType, Orders}; + +use super::mock::*; + +// ───────────────────────────────────────────────────────────────────────────── +// net_amount_for_event +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn net_amount_for_event_buy_dominant() { + new_test_ext().execute_with(|| { + // Buys = 1000 TAO net, sells TAO-equiv = 300 TAO → net 700 TAO buy-side + let price = U64F64::from_num(2u32); // 2 TAO/alpha + let net = LimitOrders::::net_amount_for_event( + &OrderSide::Buy, + 1_000u128, // total_buy_net (TAO) + 150u128, // total_sell_net (alpha) ← not used in Buy branch + 300u128, // total_sell_tao_equiv + price, + ) + .expect("conversion does not overflow"); + assert_eq!(net, 700u64); + }); +} + +#[test] +fn net_amount_for_event_sell_dominant() { + new_test_ext().execute_with(|| { + // Sells = 500 alpha net, buys TAO = 200 TAO at price 2 → buy_alpha_equiv = 100 + // net sell = 500 - 100 = 400 alpha + let price = U64F64::from_num(2u32); // 2 TAO/alpha → 1 alpha = 2 TAO + let net = LimitOrders::::net_amount_for_event( + &OrderSide::Sell, + 200u128, // total_buy_net (TAO) + 500u128, // total_sell_net (alpha) + 400u128, // total_sell_tao_equiv (not used in Sell branch directly) + price, + ) + .expect("conversion does not overflow"); + // buy_alpha_equiv = 200 / 2 = 100; net = 500 - 100 = 400 + assert_eq!(net, 400u64); + }); +} + +#[test] +fn net_amount_for_event_perfectly_offset() { + new_test_ext().execute_with(|| { + // Buys = 200 TAO, sells TAO-equiv = 200 → net = 0 (buy-side result = 0) + let price = U64F64::from_num(2u32); + let net = LimitOrders::::net_amount_for_event( + &OrderSide::Buy, + 200u128, + 100u128, + 200u128, + price, + ) + .expect("conversion does not overflow"); + assert_eq!(net, 0u64); + }); +} + +#[test] +fn net_amount_for_event_sell_overflow_returns_error() { + new_test_ext().execute_with(|| { + let tiny_price = U64F64::from_bits(1); + assert_eq!( + LimitOrders::::net_amount_for_event( + &OrderSide::Sell, + u128::MAX, + 500u128, + 0u128, + tiny_price, + ), + Err(Error::::ArithmeticOverflow.into()), + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// validate_and_classify +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn validate_and_classify_separates_buys_and_sells() { + new_test_ext().execute_with(|| { + // Current time = 1_000_000 ms; expiry = 2_000_000 ms (well in the future). + MockTime::set(1_000_000); + // Price = 1.0 TAO/alpha. + MockSwap::set_price(1.0); + + let buy_order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000u64, // amount in TAO + 2_000_000_000u64, // limit_price: willing to pay up to 2 TAO/alpha in ×10⁹ scale (scaled=1_000_000_000 ≤ 2_000_000_000 ✓) + 2_000_000u64, // expiry ms + Perbill::zero(), + fee_recipient(), + None, + ); + let sell_order = make_signed_order( + AccountKeyring::Bob, + alice(), + netuid(), + OrderType::TakeProfit, + 500u64, // amount in alpha + 1_000_000_000u64, // limit_price: sell if price >= 1 TAO/alpha in ×10⁹ scale (scaled=1_000_000_000 >= 1_000_000_000 ✓) + 2_000_000u64, + Perbill::zero(), + fee_recipient(), + None, + ); + + let orders = bounded(vec![buy_order, sell_order]); + let (buys, sells) = LimitOrders::::validate_and_classify( + netuid(), + &orders, + 1_000_000u64, + U64F64::from_num(1u32), + bob(), + ) + .expect("validate_and_classify should succeed"); + + assert_eq!(buys.len(), 1, "expected 1 valid buy"); + assert_eq!(sells.len(), 1, "expected 1 valid sell"); + + // Buy entry: gross=1000, net=1000 (0% fee_rate) + let buy = &buys[0]; + assert_eq!(buy.signer, alice()); + assert_eq!(buy.gross, 1_000u64); + assert_eq!(buy.net, 1_000u64); + assert_eq!(buy.fee_rate, Perbill::zero()); + + // Sell entry: gross=500, net=500 (fee applied on TAO output, not alpha input) + let sell = &sells[0]; + assert_eq!(sell.signer, bob()); + assert_eq!(sell.gross, 500u64); + assert_eq!(sell.net, 500u64); + }); +} + +#[test] +fn validate_and_classify_fails_for_wrong_netuid() { + new_test_ext().execute_with(|| { + // An order whose netuid does not match the batch netuid must cause a hard failure. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + let wrong_netuid_order = make_signed_order( + AccountKeyring::Alice, + bob(), + NetUid::from(99u16), // different netuid + OrderType::LimitBuy, + 1_000u64, + 2_000_000_000u64, // 2.0 in ×10⁹ scale + 2_000_000u64, + Perbill::zero(), + fee_recipient(), + None, + ); + + let orders = bounded(vec![wrong_netuid_order]); + assert_noop!( + LimitOrders::::validate_and_classify( + netuid(), // batch is for netuid 1 + &orders, + 1_000_000u64, + U64F64::from_num(1u32), + bob() + ), + crate::Error::::OrderNetUidMismatch + ); + }); +} + +#[test] +fn validate_and_classify_fails_for_expired_order() { + new_test_ext().execute_with(|| { + // now_ms = 2_000_001, expiry = 2_000_000 → expired → hard failure. + MockTime::set(2_000_001); + MockSwap::set_price(1.0); + + let expired = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000u64, + 2_000_000_000u64, // 2.0 in ×10⁹ scale + 2_000_000u64, // expiry already past + Perbill::zero(), + fee_recipient(), + None, + ); + + let orders = bounded(vec![expired]); + assert_noop!( + LimitOrders::::validate_and_classify( + netuid(), + &orders, + 2_000_001u64, + U64F64::from_num(1u32), + bob() + ), + crate::Error::::OrderExpired + ); + }); +} + +#[test] +fn validate_and_classify_fails_for_price_condition_not_met_for_buy() { + new_test_ext().execute_with(|| { + // Price = 3.0 TAO/alpha, scaled = 3_000_000_000, buyer's limit = 2_000_000_000 (2.0 in ×10⁹) → scaled > limit → hard failure. + MockTime::set(1_000_000); + let order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000u64, + 2_000_000_000u64, // 2.0 in ×10⁹ scale + 2_000_000u64, + Perbill::zero(), + fee_recipient(), + None, + ); + + let orders = bounded(vec![order]); + assert_noop!( + LimitOrders::::validate_and_classify( + netuid(), + &orders, + 1_000_000u64, + U64F64::from_num(3u32), // current price = 3 > limit 2 → fails + bob() + ), + crate::Error::::PriceConditionNotMet + ); + }); +} + +#[test] +fn validate_and_classify_fails_for_already_processed_order() { + new_test_ext().execute_with(|| { + // An order already marked Fulfilled must cause a hard failure. + MockTime::set(1_000_000); + let order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000u64, + 2_000_000_000u64, // 2.0 in ×10⁹ scale + 2_000_000u64, + Perbill::zero(), + fee_recipient(), + None, + ); + + // Pre-mark as fulfilled on-chain. + let oid = LimitOrders::::derive_order_id(&order.order); + Orders::::insert(oid, OrderStatus::Fulfilled); + + let orders = bounded(vec![order]); + assert_noop!( + LimitOrders::::validate_and_classify( + netuid(), + &orders, + 1_000_000u64, + U64F64::from_num(1u32), + bob() + ), + crate::Error::::OrderAlreadyProcessed + ); + }); +} + +#[test] +fn validate_and_classify_applies_buy_fee_to_net() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + // 1_000_000 ppb = 0.1% + // amount = 1_000_000_000, fee = 1_000_000, net = 999_000_000 + + let order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000_000_000u64, + u64::MAX, // limit price: accept any price + 2_000_000u64, + Perbill::from_parts(1_000_000), // 0.1% fee + fee_recipient(), + None, + ); + + let orders = bounded(vec![order]); + let (buys, _) = LimitOrders::::validate_and_classify( + netuid(), + &orders, + 1_000_000u64, + U64F64::from_num(1u32), + bob(), + ) + .expect("validate_and_classify should succeed"); + + assert_eq!(buys.len(), 1); + let entry = &buys[0]; + assert_eq!(entry.gross, 1_000_000_000u64); + assert_eq!(entry.fee_rate, Perbill::from_parts(1_000_000)); + assert_eq!(entry.net, 999_000_000u64); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// compute_effective_swap_limit +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn compute_effective_swap_limit_buy_no_slippage() { + new_test_ext().execute_with(|| { + // No slippage → u64::MAX (no ceiling). + let limit = LimitOrders::::compute_effective_swap_limit(true, 1_000, None); + assert_eq!(limit, u64::MAX); + }); +} + +#[test] +fn compute_effective_swap_limit_sell_no_slippage() { + new_test_ext().execute_with(|| { + // No slippage → 0 (no floor). + let limit = LimitOrders::::compute_effective_swap_limit(false, 1_000, None); + assert_eq!(limit, 0); + }); +} + +#[test] +fn compute_effective_swap_limit_buy_one_percent() { + new_test_ext().execute_with(|| { + // 1% slippage on a buy with limit_price=1000 → ceiling = 1010. + let limit = LimitOrders::::compute_effective_swap_limit( + true, + 1_000, + Some(Perbill::from_percent(1)), + ); + assert_eq!(limit, 1_010); + }); +} + +#[test] +fn compute_effective_swap_limit_sell_one_percent() { + new_test_ext().execute_with(|| { + // 1% slippage on a sell with limit_price=1000 → floor = 990. + let limit = LimitOrders::::compute_effective_swap_limit( + false, + 1_000, + Some(Perbill::from_percent(1)), + ); + assert_eq!(limit, 990); + }); +} + +#[test] +fn compute_effective_swap_limit_sell_saturates_at_zero() { + new_test_ext().execute_with(|| { + // 100% slippage on a sell with limit_price=500 → floor saturates at 0. + let limit = LimitOrders::::compute_effective_swap_limit( + false, + 500, + Some(Perbill::from_percent(100)), + ); + assert_eq!(limit, 0); + }); +} + +#[test] +fn compute_effective_swap_limit_buy_saturates_at_u64_max() { + new_test_ext().execute_with(|| { + // 100% slippage on a buy with limit_price=u64::MAX → ceiling saturates at u64::MAX. + let limit = LimitOrders::::compute_effective_swap_limit( + true, + u64::MAX, + Some(Perbill::from_percent(100)), + ); + assert_eq!(limit, u64::MAX); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// validate_and_classify — effective_swap_limit propagation +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn validate_and_classify_stores_effective_swap_limit_for_buy() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + // 1% slippage on limit_price=2_000_000_000 (2.0 in ×10⁹) → ceiling = 2_020_000_000. + // price=1.0, scaled=1_000_000_000 <= 2_000_000_000 ✓. + let order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 500u64, + 2_000_000_000u64, // 2.0 in ×10⁹ scale + 2_000_000u64, + Perbill::zero(), + fee_recipient(), + None, + ); + // Override max_slippage on the inner order after signing — we need to rebuild + // the signed order so the signature covers the updated payload. + let new_inner = { + let mut o = order.order.inner().clone(); + o.max_slippage = Some(Perbill::from_percent(1)); + o + }; + let versioned = crate::VersionedOrder::V1(new_inner.clone()); + let sig = AccountKeyring::Alice.pair().sign(&versioned.encode()); + let signed_with_slippage = crate::SignedOrder { + order: versioned, + signature: sp_runtime::MultiSignature::Sr25519(sig), + partial_fill: None, + }; + + let orders = bounded(vec![signed_with_slippage]); + let (buys, _) = LimitOrders::::validate_and_classify( + netuid(), + &orders, + 1_000_000u64, + U64F64::from_num(1u32), + bob(), + ) + .expect("should succeed"); + + assert_eq!(buys[0].effective_swap_limit, 2_020_000_000); + }); +} + +#[test] +fn validate_and_classify_stores_effective_swap_limit_for_sell() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + // Price must be >= limit_price (in ×10⁹ scale) for TakeProfit to trigger. + // limit_price=1_000_000_000 (1.0 in ×10⁹), 1% slippage → floor = 990_000_000. + let new_inner = crate::Order { + signer: AccountKeyring::Alice.to_account_id(), + hotkey: bob(), + netuid: netuid(), + order_type: OrderType::TakeProfit, + amount: 500u64, + limit_price: 1_000_000_000u64, // 1.0 in ×10⁹ scale + expiry: u64::MAX, + fee_rate: Perbill::zero(), + fee_recipient: fee_recipient(), + relayer: None, + max_slippage: Some(Perbill::from_percent(1)), + chain_id: 945, + partial_fills_enabled: false, + }; + let versioned = crate::VersionedOrder::V1(new_inner); + let sig = AccountKeyring::Alice.pair().sign(&versioned.encode()); + let signed = crate::SignedOrder { + order: versioned, + signature: sp_runtime::MultiSignature::Sr25519(sig), + partial_fill: None, + }; + + let orders = bounded(vec![signed]); + let (_, sells) = LimitOrders::::validate_and_classify( + netuid(), + &orders, + 1_000_000u64, + U64F64::from_num(2u32), // current_price=2.0, scaled=2_000_000_000 >= limit_price=1_000_000_000 ✓ + bob(), + ) + .expect("should succeed"); + + assert_eq!(sells[0].effective_swap_limit, 990_000_000); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// validate_and_classify — relayer enforcement +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn validate_and_classify_fails_for_wrong_relayer() { + new_test_ext().execute_with(|| { + // Order explicitly locks execution to charlie(); submitting as bob() must fail. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + let order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000u64, + u64::MAX, + 2_000_000u64, + Perbill::zero(), + fee_recipient(), + Some(BoundedVec::try_from(vec![charlie()]).unwrap()), // only charlie may relay this order + ); + + let orders = bounded(vec![order]); + assert_noop!( + LimitOrders::::validate_and_classify( + netuid(), + &orders, + 1_000_000u64, + U64F64::from_num(1u32), + bob() // wrong relayer + ), + crate::Error::::RelayerMissMatch + ); + }); +} + +#[test] +fn validate_and_classify_succeeds_for_correct_relayer() { + new_test_ext().execute_with(|| { + // Same setup as above but now the correct relayer (charlie) is used. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + let order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000u64, + u64::MAX, + 2_000_000u64, + Perbill::zero(), + fee_recipient(), + Some(BoundedVec::try_from(vec![charlie()]).unwrap()), // only charlie may relay this order + ); + + let orders = bounded(vec![order]); + let (buys, sells) = LimitOrders::::validate_and_classify( + netuid(), + &orders, + 1_000_000u64, + U64F64::from_num(1u32), + charlie(), // correct relayer + ) + .expect("validate_and_classify should succeed"); + + assert_eq!(buys.len(), 1, "expected 1 valid buy"); + assert_eq!(sells.len(), 0); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// distribute_alpha_pro_rata +// ───────────────────────────────────────────────────────────────────────────── +// +// Scenario A – buy-dominant, pool rate = 1:1 +// ─────────────────────────────────────────── +// Both buyers and sellers are present, but buys exceed sells in TAO terms. +// Sellers are settled first (they receive TAO in distribute_tao_pro_rata). +// Their alpha (200 total) stays in the pallet account as passthrough for buyers. +// The residual buy TAO hits the pool and returns 800 alpha (at 1:1 rate). +// +// 3 buyers: Alice 300 TAO net, Bob 200 TAO net, Charlie 500 TAO net (total 1000) +// Sellers contributed 200 alpha (passthrough, no pool interaction). +// Net residual TAO to pool = 1000 - 200 = 800 TAO → pool returns 800 alpha (1:1). +// Total alpha available to buyers = 800 (pool) + 200 (seller passthrough) = 1000. +// +// Pro-rata shares (proportional to each buyer's net TAO): +// Alice: 1000 * 300 / 1000 = 300 alpha +// Bob: 1000 * 200 / 1000 = 200 alpha +// Charlie: 1000 * 500 / 1000 = 500 alpha +// +// Scenario B – sell-dominant +// ─────────────────────────── +// Both buyers and sellers are present, but sells exceed buys in TAO terms. +// Buyers are settled from the sellers' alpha directly (no pool for them). +// The residual sell alpha hits the pool; sellers receive TAO in distribute_tao_pro_rata. +// +// 2 buyers: Alice 400 TAO net, Bob 600 TAO net (total 1000) +// Price = 2.0 TAO/alpha → total alpha for buyers = 1000 / 2 = 500 alpha. +// +// Pro-rata shares: +// Alice: 500 * 400 / 1000 = 200 alpha +// Bob: 500 * 600 / 1000 = 300 alpha +// +// Scenario C – buy-dominant, pool rate != 1:1 +// ──────────────────────────────────────────────────────── +// Same structure as Scenario A but the pool returns fewer alpha than the TAO +// sent in, simulating realistic AMM. Pro-rata is computed over +// whatever the pool actually returned — the distribution logic is rate-agnostic. +// +// 3 buyers: Alice 300 TAO net, Bob 200 TAO net, Charlie 500 TAO net (total 1000) +// Sellers contributed 200 alpha (passthrough). +// Net residual TAO to pool = 800 TAO → pool returns 750 alpha (slippage). +// Total alpha available to buyers = 750 (pool) + 200 (seller passthrough) = 950. +// +// Pro-rata shares: +// Alice: 950 * 300 / 1000 = 285 alpha +// Bob: 950 * 200 / 1000 = 190 alpha +// Charlie: 950 * 500 / 1000 = 475 alpha +// +// Scenario D – buy-dominant, indivisible remainder (dust) +// ───────────────────────────────────────────────────────── +// Integer division floors every share. The sum of floors is strictly less than +// total_alpha when total_alpha is not divisible by total_buy_net. +// The leftover alpha stays in the pallet intermediary account (never transferred). +// +// 3 buyers: Alice 1 TAO net, Bob 1 TAO net, Charlie 1 TAO net (total 3) +// Pool returns 10 alpha; no sellers → total_alpha = 10. +// +// Pro-rata shares (floor): +// Alice: floor(10 * 1 / 3) = 3 alpha +// Bob: floor(10 * 1 / 3) = 3 alpha +// Charlie: floor(10 * 1 / 3) = 3 alpha +// Total distributed: 9 alpha +// Dust remaining in pallet account: 10 - 9 = 1 alpha (never transferred) + +fn make_buy_entry( + order_id: H256, + signer: AccountId, + hotkey: AccountId, + gross: u64, + net: u64, + fee_rate: Perbill, + fee_recipient: AccountId, +) -> OrderEntry { + OrderEntry { + order_id, + signer, + hotkey, + side: OrderType::LimitBuy, + gross, + order_amount: gross, + net, + fee_rate, + fee_recipient, + effective_swap_limit: u64::MAX, // no slippage constraint + partial_fill: None, + } +} + +fn bounded_buy_entries( + v: Vec>, +) -> BoundedVec, ConstU32<64>> { + BoundedVec::try_from(v).unwrap() +} + +fn bounded_sell_entries( + v: Vec>, +) -> BoundedVec, ConstU32<64>> { + BoundedVec::try_from(v).unwrap() +} + +#[test] +fn distribute_alpha_pro_rata_buy_dominant_scenario_a() { + new_test_ext().execute_with(|| { + // Pool returned 800 alpha; sell-side passthrough = 200 alpha. + // Total = 1000 alpha distributed across 3 buyers (300, 200, 500 TAO net). + // Expected shares: Alice 300, Bob 200, Charlie 500. + + let hotkey = AccountKeyring::Dave.to_account_id(); + let entries = bounded_buy_entries(vec![ + make_buy_entry( + H256::repeat_byte(1), + alice(), + hotkey.clone(), + 300, + 300, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(2), + bob(), + hotkey.clone(), + 200, + 200, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(3), + charlie(), + hotkey.clone(), + 500, + 500, + Perbill::zero(), + fee_recipient(), + ), + ]); + let pallet_acct = PalletHotkeyAccount::get(); // reuse as coldkey for brevity + let pallet_hk = PalletHotkeyAccount::get(); + + LimitOrders::::distribute_alpha_pro_rata( + &entries, + 800u128, // actual_out from pool (alpha) + 1_000u128, // total_buy_net (TAO) + 200u128, // total_sell_net (alpha passthrough) + &OrderSide::Buy, + U64F64::from_num(1u32), + &pallet_acct, + &pallet_hk, + netuid(), + ) + .unwrap(); + + let transfers = MockSwap::alpha_transfers(); + // 3 transfers expected (one per buyer) + assert_eq!(transfers.len(), 3); + + // Check each recipient's amount (signer is to_coldkey). + let alice_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &alice()) + .unwrap() + .5; + let bob_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &bob()) + .unwrap() + .5; + let charlie_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &charlie()) + .unwrap() + .5; + + assert_eq!(alice_amt, 300u64, "Alice should receive 300 alpha"); + assert_eq!(bob_amt, 200u64, "Bob should receive 200 alpha"); + assert_eq!(charlie_amt, 500u64, "Charlie should receive 500 alpha"); + }); +} + +#[test] +fn distribute_alpha_pro_rata_sell_dominant_scenario_b() { + new_test_ext().execute_with(|| { + // Price = 2.0 TAO/alpha; buyers have 400 + 600 = 1000 TAO net. + // Total alpha = 1000 / 2 = 500. + // Expected: Alice 200 alpha, Bob 300 alpha. + + let hotkey = AccountKeyring::Dave.to_account_id(); + let entries = bounded_buy_entries(vec![ + make_buy_entry( + H256::repeat_byte(4), + alice(), + hotkey.clone(), + 400, + 400, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(5), + bob(), + hotkey.clone(), + 600, + 600, + Perbill::zero(), + fee_recipient(), + ), + ]); + let pallet_acct = PalletHotkeyAccount::get(); + let pallet_hk = PalletHotkeyAccount::get(); + + LimitOrders::::distribute_alpha_pro_rata( + &entries, + 0u128, // actual_out unused in sell-dominant branch + 1_000u128, // total_buy_net (TAO) + 999u128, // total_sell_net — doesn't matter for sell-dominant logic + &OrderSide::Sell, + U64F64::from_num(2u32), // price = 2 TAO/alpha + &pallet_acct, + &pallet_hk, + netuid(), + ) + .unwrap(); + + let transfers = MockSwap::alpha_transfers(); + assert_eq!(transfers.len(), 2); + + let alice_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &alice()) + .unwrap() + .5; + let bob_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &bob()) + .unwrap() + .5; + + assert_eq!(alice_amt, 200u64, "Alice should receive 200 alpha"); + assert_eq!(bob_amt, 300u64, "Bob should receive 300 alpha"); + }); +} + +#[test] +fn distribute_alpha_pro_rata_buy_dominant_scenario_c() { + new_test_ext().execute_with(|| { + // Scenario C: same buyer setup as A but pool returns 750 alpha (slippage) + // instead of 800. Proves pro-rata is computed over actual pool output and + // is therefore rate-agnostic — the distribution logic doesn't assume 1:1. + // + // Net residual TAO to pool = 800 TAO → pool returns 750 alpha (not 800). + // Total alpha = 750 (pool) + 200 (seller passthrough) = 950. + // + // Expected shares: + // Alice: 950 * 300 / 1000 = 285 alpha + // Bob: 950 * 200 / 1000 = 190 alpha + // Charlie: 950 * 500 / 1000 = 475 alpha + + let hotkey = AccountKeyring::Dave.to_account_id(); + let entries = bounded_buy_entries(vec![ + make_buy_entry( + H256::repeat_byte(6), + alice(), + hotkey.clone(), + 300, + 300, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(7), + bob(), + hotkey.clone(), + 200, + 200, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(8), + charlie(), + hotkey.clone(), + 500, + 500, + Perbill::zero(), + fee_recipient(), + ), + ]); + let pallet_acct = PalletHotkeyAccount::get(); + let pallet_hk = PalletHotkeyAccount::get(); + + LimitOrders::::distribute_alpha_pro_rata( + &entries, + 750u128, // actual_out from pool (750, not 800 — slippage) + 1_000u128, // total_buy_net (TAO) + 200u128, // total_sell_net (alpha passthrough) + &OrderSide::Buy, + U64F64::from_num(1u32), + &pallet_acct, + &pallet_hk, + netuid(), + ) + .unwrap(); + + let transfers = MockSwap::alpha_transfers(); + assert_eq!(transfers.len(), 3); + + let alice_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &alice()) + .unwrap() + .5; + let bob_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &bob()) + .unwrap() + .5; + let charlie_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &charlie()) + .unwrap() + .5; + + assert_eq!( + alice_amt, 285u64, + "Alice receives 950 * 300/1000 = 285 alpha" + ); + assert_eq!(bob_amt, 190u64, "Bob receives 950 * 200/1000 = 190 alpha"); + assert_eq!( + charlie_amt, 475u64, + "Charlie receives 950 * 500/1000 = 475 alpha" + ); + }); +} + +#[test] +fn distribute_alpha_pro_rata_dust_remains_in_pallet_scenario_d() { + new_test_ext().execute_with(|| { + // Scenario D: total_alpha = 10, three equal buyers (total_buy_net = 3). + // floor(10 * 1/3) = 3 each → 9 distributed → 1 alpha dust stays in pallet. + + let hotkey = AccountKeyring::Dave.to_account_id(); + let pallet_acct = PalletHotkeyAccount::get(); + let pallet_hk = PalletHotkeyAccount::get(); + + // Seed the pallet account with the 10 alpha it would hold after collect_assets + // and the pool swap (actual_out=10, no sellers). + MockSwap::set_alpha_balance(pallet_acct.clone(), pallet_hk.clone(), netuid(), 10); + + let entries = bounded_buy_entries(vec![ + make_buy_entry( + H256::repeat_byte(9), + alice(), + hotkey.clone(), + 1, + 1, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(10), + bob(), + hotkey.clone(), + 1, + 1, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(11), + charlie(), + hotkey.clone(), + 1, + 1, + Perbill::zero(), + fee_recipient(), + ), + ]); + + LimitOrders::::distribute_alpha_pro_rata( + &entries, + 10u128, // actual_out from pool + 3u128, // total_buy_net (TAO) — not divisible into 10 evenly + 0u128, // total_sell_net — no sellers + &OrderSide::Buy, + U64F64::from_num(1u32), + &pallet_acct, + &pallet_hk, + netuid(), + ) + .unwrap(); + + let transfers = MockSwap::alpha_transfers(); + assert_eq!(transfers.len(), 3); + + let alice_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &alice()) + .unwrap() + .5; + let bob_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &bob()) + .unwrap() + .5; + let charlie_amt = transfers + .iter() + .find(|(_, _, to_ck, _, _, _)| to_ck == &charlie()) + .unwrap() + .5; + + assert_eq!(alice_amt, 3u64, "floor(10 * 1/3) = 3"); + assert_eq!(bob_amt, 3u64, "floor(10 * 1/3) = 3"); + assert_eq!(charlie_amt, 3u64, "floor(10 * 1/3) = 3"); + + // The pallet account started with 10 and sent out 9 — 1 alpha dust remains + // in the pallet account, not burnt, not distributed. + let pallet_remaining = MockSwap::alpha_balance(&pallet_acct, &pallet_hk, netuid()); + assert_eq!( + pallet_remaining, 1u64, + "1 alpha dust stays in pallet account, not burnt" + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// distribute_tao_pro_rata +// ───────────────────────────────────────────────────────────────────────────── +// +// Scenario A – sell-dominant, fee = 0 +// ───────────────────────────────────── +// Both buyers and sellers are present, but sells exceed buys in TAO terms. +// Buyers are settled first (they receive alpha in distribute_alpha_pro_rata). +// The residual sell alpha hits the pool; pool returns TAO. +// Buy-side TAO also stays in pallet as passthrough for sellers. +// +// 2 sellers: Alice 400 alpha, Bob 600 alpha (total 1000 alpha) +// Price = 2.0 TAO/alpha → sell_tao_equiv: Alice 800, Bob 1200, total 2000. +// Pool returned 1200 TAO for the residual alpha; buy passthrough = 800 TAO. +// Total TAO available to sellers = 1200 (pool) + 800 (buy passthrough) = 2000. +// +// Pro-rata shares (proportional to each seller's TAO-equiv): +// Alice: 2000 * 800 / 2000 = 800 TAO +// Bob: 2000 * 1200 / 2000 = 1200 TAO +// +// Scenario B – sell-dominant, fee = 1% (10_000_000 ppb) +// ──────────────────────────────────────────────────────── +// Same structure as Scenario A. Fee is deducted from each seller's gross TAO +// payout; the withheld TAO stays in the pallet account for collect_fees. +// +// Alice gross=800, fee=8 (1% of 800), net=792 TAO +// Bob gross=1200, fee=12, net=1188 TAO +// Total sell fee returned: 20 TAO +// +// Scenario C – buy-dominant +// ────────────────────────── +// Both buyers and sellers are present, but buys exceed sells in TAO terms. +// Sellers receive their alpha valued at current_price — no pool interaction +// for them. The TAO they receive comes from the buyers' collected TAO directly. +// +// 2 sellers: Alice 300 alpha, Bob 200 alpha (total 500 alpha) +// Price = 2.0 TAO/alpha → sell_tao_equiv: Alice 600, Bob 400, total 1000. +// Buy-dominant branch: total_tao = total_sell_tao_equiv = 1000 TAO. +// +// Shares: +// Alice: 1000 * 600 / 1000 = 600 TAO +// Bob: 1000 * 400 / 1000 = 400 TAO +// +// Scenario D – sell-dominant, indivisible remainder (dust) +// ───────────────────────────────────────────────────────── +// Integer division floors every gross share. The leftover TAO stays in the +// pallet intermediary account (never transferred, not burnt). +// +// 3 sellers: Alice 1 alpha, Bob 1 alpha, Charlie 1 alpha (total 3 alpha) +// Price = 1.0 TAO/alpha → sell_tao_equiv = 1 each, total_sell_tao_equiv = 3. +// No buyers; actual_out from pool = 10 TAO, buy passthrough = 0. +// total_tao = 10 + 0 = 10. +// +// Pro-rata shares (floor): +// Alice: floor(10 * 1 / 3) = 3 TAO +// Bob: floor(10 * 1 / 3) = 3 TAO +// Charlie: floor(10 * 1 / 3) = 3 TAO +// Total distributed: 9 TAO +// Dust remaining in pallet account: 10 - 9 = 1 TAO (never transferred) + +#[test] +fn distribute_tao_pro_rata_sell_dominant_no_fee_scenario_a() { + new_test_ext().execute_with(|| { + // Price = 2, total_tao = 1200 (pool) + 800 (buy passthrough) = 2000 + // Alice alpha=400 → tao_equiv=800; Bob alpha=600 → tao_equiv=1200. + // total_sell_tao_equiv = 2000. + // Shares: Alice 800, Bob 1200. + + let hotkey = AccountKeyring::Dave.to_account_id(); + let entries = bounded_sell_entries(vec![ + make_buy_entry( + H256::repeat_byte(6), + alice(), + hotkey.clone(), + 400, + 400, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(7), + bob(), + hotkey.clone(), + 600, + 600, + Perbill::zero(), + fee_recipient(), + ), + ]); + let pallet_acct = PalletHotkeyAccount::get(); + + let sell_fees = LimitOrders::::distribute_tao_pro_rata( + &entries, + 1_200u128, // actual_out (pool TAO) + 800u128, // total_buy_net (buy passthrough TAO) + 2_000u128, // total_sell_tao_equiv (Alice 800 + Bob 1200) + &OrderSide::Sell, + U64F64::from_num(2u32), + &pallet_acct, + netuid(), + ) + .unwrap(); + + let transfers = MockSwap::tao_transfers(); + assert_eq!(transfers.len(), 2); + let alice_tao = transfers + .iter() + .find(|(_, to, _)| to == &alice()) + .unwrap() + .2; + let bob_tao = transfers.iter().find(|(_, to, _)| to == &bob()).unwrap().2; + + assert_eq!(alice_tao, 800u64, "Alice should receive 800 TAO"); + assert_eq!(bob_tao, 1_200u64, "Bob should receive 1200 TAO"); + assert_eq!( + sell_fees, + vec![] as Vec<(AccountId, u64)>, + "No fees at 0 ppb" + ); + }); +} + +#[test] +fn distribute_tao_pro_rata_sell_dominant_with_fee_scenario_b() { + new_test_ext().execute_with(|| { + // Same setup as above but fee = 10_000_000 ppb = 1%. + // Alice gross=800, fee=8, net=792; Bob gross=1200, fee=12, net=1188. + // Total sell fee = 20. + + let hotkey = AccountKeyring::Dave.to_account_id(); + let entries = bounded_sell_entries(vec![ + make_buy_entry( + H256::repeat_byte(8), + alice(), + hotkey.clone(), + 400, + 400, + Perbill::from_parts(10_000_000), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(9), + bob(), + hotkey.clone(), + 600, + 600, + Perbill::from_parts(10_000_000), + fee_recipient(), + ), + ]); + let pallet_acct = PalletHotkeyAccount::get(); + + let sell_fees = LimitOrders::::distribute_tao_pro_rata( + &entries, + 1_200u128, + 800u128, + 2_000u128, + &OrderSide::Sell, + U64F64::from_num(2u32), + &pallet_acct, + netuid(), + ) + .unwrap(); + + let transfers = MockSwap::tao_transfers(); + assert_eq!(transfers.len(), 2); + let alice_tao = transfers + .iter() + .find(|(_, to, _)| to == &alice()) + .unwrap() + .2; + let bob_tao = transfers.iter().find(|(_, to, _)| to == &bob()).unwrap().2; + + assert_eq!(alice_tao, 792u64, "Alice net after 1% fee on 800"); + assert_eq!(bob_tao, 1_188u64, "Bob net after 1% fee on 1200"); + assert_eq!( + sell_fees, + vec![(fee_recipient(), 20u64)], + "total sell fee = 8 + 12" + ); + }); +} + +#[test] +fn distribute_tao_pro_rata_buy_dominant_scenario_c() { + new_test_ext().execute_with(|| { + // Buy-dominant: total_tao = total_sell_tao_equiv = 1000. + // Alice alpha=300 → tao_equiv=600; Bob alpha=200 → tao_equiv=400. + // Shares: Alice 600, Bob 400. + + let hotkey = AccountKeyring::Dave.to_account_id(); + let entries = bounded_sell_entries(vec![ + make_buy_entry( + H256::repeat_byte(10), + alice(), + hotkey.clone(), + 300, + 300, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(11), + bob(), + hotkey.clone(), + 200, + 200, + Perbill::zero(), + fee_recipient(), + ), + ]); + let pallet_acct = PalletHotkeyAccount::get(); + + let sell_fees = LimitOrders::::distribute_tao_pro_rata( + &entries, + 0u128, // actual_out unused in Buy-dominant branch + 0u128, // total_buy_net unused in Buy-dominant branch + 1_000u128, // total_sell_tao_equiv (total_tao = this in Buy branch) + &OrderSide::Buy, + U64F64::from_num(2u32), + &pallet_acct, + netuid(), + ) + .unwrap(); + + let transfers = MockSwap::tao_transfers(); + assert_eq!(transfers.len(), 2); + let alice_tao = transfers + .iter() + .find(|(_, to, _)| to == &alice()) + .unwrap() + .2; + let bob_tao = transfers.iter().find(|(_, to, _)| to == &bob()).unwrap().2; + + assert_eq!(alice_tao, 600u64, "Alice should receive 600 TAO"); + assert_eq!(bob_tao, 400u64, "Bob should receive 400 TAO"); + assert_eq!(sell_fees, vec![] as Vec<(AccountId, u64)>); + }); +} + +#[test] +fn distribute_tao_pro_rata_dust_remains_in_pallet_scenario_d() { + new_test_ext().execute_with(|| { + // Scenario D: total_tao = 10, three equal sellers (total_sell_tao_equiv = 3). + // floor(10 * 1/3) = 3 each → 9 distributed → 1 TAO dust stays in pallet. + + let hotkey = AccountKeyring::Dave.to_account_id(); + let pallet_acct = PalletHotkeyAccount::get(); + + // Seed the pallet account with the 10 TAO it would hold after collect_assets + // and the pool swap (actual_out=10, no buyers). + MockSwap::set_tao_balance(pallet_acct.clone(), 10); + + let entries = bounded_sell_entries(vec![ + make_buy_entry( + H256::repeat_byte(12), + alice(), + hotkey.clone(), + 1, + 1, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(13), + bob(), + hotkey.clone(), + 1, + 1, + Perbill::zero(), + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(14), + charlie(), + hotkey.clone(), + 1, + 1, + Perbill::zero(), + fee_recipient(), + ), + ]); + + let sell_fees = LimitOrders::::distribute_tao_pro_rata( + &entries, + 10u128, // actual_out from pool (TAO) + 0u128, // total_buy_net — no buyers + 3u128, // total_sell_tao_equiv — not divisible into 10 evenly + &OrderSide::Sell, + U64F64::from_num(1u32), + &pallet_acct, + netuid(), + ) + .unwrap(); + + let transfers = MockSwap::tao_transfers(); + assert_eq!(transfers.len(), 3); + + let alice_tao = transfers + .iter() + .find(|(_, to, _)| to == &alice()) + .unwrap() + .2; + let bob_tao = transfers.iter().find(|(_, to, _)| to == &bob()).unwrap().2; + let charlie_tao = transfers + .iter() + .find(|(_, to, _)| to == &charlie()) + .unwrap() + .2; + + assert_eq!(alice_tao, 3u64, "floor(10 * 1/3) = 3"); + assert_eq!(bob_tao, 3u64, "floor(10 * 1/3) = 3"); + assert_eq!(charlie_tao, 3u64, "floor(10 * 1/3) = 3"); + assert_eq!(sell_fees, vec![] as Vec<(AccountId, u64)>); + + // The pallet account started with 10 TAO and sent out 9 — 1 TAO dust remains, + // not burnt, not distributed. + let pallet_remaining = MockSwap::tao_balance(&pallet_acct); + assert_eq!( + pallet_remaining, 1u64, + "1 TAO dust stays in pallet account, not burnt" + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// collect_fees +// ───────────────────────────────────────────────────────────────────────────── +// +// Scenario: +// 2 buy orders with fees 50 and 150 TAO → total_buy_fee = 200 TAO. +// sell_fee_tao passed in = 80 TAO. +// Total fee = 280 TAO forwarded to FeeCollector in one transfer. + +#[test] +fn collect_fees_forwards_combined_fees_to_collector() { + new_test_ext().execute_with(|| { + let hotkey = AccountKeyring::Dave.to_account_id(); + // Buy entries carry fee in field index 5. + let buys = bounded_buy_entries(vec![ + make_buy_entry( + H256::repeat_byte(20), + alice(), + hotkey.clone(), + 1_000, + 950, + Perbill::from_parts(50_000_000), // 5% of 1000 = 50 + fee_recipient(), + ), + make_buy_entry( + H256::repeat_byte(21), + bob(), + hotkey.clone(), + 1_500, + 1_350, + Perbill::from_parts(100_000_000), // 10% of 1500 = 150 + fee_recipient(), + ), + ]); + let pallet_acct = PalletHotkeyAccount::get(); + + assert_ok!(LimitOrders::::collect_fees( + &buys, + vec![(fee_recipient(), 80u64)], + &pallet_acct + )); + + let tao_transfers = MockSwap::tao_transfers(); + assert_eq!(tao_transfers.len(), 1, "single transfer to fee_recipient"); + let (from, to, amount) = &tao_transfers[0]; + assert_eq!(from, &pallet_acct, "fee comes from pallet account"); + assert_eq!(to, &fee_recipient(), "fee goes to fee_recipient"); + assert_eq!(*amount, 280u64, "total fee = 200 (buy) + 80 (sell)"); + }); +} + +#[test] +fn collect_fees_no_transfer_when_zero_fees() { + new_test_ext().execute_with(|| { + // No buy fees, no sell fee. + let hotkey = AccountKeyring::Dave.to_account_id(); + let buys = bounded_buy_entries(vec![make_buy_entry( + H256::repeat_byte(22), + alice(), + hotkey, + 1_000, + 1_000, + Perbill::zero(), + fee_recipient(), + )]); + let pallet_acct = PalletHotkeyAccount::get(); + + assert_ok!(LimitOrders::::collect_fees( + &buys, + vec![], + &pallet_acct + )); + + let tao_transfers = MockSwap::tao_transfers(); + assert_eq!(tao_transfers.len(), 0, "no transfer when total fee is zero"); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// is_order_valid +// ───────────────────────────────────────────────────────────────────────────── + +use crate::Error; +use codec::Encode; +use sp_core::Pair; +use sp_runtime::MultiSignature; +use subtensor_swap_interface::OrderSwapInterface; + +fn make_valid_signed_order() -> (crate::SignedOrder, sp_core::H256) { + let keyring = AccountKeyring::Alice; + let order = crate::VersionedOrder::V1(crate::Order { + signer: keyring.to_account_id(), + hotkey: AccountKeyring::Bob.to_account_id(), + netuid: netuid(), + order_type: OrderType::LimitBuy, + amount: 1_000, + limit_price: u64::MAX, + expiry: u64::MAX, + fee_rate: Perbill::zero(), + fee_recipient: fee_recipient(), + relayer: None, + max_slippage: None, + chain_id: 945, + partial_fills_enabled: false, + }); + let id = H256(sp_io::hashing::blake2_256(&order.encode())); + let sig = keyring.pair().sign(&order.encode()); + let signed = crate::SignedOrder { + order, + signature: MultiSignature::Sr25519(sig), + partial_fill: None, + }; + (signed, id) +} + +#[test] +fn is_order_valid_returns_ok_for_well_formed_order() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + let (signed, id) = make_valid_signed_order(); + let price = MockSwap::current_alpha_price(netuid()); + assert_ok!(LimitOrders::::is_order_valid( + &signed, + id, + 1_000_000, + price, + &bob() + )); + }); +} + +#[test] +fn is_order_valid_invalid_signature_returns_error() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + let (mut signed, id) = make_valid_signed_order(); + // Replace with a signature from a different key. + let wrong_sig = AccountKeyring::Bob.pair().sign(&signed.order.encode()); + signed.signature = MultiSignature::Sr25519(wrong_sig); + let price = MockSwap::current_alpha_price(netuid()); + assert_noop!( + LimitOrders::::is_order_valid(&signed, id, 1_000_000, price, &bob()), + Error::::InvalidSignature + ); + }); +} + +#[test] +fn is_order_valid_non_sr25519_signature_returns_error() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + let (mut signed, id) = make_valid_signed_order(); + let ed_pair = sp_core::ed25519::Pair::from_legacy_string("//Alice", None); + let ed_sig = ed_pair.sign(&signed.order.encode()); + signed.signature = MultiSignature::Ed25519(ed_sig); + let price = MockSwap::current_alpha_price(netuid()); + assert_noop!( + LimitOrders::::is_order_valid(&signed, id, 1_000_000, price, &bob()), + Error::::InvalidSignature + ); + }); +} + +#[test] +fn is_order_valid_already_processed_returns_error() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + let (signed, id) = make_valid_signed_order(); + Orders::::insert(id, crate::OrderStatus::Fulfilled); + let price = MockSwap::current_alpha_price(netuid()); + assert_noop!( + LimitOrders::::is_order_valid(&signed, id, 1_000_000, price, &bob()), + Error::::OrderAlreadyProcessed + ); + }); +} + +#[test] +fn is_order_valid_expired_order_returns_error() { + new_test_ext().execute_with(|| { + MockSwap::set_price(1.0); + let (signed, _id) = make_valid_signed_order(); + // now_ms (2_000_001) > expiry (u64::MAX is fine, so use a low expiry order). + // Re-build a signed order with a past expiry. + let keyring = AccountKeyring::Alice; + let order = crate::VersionedOrder::V1(crate::Order { + expiry: 500_000, + ..signed.order.inner().clone() + }); + let id2 = H256(sp_io::hashing::blake2_256(&order.encode())); + let sig = keyring.pair().sign(&order.encode()); + let signed2 = crate::SignedOrder { + order, + signature: MultiSignature::Sr25519(sig), + partial_fill: None, + }; + let price = MockSwap::current_alpha_price(netuid()); + assert_noop!( + LimitOrders::::is_order_valid(&signed2, id2, 1_000_000, price, &bob()), + Error::::OrderExpired + ); + }); +} + +#[test] +fn is_order_valid_price_condition_not_met_returns_error() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + // Price 5.0, scaled = 5_000_000_000 > limit_price 2_000_000_000 (2.0 in ×10⁹) → LimitBuy condition (scaled ≤ limit) not met. + MockSwap::set_price(5.0); + let keyring = AccountKeyring::Alice; + let order = crate::VersionedOrder::V1(crate::Order { + signer: keyring.to_account_id(), + hotkey: AccountKeyring::Bob.to_account_id(), + netuid: netuid(), + order_type: OrderType::LimitBuy, + amount: 1_000, + limit_price: 2_000_000_000, // 2.0 in ×10⁹ scale + expiry: u64::MAX, + fee_rate: Perbill::zero(), + fee_recipient: fee_recipient(), + relayer: None, + max_slippage: None, + chain_id: 945, + partial_fills_enabled: false, + }); + let id = H256(sp_io::hashing::blake2_256(&order.encode())); + let sig = keyring.pair().sign(&order.encode()); + let signed = crate::SignedOrder { + order, + signature: MultiSignature::Sr25519(sig), + partial_fill: None, + }; + let price = MockSwap::current_alpha_price(netuid()); + assert_noop!( + LimitOrders::::is_order_valid(&signed, id, 1_000_000, price, &bob()), + Error::::PriceConditionNotMet + ); + }); +} + +#[test] +fn is_order_valid_wrong_chain_id_returns_error() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + let keyring = AccountKeyring::Alice; + // Build an order with a chain_id that doesn't match the mock config (945). + let order = crate::VersionedOrder::V1(crate::Order { + chain_id: 9999, + ..make_valid_signed_order().0.order.inner().clone() + }); + let id = H256(sp_io::hashing::blake2_256(&order.encode())); + let sig = keyring.pair().sign(&order.encode()); + let signed = crate::SignedOrder { + order, + signature: MultiSignature::Sr25519(sig), + partial_fill: None, + }; + let price = MockSwap::current_alpha_price(netuid()); + assert_noop!( + LimitOrders::::is_order_valid(&signed, id, 1_000_000, price, &bob()), + Error::::ChainIdMismatch + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// compute_order_status +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn compute_order_status_no_partial_fill_returns_fulfilled() { + new_test_ext().execute_with(|| { + let id = H256::repeat_byte(1); + // No existing state, no partial fill → Fulfilled immediately. + let status = LimitOrders::::compute_order_status(id, None, 1_000); + assert_eq!(status, OrderStatus::Fulfilled); + }); +} + +#[test] +fn compute_order_status_partial_fill_below_total_returns_partially_filled() { + new_test_ext().execute_with(|| { + let id = H256::repeat_byte(2); + // First partial fill of 400 on a 1000-unit order → PartiallyFilled(400). + let status = LimitOrders::::compute_order_status(id, Some(400), 1_000); + assert_eq!(status, OrderStatus::PartiallyFilled(400)); + }); +} + +#[test] +fn compute_order_status_partial_fill_exact_total_returns_fulfilled() { + new_test_ext().execute_with(|| { + let id = H256::repeat_byte(3); + // Single partial fill that equals the full order amount → Fulfilled. + let status = LimitOrders::::compute_order_status(id, Some(1_000), 1_000); + assert_eq!(status, OrderStatus::Fulfilled); + }); +} + +#[test] +fn compute_order_status_accumulates_previous_partial_fill() { + new_test_ext().execute_with(|| { + let id = H256::repeat_byte(4); + // Pre-seed storage as if a prior partial fill of 300 already happened. + Orders::::insert(id, OrderStatus::PartiallyFilled(300)); + + // Second fill of 400 → 300 + 400 = 700, still below 1000. + let status = LimitOrders::::compute_order_status(id, Some(400), 1_000); + assert_eq!(status, OrderStatus::PartiallyFilled(700)); + }); +} + +#[test] +fn compute_order_status_completes_order_when_accumulated_total_reaches_amount() { + new_test_ext().execute_with(|| { + let id = H256::repeat_byte(5); + Orders::::insert(id, OrderStatus::PartiallyFilled(600)); + + // Fill the remaining 400 → 600 + 400 = 1000 = order_amount → Fulfilled. + let status = LimitOrders::::compute_order_status(id, Some(400), 1_000); + assert_eq!(status, OrderStatus::Fulfilled); + }); +} + +#[test] +fn compute_order_status_ignores_fulfilled_storage_when_no_partial_fill() { + new_test_ext().execute_with(|| { + let id = H256::repeat_byte(6); + // If somehow called with no partial_fill regardless of what's in storage + // (should not happen in practice) it still returns Fulfilled. + Orders::::insert(id, OrderStatus::PartiallyFilled(500)); + let status = LimitOrders::::compute_order_status(id, None, 1_000); + assert_eq!(status, OrderStatus::Fulfilled); + }); +} diff --git a/pallets/limit-orders/src/tests/extrinsics.rs b/pallets/limit-orders/src/tests/extrinsics.rs new file mode 100644 index 0000000000..2e92a32838 --- /dev/null +++ b/pallets/limit-orders/src/tests/extrinsics.rs @@ -0,0 +1,3203 @@ +#![allow(clippy::indexing_slicing)] +//! Integration tests for `pallet-limit-orders` extrinsics. +//! +//! Tests go through the full dispatch path: origin enforcement, storage changes, +//! and event emission are all verified. SwapInterface calls are handled by +//! `MockSwap`, which records calls and maintains in-memory balance ledgers. + +use codec::Encode; +use frame_support::{BoundedVec, assert_noop, assert_ok}; +use sp_core::Pair; +use sp_keyring::Sr25519Keyring as AccountKeyring; +use sp_runtime::{DispatchError, Perbill}; +use subtensor_runtime_common::NetUid; + +use crate::{ + Error, Order, OrderSide, OrderStatus, OrderType, Orders, VersionedOrder, pallet::Event, +}; + +type LimitOrders = crate::pallet::Pallet; + +use super::mock::*; + +/// Check that a specific pallet event was emitted. +fn assert_event(event: Event) { + assert!( + System::events() + .iter() + .any(|r| r.event == RuntimeEvent::LimitOrders(event.clone())), + "expected event not found: {event:?}", + ); +} + +// ───────────────────────────────────────────────────────────────────────────── +// cancel_order +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn cancel_order_signer_can_cancel() { + new_test_ext().execute_with(|| { + let order = VersionedOrder::V1(Order { + signer: alice(), + hotkey: bob(), + netuid: netuid(), + order_type: OrderType::LimitBuy, + amount: 1_000, + limit_price: u64::MAX, + expiry: FAR_FUTURE, + fee_rate: Perbill::zero(), + fee_recipient: fee_recipient(), + relayer: None, + max_slippage: None, + chain_id: 945, + partial_fills_enabled: false, + }); + let id = order_id(&order); + + assert_ok!(LimitOrders::cancel_order( + RuntimeOrigin::signed(alice()), + order + )); + assert_eq!(Orders::::get(id), Some(OrderStatus::Cancelled)); + assert_event(Event::OrderCancelled { + order_id: id, + signer: alice(), + }); + }); +} + +#[test] +fn cancel_order_non_signer_rejected() { + new_test_ext().execute_with(|| { + let order = VersionedOrder::V1(Order { + signer: alice(), + hotkey: bob(), + netuid: netuid(), + order_type: OrderType::LimitBuy, + amount: 1_000, + limit_price: u64::MAX, + expiry: FAR_FUTURE, + fee_rate: Perbill::zero(), + fee_recipient: fee_recipient(), + relayer: None, + max_slippage: None, + chain_id: 945, + partial_fills_enabled: false, + }); + // Bob tries to cancel Alice's order. + assert_noop!( + LimitOrders::cancel_order(RuntimeOrigin::signed(bob()), order), + Error::::Unauthorized + ); + }); +} + +#[test] +fn cancel_order_already_cancelled_rejected() { + new_test_ext().execute_with(|| { + let order = VersionedOrder::V1(Order { + signer: alice(), + hotkey: bob(), + netuid: netuid(), + order_type: OrderType::LimitBuy, + amount: 1_000, + limit_price: u64::MAX, + expiry: FAR_FUTURE, + fee_rate: Perbill::zero(), + fee_recipient: fee_recipient(), + relayer: None, + max_slippage: None, + chain_id: 945, + partial_fills_enabled: false, + }); + let id = order_id(&order); + Orders::::insert(id, OrderStatus::Cancelled); + + assert_noop!( + LimitOrders::cancel_order(RuntimeOrigin::signed(alice()), order), + Error::::OrderAlreadyProcessed + ); + }); +} + +#[test] +fn cancel_order_already_fulfilled_rejected() { + new_test_ext().execute_with(|| { + let order = VersionedOrder::V1(Order { + signer: alice(), + hotkey: bob(), + netuid: netuid(), + order_type: OrderType::LimitBuy, + amount: 1_000, + limit_price: u64::MAX, + expiry: FAR_FUTURE, + fee_rate: Perbill::zero(), + fee_recipient: fee_recipient(), + relayer: None, + max_slippage: None, + chain_id: 945, + partial_fills_enabled: false, + }); + let id = order_id(&order); + Orders::::insert(id, OrderStatus::Fulfilled); + + assert_noop!( + LimitOrders::cancel_order(RuntimeOrigin::signed(alice()), order), + Error::::OrderAlreadyProcessed + ); + }); +} + +#[test] +fn cancel_order_unsigned_rejected() { + new_test_ext().execute_with(|| { + let order = VersionedOrder::V1(Order { + signer: alice(), + hotkey: bob(), + netuid: netuid(), + order_type: OrderType::LimitBuy, + amount: 1_000, + limit_price: u64::MAX, + expiry: FAR_FUTURE, + fee_rate: Perbill::zero(), + fee_recipient: fee_recipient(), + relayer: None, + max_slippage: None, + chain_id: 945, + partial_fills_enabled: false, + }); + assert_noop!( + LimitOrders::cancel_order(RuntimeOrigin::none(), order), + DispatchError::BadOrigin + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// execute_orders +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn execute_orders_buy_order_fulfilled() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + // Price = 1.0 ≤ limit = 2.0 → condition met. + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + 2_000_000_000, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + assert_event(Event::OrderExecuted { + order_id: id, + signer: alice(), + netuid: netuid(), + order_type: OrderType::LimitBuy, + amount_in: 1_000, + amount_out: 0, + }); + }); +} + +#[test] +fn execute_orders_sell_order_fulfilled() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(2.0); + // Price = 2.0, scaled = 2_000_000_000 ≥ limit = 1_000_000_000 → condition met. + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::TakeProfit, + 500, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + assert_event(Event::OrderExecuted { + order_id: id, + signer: alice(), + netuid: netuid(), + order_type: OrderType::TakeProfit, + amount_in: 500, + amount_out: 0, + }); + }); +} + +#[test] +fn execute_orders_stop_loss_order_fulfilled() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(0.5); + // Price = 0.5, scaled = 500_000_000 ≤ limit = 1_000_000_000 → condition met. + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::StopLoss, + 500, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + assert_event(Event::OrderExecuted { + order_id: id, + signer: alice(), + netuid: netuid(), + order_type: OrderType::StopLoss, + amount_in: 500, + amount_out: 0, + }); + }); +} + +#[test] +fn execute_orders_stop_loss_price_not_met_skipped() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(2.0); // price 2.0, scaled=2_000_000_000 > limit 1_000_000_000 → stop loss condition not met + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::StopLoss, + 500, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + assert!(Orders::::get(id).is_none()); + assert_event(Event::OrderSkipped { + order_id: id, + reason: Error::::PriceConditionNotMet.into(), + }); + }); +} + +#[test] +fn execute_orders_expired_order_skipped() { + new_test_ext().execute_with(|| { + MockTime::set(2_000_001); // now > expiry + MockSwap::set_price(1.0); + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + 2_000_000, // expiry in the past + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + // Skipped — storage untouched. + assert!(Orders::::get(id).is_none()); + assert_event(Event::OrderSkipped { + order_id: id, + reason: Error::::OrderExpired.into(), + }); + }); +} + +#[test] +fn execute_orders_price_not_met_skipped() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(5.0); // price 5.0, scaled=5_000_000_000 > limit 2_000_000_000 → buy condition not met + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + 2_000_000_000, // 2.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + assert!(Orders::::get(id).is_none()); + assert_event(Event::OrderSkipped { + order_id: id, + reason: Error::::PriceConditionNotMet.into(), + }); + }); +} + +// Regression tests: with the ×10⁹ scale fix, sub-unity prices can be meaningfully +// expressed as limit_price values. A price of 0.5 TAO/alpha is represented as +// 500_000_000 in ×10⁹ scale, enabling fine-grained TakeProfit thresholds below 1.0. +#[test] +fn take_profit_sub_unity_price_executes_when_limit_met() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + // Market price = 0.5 TAO/alpha → scaled = 500_000_000. + MockSwap::set_price(0.5); + + // limit_price = 400_000_000 (0.4 in ×10⁹ scale). + // TakeProfit condition: scaled_price (500_000_000) >= limit_price (400_000_000) ✓ + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::TakeProfit, + 500, + 400_000_000, // 0.4 in ×10⁹ scale — below current price of 0.5 + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + // Executes: 500_000_000 >= 400_000_000 → condition met. + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + }); +} + +#[test] +fn take_profit_sub_unity_price_skipped_when_limit_not_met() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + // Market price = 0.5 TAO/alpha → scaled = 500_000_000. + MockSwap::set_price(0.5); + + // limit_price = 600_000_000 (0.6 in ×10⁹ scale). + // TakeProfit condition: scaled_price (500_000_000) >= limit_price (600_000_000) → FALSE. + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::TakeProfit, + 500, + 600_000_000, // 0.6 in ×10⁹ scale — above current price of 0.5 + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + // Skipped: 500_000_000 >= 600_000_000 is false. + assert!(Orders::::get(id).is_none()); + assert_event(Event::OrderSkipped { + order_id: id, + reason: Error::::PriceConditionNotMet.into(), + }); + }); +} + +#[test] +fn execute_orders_already_processed_skipped() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + Orders::::insert(id, OrderStatus::Fulfilled); + + // Should succeed (batch-level) but skip this order silently. + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + // Still Fulfilled (not changed). + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + assert_event(Event::OrderSkipped { + order_id: id, + reason: Error::::OrderAlreadyProcessed.into(), + }); + }); +} + +#[test] +fn execute_orders_mixed_batch_valid_and_skipped() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + let valid = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let expired = make_signed_order( + AccountKeyring::Bob, + alice(), + netuid(), + OrderType::LimitBuy, + 500, + u64::MAX, + 500_000, // already expired + Perbill::zero(), + fee_recipient(), + None, + ); + let valid_id = order_id(&valid.order); + let expired_id = order_id(&expired.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![valid, expired]), + false, + )); + + assert_eq!(Orders::::get(valid_id), Some(OrderStatus::Fulfilled)); + assert_event(Event::OrderSkipped { + order_id: expired_id, + reason: Error::::OrderExpired.into(), + }); + }); +} + +#[test] +fn execute_orders_unsigned_rejected() { + new_test_ext().execute_with(|| { + assert_noop!( + LimitOrders::execute_orders(RuntimeOrigin::none(), bounded(vec![]), false), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn execute_orders_buy_with_fee_charges_fee() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + // fee_rate = 1% (10_000_000 parts-per-billion), recipient = fee_recipient(). + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + fee_recipient(), + None, + ); + MockSwap::set_tao_balance(alice(), 1_000); + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + // One buy_alpha call for the net amount (990 TAO after 1% fee). + let buys: Vec<_> = MockSwap::log() + .into_iter() + .filter_map(|c| { + if let super::mock::SwapCall::BuyAlpha { tao, .. } = c { + Some(tao) + } else { + None + } + }) + .collect(); + assert_eq!(buys, vec![990], "main swap must use 990 TAO after 1% fee"); + + // Fee (10 TAO) forwarded directly to fee_recipient via transfer_tao. + assert_eq!(MockSwap::tao_balance(&fee_recipient()), 10); + }); +} + +#[test] +fn execute_orders_sell_with_fee_charges_fee() { + new_test_ext().execute_with(|| { + // fee = 1% (10_000_000 ppb). + // Alice sells 1_000 alpha; pool returns 800 TAO. + // fee_tao = 1% of 800 = 8 TAO, forwarded to fee_recipient via transfer_tao. + // Alice keeps 792 TAO. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_sell_tao_return(800); + MockSwap::set_alpha_balance(alice(), bob(), netuid(), 1_000); + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::TakeProfit, + 1_000, + 0, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + fee_recipient(), + None, + ); + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + // Full 1_000 alpha sold (no alpha deducted for fee). + let sells: Vec<_> = MockSwap::log() + .into_iter() + .filter_map(|c| { + if let super::mock::SwapCall::SellAlpha { alpha, .. } = c { + Some(alpha) + } else { + None + } + }) + .collect(); + assert_eq!(sells, vec![1_000], "full alpha amount must be sold"); + + // fee_recipient received 8 TAO (1% of 800). + assert_eq!(MockSwap::tao_balance(&fee_recipient()), 8); + // Alice kept the remaining 792 TAO. + assert_eq!(MockSwap::tao_balance(&alice()), 792); + }); +} + +#[test] +fn execute_orders_empty_batch_returns_ok() { + new_test_ext().execute_with(|| { + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![]), + false, + )); + }); +} + +#[test] +fn execute_orders_fee_transfer_failure_skips_order() { + new_test_ext().execute_with(|| { + // When the fee transfer fails the entire order is rolled back and emits OrderSkipped. + // This prevents users from exploiting a tight balance to execute swaps fee-free. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(500); + MockSwap::set_tao_balance(alice(), 10_000); + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + fee_recipient(), + None, + ); + + FAIL_FEE_TRANSFER.with(|f| *f.borrow_mut() = true); + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed.clone()]), + false, + )); + FAIL_FEE_TRANSFER.with(|f| *f.borrow_mut() = false); + + // Order was skipped — not stored as Fulfilled. + let id = crate::tests::mock::order_id(&signed.order); + assert!(Orders::::get(id).is_none()); + + // OrderSkipped was emitted with the fee-transfer error as the reason. + assert_event(Event::OrderSkipped { + order_id: id, + reason: DispatchError::CannotLookup, + }); + + // fee_recipient received nothing. + assert_eq!(MockSwap::tao_balance(&fee_recipient()), 0); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// execute_orders — silent-skip behaviour +// ───────────────────────────────────────────────────────────────────────────── + +mod execute_orders_skip_invalid { + use super::*; + + /// A single expired order is silently skipped: the call returns `Ok` and + /// nothing is written to the `Orders` storage map. + #[test] + fn execute_orders_skips_expired_order() { + new_test_ext().execute_with(|| { + MockTime::set(2_000_001); // now > expiry + MockSwap::set_price(1.0); + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + 2_000_000, // expiry in the past + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + // Skipped — storage untouched. + assert!(Orders::::get(id).is_none()); + assert_event(Event::OrderSkipped { + order_id: id, + reason: Error::::OrderExpired.into(), + }); + }); + } + + /// A LimitBuy with `limit_price = 0` (price ceiling below current price) + /// is silently skipped: the call returns `Ok` and nothing is written to + /// the `Orders` storage map. + #[test] + fn execute_orders_skips_price_condition_not_met() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(5.0); // price 5.0 > limit 0 → buy condition not met + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + 0, // price ceiling of 0 — never satisfied at price 5.0 + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + // Skipped — storage untouched. + assert!(Orders::::get(id).is_none()); + assert_event(Event::OrderSkipped { + order_id: id, + reason: Error::::PriceConditionNotMet.into(), + }); + }); + } + + /// A batch containing one valid order and one expired order: the call + /// returns `Ok`, the valid order is stored as `Fulfilled`, and the expired + /// order is NOT written to storage. + #[test] + fn execute_orders_valid_and_invalid_mixed() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + let valid = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let expired = make_signed_order( + AccountKeyring::Bob, + alice(), + netuid(), + OrderType::LimitBuy, + 500, + u64::MAX, + 500_000, // already expired + Perbill::zero(), + fee_recipient(), + None, + ); + let valid_id = order_id(&valid.order); + let expired_id = order_id(&expired.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![valid, expired]), + false, + )); + + // Valid order executed successfully. + assert_eq!(Orders::::get(valid_id), Some(OrderStatus::Fulfilled)); + // Expired order silently skipped — not written to storage. + assert!(Orders::::get(expired_id).is_none()); + assert_event(Event::OrderSkipped { + order_id: expired_id, + reason: Error::::OrderExpired.into(), + }); + }); + } + + /// With `should_fail = true` a single expired order is NOT silently skipped: + /// the whole call fails with `OrderExpired` and storage stays untouched. + #[test] + fn execute_orders_should_fail_expired_order_reverts() { + new_test_ext().execute_with(|| { + MockTime::set(2_000_001); // now > expiry + MockSwap::set_price(1.0); + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + 2_000_000, // expiry in the past + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + // all-or-nothing: the failing order makes the whole call return Err + // and assert_noop! confirms storage is unchanged. + assert_noop!( + LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + true, + ), + Error::::OrderExpired + ); + + assert!(Orders::::get(id).is_none()); + }); + } + + /// With `should_fail = true` a batch containing a VALID order followed by an + /// INVALID (expired) order reverts entirely: the valid order's effects are + /// rolled back, so it is NOT recorded as `Fulfilled` and the relayer's TAO + /// is not consumed. Contrast `execute_orders_valid_and_invalid_mixed`, where + /// the same batch with `should_fail = false` keeps the valid order. + #[test] + fn execute_orders_should_fail_valid_then_invalid_reverts_whole_batch() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + let valid = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let expired = make_signed_order( + AccountKeyring::Bob, + alice(), + netuid(), + OrderType::LimitBuy, + 500, + u64::MAX, + 500_000, // already expired + Perbill::zero(), + fee_recipient(), + None, + ); + let valid_id = order_id(&valid.order); + let expired_id = order_id(&expired.order); + + // The expired order is the second in the batch; with should_fail = true + // its failure reverts the already-executed valid order too. + assert_noop!( + LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![valid, expired]), + true, + ), + Error::::OrderExpired + ); + + // Neither order survived: the valid order's Fulfilled status was rolled back. + assert!(Orders::::get(valid_id).is_none()); + assert!(Orders::::get(expired_id).is_none()); + }); + } + + /// With `should_fail = true` a price-condition-not-met order hard-fails the + /// whole call with `PriceConditionNotMet`, mirroring `execute_batched_orders` + /// rather than the best-effort skip path. + #[test] + fn execute_orders_should_fail_price_condition_not_met_reverts() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(5.0); // price 5.0 > limit 0 → buy condition not met + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + 0, // price ceiling of 0 — never satisfied at price 5.0 + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + + assert_noop!( + LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + true, + ), + Error::::PriceConditionNotMet + ); + + assert!(Orders::::get(id).is_none()); + }); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// execute_batched_orders +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn execute_batched_orders_unsigned_rejected() { + new_test_ext().execute_with(|| { + assert_noop!( + LimitOrders::execute_batched_orders(RuntimeOrigin::none(), netuid(), bounded(vec![])), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn execute_batched_orders_all_invalid_fails() { + new_test_ext().execute_with(|| { + // An expired order causes the whole batch to fail. + MockTime::set(2_000_001); // all expired + let expired = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + 1_000_000, + Perbill::zero(), + fee_recipient(), + None, + ); + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![expired]), + ), + Error::::OrderExpired + ); + }); +} + +#[test] +fn execute_batched_orders_fails_for_wrong_netuid() { + new_test_ext().execute_with(|| { + // An order whose netuid does not match the batch netuid must cause the batch to fail. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(100); + + let wrong_net = make_signed_order( + AccountKeyring::Alice, + bob(), + NetUid::from(99u16), // wrong netuid + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), // batch targets netuid 1 + bounded(vec![wrong_net]), + ), + Error::::OrderNetUidMismatch + ); + }); +} + +#[test] +fn execute_batched_orders_price_condition_not_met_fails_entire_batch() { + new_test_ext().execute_with(|| { + // Price condition not met is a hard-fail in execute_batched_orders — + // unlike execute_orders where it silently skips the order. + MockTime::set(1_000_000); + MockSwap::set_price(100.0); // current price = 100, scaled = 100_000_000_000 + + // LimitBuy requires scaled_price <= limit_price; with limit_price=1_000_000_000 (1.0) this fails. + let order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + 1_000_000_000, // 1.0 in ×10⁹ scale, far below scaled price of 100_000_000_000 + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![order]) + ), + Error::::PriceConditionNotMet + ); + }); +} + +#[test] +fn execute_batched_orders_buy_only_fulfills_orders_and_distributes_alpha() { + new_test_ext().execute_with(|| { + // Setup: + // Alice buys 600 TAO, Bob buys 400 TAO (total 1000 TAO net, fee=0). + // Pool returns 500 alpha (MOCK_BUY_ALPHA_RETURN). + // No sellers → total_alpha = 500. + // Pro-rata: Alice 500*600/1000=300, Bob 500*400/1000=200. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(500); + MockSwap::set_tao_balance(alice(), 600); + MockSwap::set_tao_balance(bob(), 400); + + let alice_order = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 600, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let bob_order = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::LimitBuy, + 400, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let alice_id = order_id(&alice_order.order); + let bob_id = order_id(&bob_order.order); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![alice_order, bob_order]), + )); + + // Both orders fulfilled. + assert_eq!(Orders::::get(alice_id), Some(OrderStatus::Fulfilled)); + assert_eq!(Orders::::get(bob_id), Some(OrderStatus::Fulfilled)); + + // Alpha distributed pro-rata. + assert_eq!(MockSwap::alpha_balance(&alice(), &dave(), netuid()), 300); + assert_eq!(MockSwap::alpha_balance(&bob(), &dave(), netuid()), 200); + + // Summary event. + assert_event(Event::GroupExecutionSummary { + netuid: netuid(), + net_side: OrderSide::Buy, + net_amount: 1_000, + actual_out: 500, + executed_count: 2, + }); + }); +} + +#[test] +fn execute_batched_orders_sell_only_fulfills_orders_and_distributes_tao() { + new_test_ext().execute_with(|| { + // Setup: + // Alice sells 300 alpha, Bob sells 200 alpha (total 500 alpha, fee=0). + // Price = 2.0 → sell_tao_equiv: Alice 600, Bob 400, total 1000. + // Pool returns 800 TAO (MOCK_SELL_TAO_RETURN) for the net 500 alpha. + // No buyers → total_tao = 800 + 0 = 800. + // Pro-rata: Alice 800*600/1000=480, Bob 800*400/1000=320. + MockTime::set(1_000_000); + MockSwap::set_price(2.0); + MockSwap::set_sell_tao_return(800); + MockSwap::set_alpha_balance(alice(), dave(), netuid(), 300); + MockSwap::set_alpha_balance(bob(), dave(), netuid(), 200); + + let alice_order = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::TakeProfit, + 300, + 0, + FAR_FUTURE, // limit=0 → accept any price + Perbill::zero(), + fee_recipient(), + None, + ); + let bob_order = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::TakeProfit, + 200, + 0, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let alice_id = order_id(&alice_order.order); + let bob_id = order_id(&bob_order.order); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![alice_order, bob_order]), + )); + + assert_eq!(Orders::::get(alice_id), Some(OrderStatus::Fulfilled)); + assert_eq!(Orders::::get(bob_id), Some(OrderStatus::Fulfilled)); + + // TAO distributed pro-rata. + assert_eq!(MockSwap::tao_balance(&alice()), 480); + assert_eq!(MockSwap::tao_balance(&bob()), 320); + + assert_event(Event::GroupExecutionSummary { + netuid: netuid(), + net_side: OrderSide::Sell, + net_amount: 500, + actual_out: 800, + executed_count: 2, + }); + }); +} + +#[test] +fn execute_batched_orders_buy_dominant_mixed() { + new_test_ext().execute_with(|| { + // Setup (fee=0, price=2.0 TAO/alpha): + // Buyers: Alice 1000 TAO, Bob 600 TAO → total_buy_net = 1600. + // Sellers: Charlie 200 alpha → sell_tao_equiv = 400 TAO. + // Net (buy-dominant): 1600 - 400 = 1200 TAO goes to pool. + // Pool returns 300 alpha (MOCK_BUY_ALPHA_RETURN). + // total_alpha for buyers = 300 (pool) + 200 (seller passthrough) = 500. + // Pro-rata buyers (by buy_net TAO): + // Alice: 500 * 1000/1600 = 312 alpha + // Bob: 500 * 600/1600 = 187 alpha + // (dust = 1 alpha stays in pallet) + // Sellers (buy-dominant branch): total_tao = total_sell_tao_equiv = 400. + // Charlie: 400 * 400/400 = 400 TAO. + MockTime::set(1_000_000); + MockSwap::set_price(2.0); + MockSwap::set_buy_alpha_return(300); + MockSwap::set_tao_balance(alice(), 1_000); + MockSwap::set_tao_balance(bob(), 600); + MockSwap::set_alpha_balance(charlie(), dave(), netuid(), 200); + + let alice_buy = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let bob_buy = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::LimitBuy, + 600, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let charlie_sell = make_signed_order( + AccountKeyring::Charlie, + dave(), + netuid(), + OrderType::TakeProfit, + 200, + 0, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(dave()), + netuid(), + bounded(vec![alice_buy, bob_buy, charlie_sell]), + )); + + assert_eq!(MockSwap::alpha_balance(&alice(), &dave(), netuid()), 312); + assert_eq!(MockSwap::alpha_balance(&bob(), &dave(), netuid()), 187); + assert_eq!(MockSwap::tao_balance(&charlie()), 400); + + assert_event(Event::GroupExecutionSummary { + netuid: netuid(), + net_side: OrderSide::Buy, + net_amount: 1_200, + actual_out: 300, + executed_count: 3, + }); + }); +} + +#[test] +fn execute_batched_orders_sell_dominant_mixed() { + new_test_ext().execute_with(|| { + // Setup (fee=0, price=2.0 TAO/alpha): + // Buyers: Alice 200 TAO → total_buy_net = 200. + // Sellers: Bob 300 alpha, Charlie 200 alpha → total_sell_net = 500. + // sell_tao_equiv: Bob 600, Charlie 400, total 1000. + // Net (sell-dominant): buy_alpha_equiv = 200/2 = 100 alpha; + // residual sell alpha = 500 - 100 = 400 alpha → pool returns 300 TAO. + // total_tao for sellers = 300 (pool) + 200 (buy passthrough) = 500 TAO. + // Pro-rata sellers (by sell_tao_equiv): + // Bob: 500 * 600/1000 = 300 TAO + // Charlie: 500 * 400/1000 = 200 TAO + // total_alpha for buyers = buy_net / price = 200/2 = 100 alpha. + // Alice: 100 * 200/200 = 100 alpha. + MockTime::set(1_000_000); + MockSwap::set_price(2.0); + MockSwap::set_sell_tao_return(300); + MockSwap::set_tao_balance(alice(), 200); + MockSwap::set_alpha_balance(bob(), dave(), netuid(), 300); + MockSwap::set_alpha_balance(charlie(), dave(), netuid(), 200); + + let alice_buy = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 200, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let bob_sell = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::TakeProfit, + 300, + 0, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let charlie_sell = make_signed_order( + AccountKeyring::Charlie, + dave(), + netuid(), + OrderType::TakeProfit, + 200, + 0, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(dave()), + netuid(), + bounded(vec![alice_buy, bob_sell, charlie_sell]), + )); + + assert_eq!(MockSwap::alpha_balance(&alice(), &dave(), netuid()), 100); + assert_eq!(MockSwap::tao_balance(&bob()), 300); + assert_eq!(MockSwap::tao_balance(&charlie()), 200); + + assert_event(Event::GroupExecutionSummary { + netuid: netuid(), + net_side: OrderSide::Sell, + net_amount: 400, + actual_out: 300, + executed_count: 3, + }); + }); +} + +#[test] +fn execute_batched_orders_fee_forwarded_to_collector() { + new_test_ext().execute_with(|| { + // fee = 1% (10_000_000 ppb). + // Alice buys 1000 TAO: fee = 10, net = 990. + // Pool returns 500 alpha for 990 TAO. + // collect_fees transfers 10 TAO (buy fee) to fee_recipient. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(500); + + let alice_buy = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + fee_recipient(), + None, + ); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![alice_buy]), + )); + + // Fee recipient received the buy-side fee. + assert_eq!(MockSwap::tao_balance(&fee_recipient()), 10); + }); +} + +#[test] +fn execute_batched_orders_fails_for_cancelled_order() { + new_test_ext().execute_with(|| { + // A cancelled order is already processed; including it in the batch must cause a hard failure. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(100); + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&signed.order); + Orders::::insert(id, OrderStatus::Cancelled); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![signed]), + ), + Error::::OrderCancelled + ); + + // Still cancelled, not changed to Fulfilled. + assert_eq!(Orders::::get(id), Some(OrderStatus::Cancelled)); + }); +} + +#[test] +fn execute_batched_orders_fees_charged_on_both_sides_when_matched_internally() { + new_test_ext().execute_with(|| { + // fee = 1% (10_000_000 ppb), price = 1.0 TAO/alpha. + // + // Alice buys 1_000 TAO → buy fee = 10 TAO, net = 990 TAO. + // Bob sells 1_000 alpha → sell_tao_equiv = 1_000 TAO. + // + // sell-dominant: residual = 1_000 - 990 = 10 alpha sent to pool. + // Pool returns 9 TAO (mocked) for that residual. + // total_tao for sellers = 9 (pool) + 990 (buy passthrough) = 999. + // Bob gross_share = 999 * 1_000/1_000 = 999. + // Sell fee = mul_floor(1%, 999) = floor(9.99) = 9; Bob nets 990 TAO. + // fee_recipient total = buy_fee(10) + sell_fee(9) = 19 TAO. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_sell_tao_return(9); + MockSwap::set_tao_balance(alice(), 1_000); + MockSwap::set_alpha_balance(bob(), dave(), netuid(), 1_000); + + let alice_buy = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + fee_recipient(), + None, + ); + let bob_sell = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::TakeProfit, + 1_000, + 0, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + fee_recipient(), + None, + ); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![alice_buy, bob_sell]), + )); + + // Both sides charged: fee_recipient gets buy fee (10) + sell fee (9) = 19. + assert_eq!(MockSwap::tao_balance(&fee_recipient()), 19); + // Bob receives 990 TAO after sell-side fee (999 gross - 9 fee). + assert_eq!(MockSwap::tao_balance(&bob()), 990); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// net_pool_swap – SwapReturnedZero errors +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn execute_batched_orders_buy_zero_alpha_returns_error() { + new_test_ext().execute_with(|| { + // buy_alpha returns 0 alpha for a non-zero TAO input → SwapReturnedZero. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(0); // pool gives back nothing + MockSwap::set_tao_balance(alice(), 1_000); + + let order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![order]), + ), + Error::::SwapReturnedZero + ); + }); +} + +#[test] +fn execute_batched_orders_sell_zero_tao_returns_error() { + new_test_ext().execute_with(|| { + // sell_alpha returns 0 TAO for a non-zero alpha input → SwapReturnedZero. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_sell_tao_return(0); // pool gives back nothing + MockSwap::set_alpha_balance(alice(), bob(), netuid(), 1_000); + + let order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::TakeProfit, + 1_000, + 0, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![order]), + ), + Error::::SwapReturnedZero + ); + }); +} + +#[test] +fn execute_batched_orders_sell_alpha_respects_swap_fail() { + new_test_ext().execute_with(|| { + // sell_alpha should propagate DispatchError when MOCK_SWAP_FAIL is set. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_swap_fail(true); + MockSwap::set_alpha_balance(alice(), bob(), netuid(), 1_000); + + let order = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::TakeProfit, + 1_000, + 0, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![order]), + ), + DispatchError::Other("pool error") + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// fee routing – multiple recipients +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn execute_batched_orders_fees_routed_to_different_recipients() { + new_test_ext().execute_with(|| { + // Alice and Bob both buy; Alice's fee goes to charlie(), Bob's to dave(). + // fee = 1% for both orders. + // Alice buys 1_000 TAO: fee = 10 → charlie(). + // Bob buys 1_000 TAO: fee = 10 → dave(). + // Pool returns 900 alpha total for 1_980 TAO net. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(900); + MockSwap::set_tao_balance(alice(), 1_000); + MockSwap::set_tao_balance(bob(), 1_000); + + let alice_buy = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + charlie(), + None, + ); + let bob_buy = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + dave(), + None, + ); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![alice_buy, bob_buy]), + )); + + // Each recipient gets exactly their order's fee. + assert_eq!( + MockSwap::tao_balance(&charlie()), + 10, + "charlie gets Alice's fee" + ); + assert_eq!(MockSwap::tao_balance(&dave()), 10, "dave gets Bob's fee"); + }); +} + +#[test] +fn execute_batched_orders_fees_batched_for_shared_recipient() { + new_test_ext().execute_with(|| { + // Both Alice and Bob's fees go to the same recipient (charlie()). + // Expect a single combined transfer of 20 TAO to charlie(). + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(900); + MockSwap::set_tao_balance(alice(), 1_000); + MockSwap::set_tao_balance(bob(), 1_000); + + let alice_buy = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + charlie(), + None, + ); + let bob_buy = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + charlie(), + None, + ); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![alice_buy, bob_buy]), + )); + + // One combined transfer: charlie() receives 10 + 10 = 20 TAO. + let fee_transfers: Vec<_> = MockSwap::tao_transfers() + .into_iter() + .filter(|(_, to, _)| to == &charlie()) + .collect(); + assert_eq!( + fee_transfers.len(), + 1, + "single transfer to shared recipient" + ); + assert_eq!(fee_transfers[0].2, 20, "combined fee = 20 TAO"); + }); +} + +/// 4 orders split across 2 fee recipients. +/// +/// Orders: +/// Alice LimitBuy 1_000 TAO fee_recipient = ferdie (buy-fee collector) +/// Bob LimitBuy 1_000 TAO fee_recipient = ferdie (buy-fee collector) +/// Charlie TakeProfit 1_000 α fee_recipient = fee_recipient() (sell-fee collector) +/// Eve TakeProfit 1_000 α fee_recipient = fee_recipient() (sell-fee collector) +/// +/// Neither ferdie nor fee_recipient() are order signers, so every TAO transfer +/// to those accounts is exclusively a fee transfer — making the single-transfer +/// assertion unambiguous. +/// +/// At price 1.0 (1 TAO = 1 α), fee = 1%: +/// net buy TAO = (1_000 - 10) + (1_000 - 10) = 1_980 +/// sell α equiv = 2_000 TAO → sell-dominant, residual = 20 α → pool +/// pool returns 18 TAO for residual +/// total TAO for sellers = 18 + 1_980 = 1_998 +/// each seller gross_share = 1_998 * 1_000 / 2_000 = 999 +/// sell fee = mul_floor(1%, 999) = floor(9.99) = 9 TAO each +/// +/// Expected: +/// ferdie receives 10 (Alice) + 10 (Bob) = 20 TAO (1 transfer) +/// fee_recipient() receives 9 (Charlie) + 9 (Eve) = 18 TAO (1 transfer) +#[test] +fn execute_batched_orders_four_orders_two_fee_recipients() { + new_test_ext().execute_with(|| { + let ferdie = AccountKeyring::Ferdie.to_account_id(); + let eve = AccountKeyring::Eve.to_account_id(); + + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_sell_tao_return(18); + MockSwap::set_tao_balance(alice(), 1_000); + MockSwap::set_tao_balance(bob(), 1_000); + MockSwap::set_alpha_balance(charlie(), dave(), netuid(), 1_000); + MockSwap::set_alpha_balance(eve.clone(), dave(), netuid(), 1_000); + + let alice_buy = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + ferdie.clone(), + None, + ); + let bob_buy = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + ferdie.clone(), + None, + ); + let charlie_sell = make_signed_order( + AccountKeyring::Charlie, + dave(), + netuid(), + OrderType::TakeProfit, + 1_000, + 0, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + fee_recipient(), + None, + ); + let eve_sell = make_signed_order( + AccountKeyring::Eve, + dave(), + netuid(), + OrderType::TakeProfit, + 1_000, + 0, + FAR_FUTURE, + Perbill::from_parts(10_000_000), // 1% + fee_recipient(), + None, + ); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(alice()), + netuid(), + bounded(vec![alice_buy, bob_buy, charlie_sell, eve_sell]), + )); + + // ferdie collects Alice's and Bob's buy fees: 10 + 10 = 20 TAO in one transfer. + let ferdie_transfers: Vec<_> = MockSwap::tao_transfers() + .into_iter() + .filter(|(_, to, _)| to == &ferdie) + .collect(); + assert_eq!(ferdie_transfers.len(), 1, "single transfer to ferdie"); + assert_eq!( + ferdie_transfers[0].2, 20, + "ferdie receives 20 TAO in buy fees" + ); + + // fee_recipient() collects Charlie's and Eve's sell fees: 10 + 10 = 20 TAO in one transfer. + let fp_transfers: Vec<_> = MockSwap::tao_transfers() + .into_iter() + .filter(|(_, to, _)| to == &fee_recipient()) + .collect(); + assert_eq!(fp_transfers.len(), 1, "single transfer to fee_recipient"); + assert_eq!( + fp_transfers[0].2, 18, + "fee_recipient receives 18 TAO in sell fees" + ); + }); +} + +/// A mixed batch (buy + sell) must not rate-limit the pallet intermediary +/// account during asset collection, which would otherwise block the +/// subsequent alpha distribution to buyers. +/// +/// Regression test: previously `transfer_staked_alpha` with a single +/// `apply_limits: true` flag set the rate-limit on `to_coldkey` (pallet) +/// during collection, then the distribution step checked `from_coldkey` +/// (pallet) and failed with `StakingOperationRateLimitExceeded`. +#[test] +fn execute_batched_orders_mixed_batch_does_not_rate_limit_pallet_intermediary() { + new_test_ext().execute_with(|| { + // Alice buys 1_000 TAO; Bob sells 500 alpha. + // Buy-dominant: residual 500 TAO goes to pool, pool returns 400 alpha. + // Total alpha = 400 (pool) + 500 (Bob passthrough) = 900 → all to Alice. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(400); + MockSwap::set_tao_balance(alice(), 1_000); + MockSwap::set_alpha_balance(bob(), dave(), netuid(), 500); + + let buy = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let sell = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::TakeProfit, + 500, + 0, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + + // Must succeed: collecting Bob's alpha must not rate-limit the pallet + // intermediary, so distributing alpha to Alice is not blocked. + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![buy, sell]), + )); + + // Alice received staked alpha. + assert!( + MockSwap::alpha_balance(&alice(), &dave(), netuid()) > 0, + "alice should hold staked alpha after the buy" + ); + // Alice is rate-limited after receiving stake (set_receiver_limit=true). + assert!( + MockSwap::is_rate_limited(&dave(), &alice(), netuid()), + "alice should be rate-limited after receiving stake" + ); + // Bob's hotkey on the pallet side is NOT rate-limited (set_receiver_limit=false on collect). + assert!( + !MockSwap::is_rate_limited(&dave(), &bob(), netuid()), + "bob's rate-limit should not be set by the collection step" + ); + }); +} + +/// Root changes the pallet status, extrinsics are filtered +#[test] +fn root_disables_and_extrinsics_are_filtered() { + new_test_ext().execute_with(|| { + // Disable the pallet + assert_ok!(LimitOrders::set_pallet_status(RuntimeOrigin::root(), false)); + + let sell = make_signed_order( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::TakeProfit, + 500, + 0, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + + // Must succeed: collecting Bob's alpha must not rate-limit the pallet + // intermediary, so distributing alpha to Alice is not blocked. + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![sell]) + ), + Error::::LimitOrdersDisabled + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// max_slippage — execute_orders passes effective_swap_limit to pool +// ───────────────────────────────────────────────────────────────────────────── + +/// Build a signed order with a specific `max_slippage` value. +#[allow(clippy::too_many_arguments)] +fn make_signed_order_with_slippage( + keyring: AccountKeyring, + hotkey: AccountId, + netuid: subtensor_runtime_common::NetUid, + order_type: OrderType, + amount: u64, + limit_price: u64, + expiry: u64, + fee_rate: sp_runtime::Perbill, + fee_recipient: AccountId, + max_slippage: Option, +) -> crate::SignedOrder { + let order = crate::VersionedOrder::V1(crate::Order { + signer: keyring.to_account_id(), + hotkey, + netuid, + order_type, + amount, + limit_price, + expiry, + fee_rate, + fee_recipient, + relayer: None, + max_slippage, + chain_id: 945, + partial_fills_enabled: false, + }); + let sig = keyring.pair().sign(&order.encode()); + crate::SignedOrder { + order, + signature: sp_runtime::MultiSignature::Sr25519(sig), + partial_fill: None, + } +} + +#[test] +fn execute_orders_buy_no_slippage_passes_u64_max_to_pool() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + let signed = make_signed_order_with_slippage( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, // no slippage → u64::MAX ceiling + ); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + // Pool must have been called with u64::MAX as price ceiling. + assert_eq!(MockSwap::buy_alpha_limit_prices(), vec![u64::MAX]); + }); +} + +#[test] +fn execute_orders_sell_no_slippage_passes_zero_to_pool() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(2.0); + + let signed = make_signed_order_with_slippage( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::TakeProfit, + 500, + 1_000_000_000, // 1.0 in ×10⁹ scale; price=2.0 (scaled=2_000_000_000) >= 1_000_000_000 ✓ + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, // no slippage → 0 floor + ); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + assert_eq!(MockSwap::sell_alpha_limit_prices(), vec![0]); + }); +} + +#[test] +fn execute_orders_buy_one_percent_slippage_passes_ceiling_to_pool() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + // limit_price=1_000_000_000 (1.0 in ×10⁹), 1% slippage → ceiling = 1_010_000_000. + let signed = make_signed_order_with_slippage( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + 1_000_000_000, // 1.0 in ×10⁹ scale; price=1.0 (scaled=1_000_000_000) <= 1_000_000_000 ✓ + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(1)), + ); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + assert_eq!(MockSwap::buy_alpha_limit_prices(), vec![1_010_000_000]); + }); +} + +#[test] +fn execute_orders_sell_one_percent_slippage_passes_floor_to_pool() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + // Price must be >= limit_price for TakeProfit to trigger. + MockSwap::set_price(2_000.0); + + // limit_price=1_000_000_000 (1.0 in ×10⁹), 1% slippage → floor = 990_000_000. + let signed = make_signed_order_with_slippage( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::TakeProfit, + 500, + 1_000_000_000, // 1.0 in ×10⁹ scale; price=2000.0 (scaled=2T) >= 1_000_000_000 ✓ + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(1)), + ); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + assert_eq!(MockSwap::sell_alpha_limit_prices(), vec![990_000_000]); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// max_slippage — execute_batched_orders aggregates tightest constraint +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn execute_batched_orders_buy_dominant_uses_min_ceiling() { + new_test_ext().execute_with(|| { + // 3 buy orders with different slippage constraints. + // Alice: limit=1_000_000_000, 2% → ceiling=1_020_000_000 + // Bob: limit=1_000_000_000, 1% → ceiling=1_010_000_000 ← tightest + // Charlie (as signer, not relayer): limit=1_000_000_000, 3% → ceiling=1_030_000_000 + // Expected pool price_limit = min(1_020_000_000, 1_010_000_000, 1_030_000_000) = 1_010_000_000. + // price=1.0, scaled=1_000_000_000 <= 1_000_000_000 ✓ for all LimitBuy orders. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(500); + MockSwap::set_tao_balance(alice(), 600); + MockSwap::set_tao_balance(bob(), 200); + MockSwap::set_tao_balance(dave(), 200); + + let alice_order = make_signed_order_with_slippage( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 600, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(2)), // ceiling = 1_020_000_000 + ); + let bob_order = make_signed_order_with_slippage( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::LimitBuy, + 200, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(1)), // ceiling = 1_010_000_000 ← tightest + ); + let dave_order = make_signed_order_with_slippage( + AccountKeyring::Dave, + dave(), + netuid(), + OrderType::LimitBuy, + 200, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(3)), // ceiling = 1_030_000_000 + ); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![alice_order, bob_order, dave_order]), + )); + + // Net pool swap must have been called with the tightest ceiling = 1_010_000_000. + assert_eq!(MockSwap::buy_alpha_limit_prices(), vec![1_010_000_000]); + }); +} + +#[test] +fn execute_batched_orders_sell_dominant_uses_max_floor() { + new_test_ext().execute_with(|| { + // 3 sell orders with different slippage constraints. + // Alice: limit=1_000_000_000, 3% → floor=970_000_000 + // Bob: limit=1_000_000_000, 1% → floor=990_000_000 ← tightest (highest floor) + // Dave: limit=1_000_000_000, 2% → floor=980_000_000 + // Expected pool price_limit = max(970_000_000, 990_000_000, 980_000_000) = 990_000_000. + // Price must be >= limit_price=1_000_000_000 (1.0 in ×10⁹) for TakeProfit to trigger. + // price=2000.0, scaled=2_000_000_000_000 >= 1_000_000_000 ✓. + MockTime::set(1_000_000); + MockSwap::set_price(2_000.0); + MockSwap::set_sell_tao_return(500); + MockSwap::set_alpha_balance(alice(), dave(), netuid(), 600); + MockSwap::set_alpha_balance(bob(), dave(), netuid(), 200); + MockSwap::set_alpha_balance(dave(), dave(), netuid(), 200); + + let alice_order = make_signed_order_with_slippage( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::TakeProfit, + 600, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(3)), // floor = 970_000_000 + ); + let bob_order = make_signed_order_with_slippage( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::TakeProfit, + 200, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(1)), // floor = 990_000_000 ← tightest + ); + let dave_order = make_signed_order_with_slippage( + AccountKeyring::Dave, + dave(), + netuid(), + OrderType::TakeProfit, + 200, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(2)), // floor = 980_000_000 + ); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![alice_order, bob_order, dave_order]), + )); + + // Net pool swap must have been called with the tightest floor = 990_000_000. + assert_eq!(MockSwap::sell_alpha_limit_prices(), vec![990_000_000]); + }); +} + +#[test] +fn execute_batched_orders_no_slippage_uses_unconstrained_limits() { + new_test_ext().execute_with(|| { + // Orders without max_slippage should pass u64::MAX (buy) or 0 (sell). + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(500); + MockSwap::set_tao_balance(alice(), 1_000); + + let order = make_signed_order_with_slippage( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![order]), + )); + + assert_eq!(MockSwap::buy_alpha_limit_prices(), vec![u64::MAX]); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// max_slippage — mixed order type coexistence +// ───────────────────────────────────────────────────────────────────────────── + +/// Sell-dominant batch: TakeProfit orders (with slippage) + StopLoss (no slippage). +/// +/// TakeProfit orders set meaningful floors; StopLoss contributes 0 (no constraint). +/// pool_price_limit = max(take_floors..., 0s) = max(take_floors). +/// All three orders are fulfilled. +#[test] +fn execute_batched_orders_takeprofit_and_stoploss_coexist_sell_dominant() { + new_test_ext().execute_with(|| { + // Price = 2000 — scaled = 2_000_000_000_000. + // TakeProfit triggers when scaled_price >= limit_price (2T >= 1_000_000_000 ✓). + // StopLoss triggers when scaled_price <= limit_price (2T <= 5_000_000_000_000 ✓). + MockTime::set(1_000_000); + MockSwap::set_price(2_000.0); + MockSwap::set_sell_tao_return(500); + + // Alice TakeProfit: limit=1_000_000_000 (1.0), 3% → floor=970_000_000. + // Bob TakeProfit: limit=1_000_000_000 (1.0), 1% → floor=990_000_000. ← tightest + // Dave StopLoss: limit=5_000_000_000_000 (5000.0), None → floor=0. + MockSwap::set_alpha_balance(alice(), dave(), netuid(), 600); + MockSwap::set_alpha_balance(bob(), dave(), netuid(), 200); + MockSwap::set_alpha_balance(dave(), alice(), netuid(), 200); + + let alice_order = make_signed_order_with_slippage( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::TakeProfit, + 600, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(3)), + ); + let bob_order = make_signed_order_with_slippage( + AccountKeyring::Bob, + dave(), + netuid(), + OrderType::TakeProfit, + 200, + 1_000_000_000, // 1.0 in ×10⁹ scale + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(1)), + ); + let dave_stoploss = make_signed_order_with_slippage( + AccountKeyring::Dave, + alice(), + netuid(), + OrderType::StopLoss, + 200, + 5_000_000_000_000, // 5000.0 in ×10⁹ scale; scaled_price 2T <= 5T ✓ + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, // StopLoss: no slippage → floor=0, does not constrain pool + ); + + let alice_id = order_id(&alice_order.order); + let bob_id = order_id(&bob_order.order); + let dave_id = order_id(&dave_stoploss.order); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![alice_order, bob_order, dave_stoploss]), + )); + + // All three fulfilled. + assert_eq!(Orders::::get(alice_id), Some(OrderStatus::Fulfilled)); + assert_eq!(Orders::::get(bob_id), Some(OrderStatus::Fulfilled)); + assert_eq!(Orders::::get(dave_id), Some(OrderStatus::Fulfilled)); + + // Pool called once with the tightest TakeProfit floor (990_000_000), not 0 from StopLoss. + assert_eq!(MockSwap::sell_alpha_limit_prices(), vec![990_000_000]); + }); +} + +/// Buy-dominant batch: LimitBuy orders (with slippage) dominant + StopLoss (no slippage) on offset side. +/// +/// The offset StopLoss is settled internally at spot price; it does not contribute +/// to the pool's price ceiling (which comes only from the dominant buy side). +/// pool_price_limit = min(buy_ceilings) = 1_010_000_000. +#[test] +fn execute_batched_orders_limitbuy_and_stoploss_offset_coexist_buy_dominant() { + new_test_ext().execute_with(|| { + // Price = 1.0, scaled = 1_000_000_000. + // LimitBuy triggers (scaled <= limit ✓). StopLoss triggers (scaled <= limit ✓). + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(900); + + // Alice LimitBuy: limit=1_000_000_000 (1.0), 2% → ceiling=1_020_000_000. + // Bob LimitBuy: limit=1_000_000_000 (1.0), 1% → ceiling=1_010_000_000. ← tightest + // Dave StopLoss: limit=2_000_000_000 (2.0), None → floor=0 (offset side, not used for pool limit). + MockSwap::set_tao_balance(alice(), 600); + MockSwap::set_tao_balance(bob(), 400); + MockSwap::set_alpha_balance(dave(), alice(), netuid(), 100); + + let alice_order = make_signed_order_with_slippage( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 600, + 1_000_000_000, // 1.0 in ×10⁹ scale; scaled=1_000_000_000 <= 1_000_000_000 ✓ + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(2)), + ); + let bob_order = make_signed_order_with_slippage( + AccountKeyring::Bob, + bob(), + netuid(), + OrderType::LimitBuy, + 400, + 1_000_000_000, // 1.0 in ×10⁹ scale; scaled=1_000_000_000 <= 1_000_000_000 ✓ + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(1)), + ); + let dave_stoploss = make_signed_order_with_slippage( + AccountKeyring::Dave, + alice(), + netuid(), + OrderType::StopLoss, + 100, + 2_000_000_000, // 2.0 in ×10⁹ scale; scaled=1_000_000_000 <= 2_000_000_000 ✓ + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, // StopLoss: no slippage; settled at spot, never constrains pool ceiling + ); + + let alice_id = order_id(&alice_order.order); + let bob_id = order_id(&bob_order.order); + let dave_id = order_id(&dave_stoploss.order); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![alice_order, bob_order, dave_stoploss]), + )); + + // All three fulfilled. + assert_eq!(Orders::::get(alice_id), Some(OrderStatus::Fulfilled)); + assert_eq!(Orders::::get(bob_id), Some(OrderStatus::Fulfilled)); + assert_eq!(Orders::::get(dave_id), Some(OrderStatus::Fulfilled)); + + // Pool buy called with min(1_020_000_000, 1_010_000_000) = 1_010_000_000. StopLoss's floor (0) is ignored on buy side. + assert_eq!(MockSwap::buy_alpha_limit_prices(), vec![1_010_000_000]); + }); +} + +/// StopLoss with a narrow slippage sets an effective floor above the current market price, +/// making the pool swap impossible and failing the entire batch. +/// +/// This demonstrates Issue 1 from the design: relayers should not apply max_slippage to +/// StopLoss orders. StopLoss triggers when price has already fallen; a floor derived from +/// the (higher) trigger threshold will almost always exceed the actual market price. +#[test] +fn execute_batched_orders_stoploss_narrow_slippage_breaks_batch() { + new_test_ext().execute_with(|| { + // StopLoss: limit=100_000_000_000 (100.0 in ×10⁹), triggers at price=50 (scaled=50_000_000_000 ≤ 100_000_000_000 ✓). + // 1% slippage → floor=99_000_000_000. Market is at 50 → pool cannot deliver ≥99_000_000_000. + MockTime::set(1_000_000); + MockSwap::set_price(50.0); + MockSwap::set_sell_tao_return(100); // non-zero so SwapReturnedZero is not the cause + MockSwap::set_enforce_price_limit(true); + MockSwap::set_alpha_balance(dave(), alice(), netuid(), 200); + + let stoploss = make_signed_order_with_slippage( + AccountKeyring::Dave, + alice(), + netuid(), + OrderType::StopLoss, + 200, + 100_000_000_000, // 100.0 in ×10⁹ scale; scaled=50_000_000_000 <= 100_000_000_000 ✓ + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(1)), // floor=99_000_000_000, but market=50 → pool rejects + ); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![stoploss]), + ), + DispatchError::Other("price limit exceeded") + ); + }); +} + +/// Same StopLoss scenario through execute_orders (best-effort): the order is silently +/// skipped rather than failing the whole call. +/// +/// Note: `DispatchError::Other` has `#[codec(skip)]` on its string field, so the reason +/// string is lost when stored in the event log. We verify the skip via storage absence +/// and by asserting the floor (99_000_000_000 = 100_000_000_000 - 1%) was actually passed +/// to the pool — which is what caused the rejection. The `execute_batched_orders` variant +/// below uses `assert_noop!` (checks the return value directly, no storage round-trip) and +/// can verify the string. +#[test] +fn execute_orders_stoploss_narrow_slippage_skips_order() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(50.0); + MockSwap::set_sell_tao_return(100); + MockSwap::set_enforce_price_limit(true); + + let stoploss = make_signed_order_with_slippage( + AccountKeyring::Dave, + alice(), + netuid(), + OrderType::StopLoss, + 200, + 100_000_000_000, // 100.0 in ×10⁹ scale; scaled=50_000_000_000 <= 100_000_000_000 ✓ + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_percent(1)), // floor=99_000_000_000, but market=50 → pool rejects + ); + let id = order_id(&stoploss.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![stoploss]), + false, + )); + + // Order not stored — pool rejected the floor. + assert!(Orders::::get(id).is_none()); + + // An OrderSkipped event must have been emitted for this order. + assert!( + System::events().iter().any(|r| matches!( + &r.event, + RuntimeEvent::LimitOrders(Event::OrderSkipped { order_id, .. }) + if *order_id == id + )), + "expected OrderSkipped event for this order" + ); + + // The sell was attempted with the correct floor (99_000_000_000 = 100_000_000_000 - 1%). + // This is the value that exceeded the market price and caused the rejection. + assert_eq!(MockSwap::sell_alpha_limit_prices(), vec![99_000_000_000]); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// relayer enforcement +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn execute_orders_wrong_relayer_skipped() { + new_test_ext().execute_with(|| { + // Order locks execution to charlie(); submitting as bob() must be silently skipped. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(BoundedVec::truncate_from(vec![charlie()])), // only charlie may relay this order + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(bob()), // wrong relayer + bounded(vec![signed]), + false, + )); + + // Order not stored — it was skipped. + assert!(Orders::::get(id).is_none()); + assert_event(Event::OrderSkipped { + order_id: id, + reason: Error::::RelayerMissMatch.into(), + }); + }); +} + +#[test] +fn execute_orders_correct_relayer_executed() { + new_test_ext().execute_with(|| { + // Same order submitted by the designated relayer (charlie) — must succeed. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(BoundedVec::truncate_from(vec![charlie()])), // charlie is the designated relayer + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), // correct relayer + bounded(vec![signed]), + false, + )); + + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + assert_event(Event::OrderExecuted { + order_id: id, + signer: alice(), + netuid: netuid(), + order_type: OrderType::LimitBuy, + amount_in: 1_000, + amount_out: 0, + }); + }); +} + +#[test] +fn execute_batched_orders_wrong_relayer_fails_entire_batch() { + new_test_ext().execute_with(|| { + // In execute_batched_orders a relayer mismatch is a hard failure — the + // whole call is reverted, unlike the best-effort skip in execute_orders. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(BoundedVec::truncate_from(vec![charlie()])), // only charlie may relay this order + ); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(bob()), // wrong relayer + netuid(), + bounded(vec![signed]) + ), + Error::::RelayerMissMatch + ); + }); +} + +#[test] +fn execute_batched_orders_correct_relayer_succeeds() { + new_test_ext().execute_with(|| { + // Same order submitted by the designated relayer — must execute and + // distribute alpha to the buyer. + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(1_000); + MockSwap::set_tao_balance(alice(), 1_000); + + let signed = make_signed_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(BoundedVec::truncate_from(vec![charlie()])), // charlie is the designated relayer + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), // correct relayer + netuid(), + bounded(vec![signed]) + )); + + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Partial fills — execute_orders +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn execute_orders_partial_fill_sets_partially_filled_status() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_tao_balance(alice(), 1_000); + + // Order for 1000 TAO; relayer is charlie (required for partial fills). + let signed = make_partial_fill_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + charlie(), + 400, // fill 400 out of 1000 + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + assert_eq!( + Orders::::get(id), + Some(OrderStatus::PartiallyFilled(400)) + ); + }); +} + +#[test] +fn execute_orders_second_partial_fill_completes_order() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_tao_balance(alice(), 1_000); + + let signed_first = make_partial_fill_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + charlie(), + 600, + ); + let id = order_id(&signed_first.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed_first.clone()]), + false, + )); + assert_eq!( + Orders::::get(id), + Some(OrderStatus::PartiallyFilled(600)) + ); + + // Re-submit the same signed order payload with a different partial_fill amount. + let mut signed_second = signed_first.clone(); + signed_second.partial_fill = Some(400); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed_second]), + false, + )); + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + }); +} + +#[test] +fn execute_orders_partial_fill_without_relayer_skipped() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_tao_balance(alice(), 1_000); + + // Build an order with partial_fills_enabled but no relayer set. + let inner = crate::Order { + signer: alice(), + hotkey: bob(), + netuid: netuid(), + order_type: OrderType::LimitBuy, + amount: 1_000, + limit_price: u64::MAX, + expiry: FAR_FUTURE, + fee_rate: Perbill::zero(), + fee_recipient: fee_recipient(), + relayer: None, // <-- no relayer + max_slippage: None, + chain_id: 945, + partial_fills_enabled: true, + }; + let versioned = VersionedOrder::V1(inner); + let sig = AccountKeyring::Alice.pair().sign(&versioned.encode()); + let signed = crate::SignedOrder { + order: versioned, + signature: sp_runtime::MultiSignature::Sr25519(sig), + partial_fill: Some(400), + }; + let id = order_id(&signed.order); + + // The order is skipped (best-effort), not reverting the batch. + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed]), + false, + )); + + // Nothing written to storage. + assert_eq!(Orders::::get(id), None); + assert_event(Event::OrderSkipped { + order_id: id, + reason: Error::::RelayerRequiredForPartialFill.into(), + }); + }); +} + +#[test] +fn execute_orders_partial_fill_exceeding_remaining_is_skipped() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_tao_balance(alice(), 1_000); + + // Pre-fill 700 of 1000. + let signed = make_partial_fill_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + charlie(), + 700, + ); + let id = order_id(&signed.order); + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![signed.clone()]), + false, + )); + assert_eq!( + Orders::::get(id), + Some(OrderStatus::PartiallyFilled(700)) + ); + + // Try to fill 500 more, but only 300 remain → should be skipped. + let mut over_fill = signed.clone(); + over_fill.partial_fill = Some(500); + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![over_fill]), + false, + )); + + // Status unchanged. + assert_eq!( + Orders::::get(id), + Some(OrderStatus::PartiallyFilled(700)) + ); + assert_event(Event::OrderSkipped { + order_id: id, + reason: Error::::IncorrectPartialFillAmount.into(), + }); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Partial fills — execute_batched_orders +// ───────────────────────────────────────────────────────────────────────────── + +#[test] +fn execute_batched_orders_partial_fill_sets_partially_filled_status() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(400); + MockSwap::set_tao_balance(alice(), 1_000); + + let signed = make_partial_fill_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + charlie(), + 400, + ); + let id = order_id(&signed.order); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![signed]), + )); + + assert_eq!( + Orders::::get(id), + Some(OrderStatus::PartiallyFilled(400)) + ); + }); +} + +#[test] +fn execute_batched_orders_second_partial_fill_completes_order() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(600); + MockSwap::set_tao_balance(alice(), 1_000); + + let signed_first = make_partial_fill_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + charlie(), + 600, + ); + let id = order_id(&signed_first.order); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![signed_first.clone()]), + )); + assert_eq!( + Orders::::get(id), + Some(OrderStatus::PartiallyFilled(600)) + ); + + let mut signed_second = signed_first.clone(); + signed_second.partial_fill = Some(400); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![signed_second]), + )); + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// In-batch order_id deduplication — regression tests +// ───────────────────────────────────────────────────────────────────────────── + +/// Regression: the same fully-signed `LimitBuy` order appearing twice in one +/// batch must hard-fail with `DuplicateOrderInBatch` rather than debiting the +/// signer twice. Pre-fix, `validate_and_classify` validated each entry against +/// the same pre-batch `Orders::get(order_id)` snapshot with no in-batch tracking, +/// so the signer was charged N× their signed amount. +/// +/// `assert_noop!` also asserts the storage root is unchanged, proving the +/// all-or-nothing batch rolled back. (The mock's TAO/alpha ledgers are +/// thread-local RefCell maps, not substrate storage, so we do not assert on +/// them here — see `mock.rs`.) We additionally assert `Orders::get` was never +/// written. +#[test] +fn execute_batched_orders_full_fill_duplicate_rejected() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(500); + MockSwap::set_tao_balance(alice(), 1_000); + + // Open-relay (relayer: None) fully-signed LimitBuy. + let order = make_signed_order( + AccountKeyring::Alice, + dave(), + netuid(), + OrderType::LimitBuy, + 600, + u64::MAX, + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + None, + ); + let id = order_id(&order.order); + + // The same order twice in one batch. + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![order.clone(), order]), + ), + Error::::DuplicateOrderInBatch + ); + + // The batch rolled back: no order status was recorded. + assert!(Orders::::get(id).is_none()); + }); +} + +/// Regression: two `SignedOrder`s that share the same inner `VersionedOrder` +/// (so the same `order_id`, since `order_id` excludes `partial_fill` and the +/// signature) but carry *different* `partial_fill` values must still collide +/// and be caught by the in-batch dedup. This exercises the partial-fill path +/// (partial_fills_enabled = true, relayer set). +#[test] +fn execute_batched_orders_partial_fill_duplicate_rejected() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_buy_alpha_return(400); + MockSwap::set_tao_balance(alice(), 1_000); + + // Same inner VersionedOrder; only the envelope `partial_fill` differs. + let first = make_partial_fill_order( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, + FAR_FUTURE, + charlie(), + 600, + ); + let mut second = first.clone(); + second.partial_fill = Some(400); + + // Same inner order ⇒ same order_id ⇒ caught by the dedup set. + assert_eq!(order_id(&first.order), order_id(&second.order)); + let id = order_id(&first.order); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![first, second]), + ), + Error::::DuplicateOrderInBatch + ); + + assert!(Orders::::get(id).is_none()); + }); +} + +/// Non-root origin cannot disable the pallet +#[test] +fn non_root_cannot_disable_the_pallet() { + new_test_ext().execute_with(|| { + // Try disabling the pallet with charlie + assert_noop!( + LimitOrders::set_pallet_status(RuntimeOrigin::signed(charlie()), false), + DispatchError::BadOrigin + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// MOCK_SIMULATE_PARTIAL_FILL — sim-swap detects partial fill before funds move +// ───────────────────────────────────────────────────────────────────────────── + +/// `execute_batched_orders` hard-fails the whole batch when the sim-swap for a +/// `LimitBuy` order detects a partial fill (price limit would stop the AMM +/// before consuming the full input). +#[test] +fn execute_batched_orders_buy_partial_fill_fails_batch() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_simulate_partial_fill(true); + + let order = make_signed_order_with_slippage( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, // limit_price always passes for a buy + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_parts(1)), // slippage field set; mock ignores value + ); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![order]), + ), + DispatchError::Other("slippage too high") + ); + }); +} + +/// `execute_orders` silently skips a `LimitBuy` order when the sim-swap detects +/// a partial fill: the order must not appear in storage and an `OrderSkipped` +/// event must be emitted. +#[test] +fn execute_orders_buy_partial_fill_skips_order() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_simulate_partial_fill(true); + + let order = make_signed_order_with_slippage( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::LimitBuy, + 1_000, + u64::MAX, // limit_price always passes for a buy + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_parts(1)), // slippage field set; mock ignores value + ); + let id = order_id(&order.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![order]), + false, + )); + + // Order must not be stored — it was skipped, not fulfilled. + assert!(Orders::::get(id).is_none()); + + // An OrderSkipped event must have been emitted for this order. + assert!( + System::events().iter().any(|r| matches!( + &r.event, + RuntimeEvent::LimitOrders(Event::OrderSkipped { order_id, .. }) + if *order_id == id + )), + "expected OrderSkipped event for this order" + ); + }); +} + +/// `execute_batched_orders` hard-fails the whole batch when the sim-swap for a +/// `TakeProfit` (sell) order detects a partial fill. +#[test] +fn execute_batched_orders_sell_partial_fill_fails_batch() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_simulate_partial_fill(true); + // Seed alpha so the order passes the balance check before reaching the swap. + MockSwap::set_alpha_balance(alice(), bob(), netuid(), 1_000); + + let order = make_signed_order_with_slippage( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::TakeProfit, + 1_000, + 0, // limit_price = 0 → floor always passes for a TakeProfit + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_parts(1)), // slippage field set; mock ignores value + ); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie()), + netuid(), + bounded(vec![order]), + ), + DispatchError::Other("slippage too high") + ); + }); +} + +/// `execute_orders` silently skips a `TakeProfit` order when the sim-swap +/// detects a partial fill: the order must not appear in storage and an +/// `OrderSkipped` event must be emitted. +#[test] +fn execute_orders_sell_partial_fill_skips_order() { + new_test_ext().execute_with(|| { + MockTime::set(1_000_000); + MockSwap::set_price(1.0); + MockSwap::set_simulate_partial_fill(true); + // Seed alpha so the order passes the balance check before reaching the swap. + MockSwap::set_alpha_balance(alice(), bob(), netuid(), 1_000); + + let order = make_signed_order_with_slippage( + AccountKeyring::Alice, + bob(), + netuid(), + OrderType::TakeProfit, + 1_000, + 0, // limit_price = 0 → floor always passes for a TakeProfit + FAR_FUTURE, + Perbill::zero(), + fee_recipient(), + Some(Perbill::from_parts(1)), // slippage field set; mock ignores value + ); + let id = order_id(&order.order); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie()), + bounded(vec![order]), + false, + )); + + // Order must not be stored — it was skipped, not fulfilled. + assert!(Orders::::get(id).is_none()); + + // An OrderSkipped event must have been emitted for this order. + assert!( + System::events().iter().any(|r| matches!( + &r.event, + RuntimeEvent::LimitOrders(Event::OrderSkipped { order_id, .. }) + if *order_id == id + )), + "expected OrderSkipped event for this order" + ); + }); +} diff --git a/pallets/limit-orders/src/tests/migration.rs b/pallets/limit-orders/src/tests/migration.rs new file mode 100644 index 0000000000..d0302158d7 --- /dev/null +++ b/pallets/limit-orders/src/tests/migration.rs @@ -0,0 +1,120 @@ +#![allow(clippy::unwrap_used)] +//! Tests for the `migrate_register_pallet_hotkey` migration. + +use frame_support::{BoundedVec, traits::Hooks}; +use sp_runtime::{BuildStorage, traits::AccountIdConversion}; +use subtensor_swap_interface::OrderSwapInterface as _; + +use crate::{ + HasMigrationRun, LimitOrdersEnabled, MigrationKeyMaxLen, + migrations::migrate_register_pallet_hotkey, + tests::mock::{LimitOrdersPalletId, MockSwap, PalletHotkeyAccount, System, Test}, +}; + +fn migration_key() -> BoundedVec { + BoundedVec::truncate_from(b"migrate_register_pallet_hotkey".to_vec()) +} + +/// Minimal externalities: system genesis only, no pallet hotkey pre-registered, +/// `LimitOrdersEnabled` at its storage default (`false`). +fn migration_ext() -> sp_io::TestExternalities { + let storage = frame_system::GenesisConfig::::default() + .build_storage() + .unwrap(); + let mut ext = sp_io::TestExternalities::new(storage); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +#[test] +fn migration_registers_hotkey_and_marks_run_on_first_call() { + migration_ext().execute_with(|| { + let pallet_acct: crate::tests::mock::AccountId = + LimitOrdersPalletId::get().into_account_truncating(); + let pallet_hotkey = PalletHotkeyAccount::get(); + + assert!(!MockSwap::pallet_hotkey_registered( + &pallet_acct, + &pallet_hotkey + )); + assert!(!HasMigrationRun::::get(migration_key())); + + migrate_register_pallet_hotkey::(); + + assert!( + MockSwap::pallet_hotkey_registered(&pallet_acct, &pallet_hotkey), + "hotkey must be registered after migration" + ); + assert!( + HasMigrationRun::::get(migration_key()), + "migration must be marked as run" + ); + // Migration no longer touches LimitOrdersEnabled — value is unchanged. + assert!(!LimitOrdersEnabled::::get()); + }); +} + +#[test] +fn migration_does_not_touch_limit_orders_enabled() { + migration_ext().execute_with(|| { + // Enable the pallet before running the migration (simulates a chain + // that already had it enabled via genesis or admin action). + LimitOrdersEnabled::::set(true); + + migrate_register_pallet_hotkey::(); + + assert!( + LimitOrdersEnabled::::get(), + "migration must not change LimitOrdersEnabled" + ); + }); +} + +#[test] +fn migration_skips_hotkey_registration_when_already_registered() { + migration_ext().execute_with(|| { + let pallet_acct: crate::tests::mock::AccountId = + LimitOrdersPalletId::get().into_account_truncating(); + let pallet_hotkey = PalletHotkeyAccount::get(); + let _ = MockSwap::register_pallet_hotkey(&pallet_acct, &pallet_hotkey); + + // Must not panic on duplicate registration. + migrate_register_pallet_hotkey::(); + + assert!(HasMigrationRun::::get(migration_key())); + }); +} + +#[test] +fn migration_is_idempotent() { + migration_ext().execute_with(|| { + let pallet_acct: crate::tests::mock::AccountId = + LimitOrdersPalletId::get().into_account_truncating(); + let pallet_hotkey = PalletHotkeyAccount::get(); + + migrate_register_pallet_hotkey::(); + assert!(MockSwap::pallet_hotkey_registered( + &pallet_acct, + &pallet_hotkey + )); + + // Second run must be a no-op — hotkey stays registered, flag stays set. + migrate_register_pallet_hotkey::(); + assert!(MockSwap::pallet_hotkey_registered( + &pallet_acct, + &pallet_hotkey + )); + assert!(HasMigrationRun::::get(migration_key())); + }); +} + +#[test] +fn on_runtime_upgrade_delegates_to_migration() { + migration_ext().execute_with(|| { + assert!(!HasMigrationRun::::get(migration_key())); + + as Hooks>::on_runtime_upgrade(); + + assert!(HasMigrationRun::::get(migration_key())); + }); +} diff --git a/pallets/limit-orders/src/tests/mock.rs b/pallets/limit-orders/src/tests/mock.rs new file mode 100644 index 0000000000..2834c54afe --- /dev/null +++ b/pallets/limit-orders/src/tests/mock.rs @@ -0,0 +1,666 @@ +#![allow(clippy::unwrap_used)] +//! Minimal mock runtime for `pallet-limit-orders` unit tests. +//! +//! `AccountId` is `sp_runtime::AccountId32` so that `MultiSignature` works +//! out of the box; test keys come from `sp_keyring::AccountKeyring`. + +use std::cell::RefCell; +use std::collections::HashMap; + +use codec::Encode; +use frame_support::{ + BoundedVec, PalletId, construct_runtime, derive_impl, parameter_types, + traits::{ConstU32, ConstU64, Everything}, +}; +use frame_system as system; +use sp_core::{H256, Pair}; +use sp_keyring::Sr25519Keyring as AccountKeyring; +use sp_runtime::{ + AccountId32, BuildStorage, MultiSignature, + traits::{AccountIdConversion, BlakeTwo256, IdentityLookup}, +}; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; +use subtensor_swap_interface::OrderSwapInterface; + +use crate::{self as pallet_limit_orders, LimitOrdersEnabled}; + +// ── Runtime ────────────────────────────────────────────────────────────────── + +construct_runtime!( + pub enum Test { + System: system = 0, + LimitOrders: pallet_limit_orders = 1, + } +); + +pub type Block = frame_system::mocking::MockBlock; +pub type AccountId = AccountId32; + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl system::Config for Test { + type BaseCallFilter = Everything; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type PalletInfo = PalletInfo; + type MaxConsumers = ConstU32<16>; + type Nonce = u64; + type Block = Block; +} + +// ── MockSwap ───────────────────────────────────────────────────────────────── +// +// Records every call so tests can assert that the right transfers happened. + +#[derive(Debug, Clone, PartialEq)] +pub enum SwapCall { + BuyAlpha { + coldkey: AccountId, + hotkey: AccountId, + netuid: NetUid, + tao: u64, + limit_price: u64, + }, + SellAlpha { + coldkey: AccountId, + hotkey: AccountId, + netuid: NetUid, + alpha: u64, + limit_price: u64, + }, + TransferTao { + from: AccountId, + to: AccountId, + amount: u64, + }, + TransferStakedAlpha { + from_coldkey: AccountId, + from_hotkey: AccountId, + to_coldkey: AccountId, + to_hotkey: AccountId, + netuid: NetUid, + amount: u64, + }, +} + +thread_local! { + /// Log of every `OrderSwapInterface` call made during a test. + pub static SWAP_LOG: RefCell> = const { RefCell::new(Vec::new()) }; + /// Fixed price returned by `current_alpha_price` (default 1.0). + pub static MOCK_PRICE: RefCell = RefCell::new(U64F64::from_num(1u32)); + /// Fixed alpha returned by `buy_alpha` (default 0 — tests override as needed). + pub static MOCK_BUY_ALPHA_RETURN: RefCell = const { RefCell::new(0u64) }; + /// Fixed TAO returned by `sell_alpha` (default 0 — tests override as needed). + pub static MOCK_SELL_TAO_RETURN: RefCell = const { RefCell::new(0u64) }; + /// In-memory staked alpha ledger: (coldkey, hotkey, netuid) → balance. + /// `transfer_staked_alpha` debits/credits this map so tests can assert + /// on residual balances after distribution. + pub static ALPHA_BALANCES: RefCell> = + RefCell::new(HashMap::new()); + /// In-memory free TAO ledger: account → balance. + /// `transfer_tao` debits/credits this map so tests can assert + /// on residual balances after distribution. + pub static TAO_BALANCES: RefCell> = + RefCell::new(HashMap::new()); + /// When set to `true`, `transfer_tao` returns `Err(CannotLookup)` so + /// tests can exercise the fee-transfer-failure path. + pub static FAIL_FEE_TRANSFER: RefCell = const { RefCell::new(false) }; + /// When `true`, `buy_alpha` and `sell_alpha` return `DispatchError::Other("pool error")`. + pub static MOCK_SWAP_FAIL: RefCell = const { RefCell::new(false) }; + /// When `true`, swap calls enforce their `limit_price` argument against `MOCK_PRICE`: + /// `buy_alpha` fails if `market_price > limit_price` (ceiling exceeded); + /// `sell_alpha` fails if `market_price < limit_price` (floor not met). + pub static MOCK_ENFORCE_PRICE_LIMIT: RefCell = const { RefCell::new(false) }; + /// When `true`, `buy_alpha` and `sell_alpha` return a slippage error to simulate + /// the case where the AMM price limit stops the swap before the full amount is consumed. + pub static MOCK_SIMULATE_PARTIAL_FILL: RefCell = const { RefCell::new(false) }; + /// Rate-limit flags set by `transfer_staked_alpha` when `set_receiver_limit` is true. + /// Key: (hotkey, coldkey, netuid) — mirrors `StakingOperationRateLimiter` in subtensor. + pub static RATE_LIMITS: RefCell> = + RefCell::new(std::collections::HashSet::new()); + /// Registered (coldkey, hotkey) ownership pairs — mirrors `Owner` storage in subtensor. + pub static HOTKEY_REGISTRATIONS: RefCell> = + RefCell::new(std::collections::HashSet::new()); +} + +pub struct MockSwap; + +impl MockSwap { + pub fn set_price(price: f64) { + MOCK_PRICE.with(|p| *p.borrow_mut() = U64F64::from_num(price)); + } + pub fn set_buy_alpha_return(alpha: u64) { + MOCK_BUY_ALPHA_RETURN.with(|v| *v.borrow_mut() = alpha); + } + pub fn set_sell_tao_return(tao: u64) { + MOCK_SELL_TAO_RETURN.with(|v| *v.borrow_mut() = tao); + } + pub fn set_swap_fail(fail: bool) { + MOCK_SWAP_FAIL.with(|v| *v.borrow_mut() = fail); + } + pub fn set_enforce_price_limit(enforce: bool) { + MOCK_ENFORCE_PRICE_LIMIT.with(|v| *v.borrow_mut() = enforce); + } + pub fn set_simulate_partial_fill(val: bool) { + MOCK_SIMULATE_PARTIAL_FILL.with(|v| *v.borrow_mut() = val); + } + pub fn clear_log() { + SWAP_LOG.with(|l| l.borrow_mut().clear()); + ALPHA_BALANCES.with(|b| b.borrow_mut().clear()); + TAO_BALANCES.with(|b| b.borrow_mut().clear()); + RATE_LIMITS.with(|r| r.borrow_mut().clear()); + HOTKEY_REGISTRATIONS.with(|r| r.borrow_mut().clear()); + MOCK_ENFORCE_PRICE_LIMIT.with(|v| *v.borrow_mut() = false); + MOCK_SIMULATE_PARTIAL_FILL.with(|v| *v.borrow_mut() = false); + } + pub fn is_rate_limited(hotkey: &AccountId, coldkey: &AccountId, netuid: NetUid) -> bool { + RATE_LIMITS.with(|r| { + r.borrow() + .contains(&(hotkey.clone(), coldkey.clone(), netuid)) + }) + } + /// Seed a staked alpha balance for a (coldkey, hotkey, netuid) triple. + pub fn set_alpha_balance(coldkey: AccountId, hotkey: AccountId, netuid: NetUid, amount: u64) { + ALPHA_BALANCES.with(|b| { + b.borrow_mut().insert((coldkey, hotkey, netuid), amount); + }); + } + /// Query the current staked alpha balance for a (coldkey, hotkey, netuid) triple. + pub fn alpha_balance(coldkey: &AccountId, hotkey: &AccountId, netuid: NetUid) -> u64 { + ALPHA_BALANCES.with(|b| { + *b.borrow() + .get(&(coldkey.clone(), hotkey.clone(), netuid)) + .unwrap_or(&0) + }) + } + /// Seed a free TAO balance for an account. + pub fn set_tao_balance(account: AccountId, amount: u64) { + TAO_BALANCES.with(|b| { + b.borrow_mut().insert(account, amount); + }); + } + /// Query the current free TAO balance for an account. + pub fn tao_balance(account: &AccountId) -> u64 { + TAO_BALANCES.with(|b| *b.borrow().get(account).unwrap_or(&0)) + } + pub fn log() -> Vec { + SWAP_LOG.with(|l| l.borrow().clone()) + } + /// Returns the `limit_price` argument from every `buy_alpha` call, in order. + pub fn buy_alpha_limit_prices() -> Vec { + Self::log() + .into_iter() + .filter_map(|c| { + if let SwapCall::BuyAlpha { limit_price, .. } = c { + Some(limit_price) + } else { + None + } + }) + .collect() + } + /// Returns the `limit_price` argument from every `sell_alpha` call, in order. + pub fn sell_alpha_limit_prices() -> Vec { + Self::log() + .into_iter() + .filter_map(|c| { + if let SwapCall::SellAlpha { limit_price, .. } = c { + Some(limit_price) + } else { + None + } + }) + .collect() + } + + pub fn tao_transfers() -> Vec<(AccountId, AccountId, u64)> { + Self::log() + .into_iter() + .filter_map(|c| { + if let SwapCall::TransferTao { from, to, amount } = c { + Some((from, to, amount)) + } else { + None + } + }) + .collect() + } + pub fn alpha_transfers() -> Vec<(AccountId, AccountId, AccountId, AccountId, NetUid, u64)> { + Self::log() + .into_iter() + .filter_map(|c| { + if let SwapCall::TransferStakedAlpha { + from_coldkey, + from_hotkey, + to_coldkey, + to_hotkey, + netuid, + amount, + } = c + { + Some(( + from_coldkey, + from_hotkey, + to_coldkey, + to_hotkey, + netuid, + amount, + )) + } else { + None + } + }) + .collect() + } +} + +impl OrderSwapInterface for MockSwap { + fn buy_alpha( + coldkey: &AccountId, + hotkey: &AccountId, + netuid: NetUid, + tao_amount: TaoBalance, + limit_price: TaoBalance, + _apply_limits: bool, + ) -> Result { + if MOCK_SWAP_FAIL.with(|v| *v.borrow()) { + return Err(frame_support::pallet_prelude::DispatchError::Other( + "pool error", + )); + } + if MOCK_SIMULATE_PARTIAL_FILL.with(|v| *v.borrow()) { + return Err(frame_support::pallet_prelude::DispatchError::Other( + "slippage too high", + )); + } + let tao = tao_amount.to_u64(); + // Record the call (including rejected ones) so tests can verify the limit was passed. + SWAP_LOG.with(|l| { + l.borrow_mut().push(SwapCall::BuyAlpha { + coldkey: coldkey.clone(), + hotkey: hotkey.clone(), + netuid, + tao, + limit_price: limit_price.to_u64(), + }) + }); + if MOCK_ENFORCE_PRICE_LIMIT.with(|v| *v.borrow()) { + let price = MOCK_PRICE.with(|p| { + p.borrow() + .saturating_mul(U64F64::from_num(1_000_000_000u64)) + .saturating_to_num::() + }); + if price > limit_price.to_u64() { + return Err(frame_support::pallet_prelude::DispatchError::Other( + "price limit exceeded", + )); + } + } + let alpha_out = MOCK_BUY_ALPHA_RETURN.with(|v| *v.borrow()); + // Debit TAO from coldkey, credit alpha to (coldkey, hotkey, netuid). + TAO_BALANCES.with(|b| { + let mut map = b.borrow_mut(); + let bal = map.entry(coldkey.clone()).or_insert(0); + *bal = bal.saturating_sub(tao); + }); + ALPHA_BALANCES.with(|b| { + let mut map = b.borrow_mut(); + let bal = map + .entry((coldkey.clone(), hotkey.clone(), netuid)) + .or_insert(0); + *bal = bal.saturating_add(alpha_out); + }); + Ok(AlphaBalance::from(alpha_out)) + } + + fn sell_alpha( + coldkey: &AccountId, + hotkey: &AccountId, + netuid: NetUid, + alpha_amount: AlphaBalance, + limit_price: TaoBalance, + _apply_limits: bool, + ) -> Result { + if MOCK_SWAP_FAIL.with(|v| *v.borrow()) { + return Err(frame_support::pallet_prelude::DispatchError::Other( + "pool error", + )); + } + if MOCK_SIMULATE_PARTIAL_FILL.with(|v| *v.borrow()) { + return Err(frame_support::pallet_prelude::DispatchError::Other( + "slippage too high", + )); + } + let alpha = alpha_amount.to_u64(); + // Record the call (including rejected ones) so tests can verify the limit was passed. + SWAP_LOG.with(|l| { + l.borrow_mut().push(SwapCall::SellAlpha { + coldkey: coldkey.clone(), + hotkey: hotkey.clone(), + netuid, + alpha, + limit_price: limit_price.to_u64(), + }) + }); + // Only enforce if a non-zero floor was requested (0 means no constraint). + if MOCK_ENFORCE_PRICE_LIMIT.with(|v| *v.borrow()) && limit_price.to_u64() > 0 { + let price = MOCK_PRICE.with(|p| { + p.borrow() + .saturating_mul(U64F64::from_num(1_000_000_000u64)) + .saturating_to_num::() + }); + if price < limit_price.to_u64() { + return Err(frame_support::pallet_prelude::DispatchError::Other( + "price limit exceeded", + )); + } + } + let tao_out = MOCK_SELL_TAO_RETURN.with(|v| *v.borrow()); + // Debit alpha from (coldkey, hotkey, netuid), credit TAO to coldkey. + ALPHA_BALANCES.with(|b| { + let mut map = b.borrow_mut(); + let bal = map + .entry((coldkey.clone(), hotkey.clone(), netuid)) + .or_insert(0); + *bal = bal.saturating_sub(alpha); + }); + TAO_BALANCES.with(|b| { + let mut map = b.borrow_mut(); + let bal = map.entry(coldkey.clone()).or_insert(0); + *bal = bal.saturating_add(tao_out); + }); + Ok(TaoBalance::from(tao_out)) + } + + fn current_alpha_price(_netuid: NetUid) -> U64F64 { + MOCK_PRICE.with(|p| *p.borrow()) + } + + fn transfer_tao( + from: &AccountId, + to: &AccountId, + amount: TaoBalance, + ) -> frame_support::pallet_prelude::DispatchResult { + if FAIL_FEE_TRANSFER.with(|f| *f.borrow()) { + return Err(frame_support::pallet_prelude::DispatchError::CannotLookup); + } + let amt = amount.to_u64(); + TAO_BALANCES.with(|b| { + let mut map = b.borrow_mut(); + let from_bal = map.entry(from.clone()).or_insert(0); + *from_bal = from_bal.saturating_sub(amt); + let to_bal = map.entry(to.clone()).or_insert(0); + *to_bal = to_bal.saturating_add(amt); + }); + SWAP_LOG.with(|l| { + l.borrow_mut().push(SwapCall::TransferTao { + from: from.clone(), + to: to.clone(), + amount: amt, + }) + }); + Ok(()) + } + + #[cfg(feature = "runtime-benchmarks")] + fn set_up_netuid_for_benchmark(_netuid: NetUid) { + // Mock price is already set; no subnet state to initialise. + } + + #[cfg(feature = "runtime-benchmarks")] + fn set_up_acc_for_benchmark(hotkey: &AccountId, coldkey: &AccountId) { + // Provide non-zero swap returns so batched-order benchmarks don't hit + // `SwapReturnedZero`. Also seed TAO and alpha balances so transfers + // succeed in the mock ledgers. + MockSwap::set_buy_alpha_return(1_000_000); + MockSwap::set_sell_tao_return(1_000_000); + MockSwap::set_tao_balance(coldkey.clone(), u64::MAX / 2); + MockSwap::set_alpha_balance( + coldkey.clone(), + hotkey.clone(), + NetUid::from(1u16), + u64::MAX / 2, + ); + } + + fn register_pallet_hotkey( + coldkey: &AccountId, + hotkey: &AccountId, + ) -> frame_support::pallet_prelude::DispatchResult { + HOTKEY_REGISTRATIONS.with(|r| { + r.borrow_mut().insert((coldkey.clone(), hotkey.clone())); + }); + Ok(()) + } + + fn pallet_hotkey_registered(coldkey: &AccountId, hotkey: &AccountId) -> bool { + HOTKEY_REGISTRATIONS.with(|r| r.borrow().contains(&(coldkey.clone(), hotkey.clone()))) + } + + fn transfer_staked_alpha( + from_coldkey: &AccountId, + from_hotkey: &AccountId, + to_coldkey: &AccountId, + to_hotkey: &AccountId, + netuid: NetUid, + amount: AlphaBalance, + validate_sender: bool, + set_receiver_limit: bool, + ) -> frame_support::pallet_prelude::DispatchResult { + if validate_sender { + let rate_limited = RATE_LIMITS.with(|r| { + r.borrow() + .contains(&(from_hotkey.clone(), from_coldkey.clone(), netuid)) + }); + if rate_limited { + return Err(frame_support::pallet_prelude::DispatchError::Other( + "StakingOperationRateLimitExceeded", + )); + } + } + let amt = amount.to_u64(); + ALPHA_BALANCES.with(|b| { + let mut map = b.borrow_mut(); + let from_bal = map + .entry((from_coldkey.clone(), from_hotkey.clone(), netuid)) + .or_insert(0); + *from_bal = from_bal.saturating_sub(amt); + let to_bal = map + .entry((to_coldkey.clone(), to_hotkey.clone(), netuid)) + .or_insert(0); + *to_bal = to_bal.saturating_add(amt); + }); + if set_receiver_limit { + RATE_LIMITS.with(|r| { + r.borrow_mut() + .insert((to_hotkey.clone(), to_coldkey.clone(), netuid)); + }); + } + SWAP_LOG.with(|l| { + l.borrow_mut().push(SwapCall::TransferStakedAlpha { + from_coldkey: from_coldkey.clone(), + from_hotkey: from_hotkey.clone(), + to_coldkey: to_coldkey.clone(), + to_hotkey: to_hotkey.clone(), + netuid, + amount: amt, + }) + }); + Ok(()) + } +} + +// ── MockTime ───────────────────────────────────────────────────────────────── + +thread_local! { + pub static MOCK_TIME_MS: RefCell = const { RefCell::new(1_000_000u64) }; +} + +pub struct MockTime; + +impl MockTime { + pub fn set(ms: u64) { + MOCK_TIME_MS.with(|t| *t.borrow_mut() = ms); + } +} + +impl frame_support::traits::UnixTime for MockTime { + fn now() -> core::time::Duration { + let ms = MOCK_TIME_MS.with(|t| *t.borrow()); + core::time::Duration::from_millis(ms) + } +} + +// ── Pallet config ───────────────────────────────────────────────────────────── + +parameter_types! { + pub const LimitOrdersPalletId: PalletId = PalletId(*b"lmt/ordr"); + pub const PalletHotkeyAccount: AccountId = AccountId::new([0xaa; 32]); +} + +/// A fixed account used in tests as the fee recipient when a concrete +/// recipient is needed but the test isn't specifically about fees. +pub fn fee_recipient() -> AccountId { + AccountId::new([0xfe; 32]) +} + +impl pallet_limit_orders::Config for Test { + type SwapInterface = MockSwap; + type TimeProvider = MockTime; + type MaxOrdersPerBatch = ConstU32<64>; + type PalletId = LimitOrdersPalletId; + type PalletHotkey = PalletHotkeyAccount; + type WeightInfo = (); + type ChainId = ConstU64<945>; +} + +// ── Shared test helpers ─────────────────────────────────────────────────────── + +pub fn alice() -> AccountId { + AccountKeyring::Alice.to_account_id() +} +pub fn bob() -> AccountId { + AccountKeyring::Bob.to_account_id() +} +pub fn charlie() -> AccountId { + AccountKeyring::Charlie.to_account_id() +} +pub fn dave() -> AccountId { + AccountKeyring::Dave.to_account_id() +} +pub fn netuid() -> NetUid { + NetUid::from(1u16) +} + +pub const FAR_FUTURE: u64 = u64::MAX; + +#[allow(clippy::too_many_arguments)] +pub fn make_signed_order( + keyring: AccountKeyring, + hotkey: AccountId, + netuid: NetUid, + order_type: crate::OrderType, + amount: u64, + limit_price: u64, + expiry: u64, + fee_rate: sp_runtime::Perbill, + fee_recipient: AccountId, + relayer: Option>>, +) -> crate::SignedOrder { + let signer = keyring.to_account_id(); + let order = crate::VersionedOrder::V1(crate::Order { + signer, + hotkey, + netuid, + order_type, + amount, + limit_price, + expiry, + fee_rate, + fee_recipient, + relayer, + max_slippage: None, + chain_id: 945, + partial_fills_enabled: false, + }); + let sig = keyring.pair().sign(&order.encode()); + crate::SignedOrder { + order, + signature: MultiSignature::Sr25519(sig), + partial_fill: None, + } +} + +/// Build a signed order with partial fills enabled and a relayer set. +/// `partial_fill` is the fill amount to inject into the `SignedOrder` envelope. +#[allow(clippy::too_many_arguments)] +pub fn make_partial_fill_order( + keyring: AccountKeyring, + hotkey: AccountId, + netuid: NetUid, + order_type: crate::OrderType, + amount: u64, + limit_price: u64, + expiry: u64, + relayer: AccountId, + partial_fill: u64, +) -> crate::SignedOrder { + let signer = keyring.to_account_id(); + let order = crate::VersionedOrder::V1(crate::Order { + signer, + hotkey, + netuid, + order_type, + amount, + limit_price, + expiry, + fee_rate: sp_runtime::Perbill::zero(), + fee_recipient: fee_recipient(), + relayer: Some(BoundedVec::try_from(vec![relayer]).unwrap()), + max_slippage: None, + chain_id: 945, + partial_fills_enabled: true, + }); + let sig = keyring.pair().sign(&order.encode()); + crate::SignedOrder { + order, + signature: MultiSignature::Sr25519(sig), + partial_fill: Some(partial_fill), + } +} + +pub fn bounded( + v: Vec>, +) -> BoundedVec, ConstU32<64>> { + BoundedVec::try_from(v).unwrap() +} + +pub fn order_id(order: &crate::VersionedOrder) -> H256 { + crate::pallet::Pallet::::derive_order_id(order) +} + +// ── Test externalities ──────────────────────────────────────────────────────── + +pub fn new_test_ext() -> sp_io::TestExternalities { + let storage = system::GenesisConfig::::default() + .build_storage() + .unwrap(); + let mut ext = sp_io::TestExternalities::new(storage); + // Register a keystore so `sp_io::crypto` functions work in benchmark tests. + let keystore = sp_keystore::testing::MemoryKeystore::new(); + ext.register_extension(sp_keystore::KeystoreExt::new(keystore)); + ext.execute_with(|| { + System::set_block_number(1); + MockSwap::clear_log(); + // Simulate genesis_build: register the pallet hotkey and enable the pallet. + let pallet_acct: AccountId = LimitOrdersPalletId::get().into_account_truncating(); + let _ = MockSwap::register_pallet_hotkey(&pallet_acct, &PalletHotkeyAccount::get()); + LimitOrdersEnabled::::set(true); + }); + ext +} diff --git a/pallets/limit-orders/src/tests/mod.rs b/pallets/limit-orders/src/tests/mod.rs new file mode 100644 index 0000000000..95e0875b26 --- /dev/null +++ b/pallets/limit-orders/src/tests/mod.rs @@ -0,0 +1,4 @@ +pub mod auxiliary; +pub mod extrinsics; +pub mod migration; +pub mod mock; diff --git a/pallets/limit-orders/src/weights.rs b/pallets/limit-orders/src/weights.rs new file mode 100644 index 0000000000..e8c24d30ba --- /dev/null +++ b/pallets/limit-orders/src/weights.rs @@ -0,0 +1,362 @@ + +//! Autogenerated weights for `pallet_limit_orders` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 49.1.0 +//! DATE: 2026-06-11, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `runnervm3jyl0`, CPU: `AMD EPYC 9V74 80-Core Processor` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` + +// Executed Command: +// /home/runner/work/subtensor/subtensor/target/production/node-subtensor +// benchmark +// pallet +// --runtime=/home/runner/work/subtensor/subtensor/target/production/wbuild/node-subtensor-runtime/node_subtensor_runtime.compact.compressed.wasm +// --genesis-builder=runtime +// --genesis-builder-preset=benchmark +// --wasm-execution=compiled +// --pallet=pallet_limit_orders +// --extrinsic=* +// --steps=50 +// --repeat=20 +// --no-storage-info +// --no-min-squares +// --no-median-slopes +// --output=/tmp/tmp.h1ZElBJrCs +// --template=/home/runner/work/subtensor/subtensor/.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] +#![allow(dead_code)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_limit_orders`. +pub trait WeightInfo { + fn cancel_order() -> Weight; + fn set_pallet_status() -> Weight; + fn execute_orders(n: u32, ) -> Weight; + fn execute_batched_orders(n: u32, ) -> Weight; +} + +/// Weights for `pallet_limit_orders` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `LimitOrders::Orders` (r:1 w:1) + /// Proof: `LimitOrders::Orders` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) + fn cancel_order() -> Weight { + // Proof Size summary in bytes: + // Measured: `66` + // Estimated: `3522` + // Minimum execution time: 13_230_000 picoseconds. + Weight::from_parts(14_822_000, 3522) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `LimitOrders::LimitOrdersEnabled` (r:0 w:1) + /// Proof: `LimitOrders::LimitOrdersEnabled` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + fn set_pallet_status() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 4_006_000 picoseconds. + Weight::from_parts(4_266_000, 0) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `LimitOrders::LimitOrdersEnabled` (r:1 w:0) + /// Proof: `LimitOrders::LimitOrdersEnabled` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:1) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) + /// Storage: `EVMChainId::ChainId` (r:1 w:0) + /// Proof: `EVMChainId::ChainId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `LimitOrders::Orders` (r:100 w:100) + /// Proof: `LimitOrders::Orders` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) + /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubtokenEnabled` (r:1 w:0) + /// Proof: `SubtensorModule::SubtokenEnabled` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Owner` (r:100 w:0) + /// Proof: `SubtensorModule::Owner` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:202 w:202) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:1) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `Swap::FeeRate` (r:1 w:0) + /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) + /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetVolume` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetVolume` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyAlpha` (r:100 w:100) + /// Proof: `SubtensorModule::TotalHotkeyAlpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyShares` (r:100 w:0) + /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:100 w:100) + /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::StakingHotkeys` (r:100 w:0) + /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Alpha` (r:100 w:0) + /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::AlphaV2` (r:100 w:100) + /// Proof: `SubtensorModule::AlphaV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTaoFlow` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTaoFlow` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Lock` (r:100 w:0) + /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (r:0 w:100) + /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `n` is `[1, 100]`. + fn execute_orders(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1134 + n * (283 ±0)` + // Estimated: `6148 + n * (5158 ±0)` + // Minimum execution time: 597_854_000 picoseconds. + Weight::from_parts(61_768_457, 6148) + // Standard Error: 136_266 + .saturating_add(Weight::from_parts(520_180_782, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(17_u64)) + .saturating_add(T::DbWeight::get().reads((11_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes(10_u64)) + .saturating_add(T::DbWeight::get().writes((7_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 5158).saturating_mul(n.into())) + } + /// Storage: `LimitOrders::LimitOrdersEnabled` (r:1 w:0) + /// Proof: `LimitOrders::LimitOrdersEnabled` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:1) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) + /// Storage: `EVMChainId::ChainId` (r:1 w:0) + /// Proof: `EVMChainId::ChainId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `LimitOrders::Orders` (r:100 w:100) + /// Proof: `LimitOrders::Orders` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:203 w:203) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) + /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubtokenEnabled` (r:1 w:0) + /// Proof: `SubtensorModule::SubtokenEnabled` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:1) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `Swap::FeeRate` (r:1 w:0) + /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) + /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetVolume` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetVolume` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyAlpha` (r:101 w:101) + /// Proof: `SubtensorModule::TotalHotkeyAlpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyShares` (r:101 w:0) + /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:101 w:101) + /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::StakingHotkeys` (r:101 w:0) + /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Alpha` (r:101 w:0) + /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::AlphaV2` (r:101 w:101) + /// Proof: `SubtensorModule::AlphaV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTaoFlow` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTaoFlow` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Lock` (r:1 w:0) + /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Owner` (r:100 w:0) + /// Proof: `SubtensorModule::Owner` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (r:0 w:101) + /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `n` is `[1, 100]`. + fn execute_batched_orders(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1263 + n * (283 ±0)` + // Estimated: `8727 + n * (5158 ±0)` + // Minimum execution time: 747_334_000 picoseconds. + Weight::from_parts(499_718_496, 8727) + // Standard Error: 74_442 + .saturating_add(Weight::from_parts(259_134_004, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(25_u64)) + .saturating_add(T::DbWeight::get().reads((10_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes(15_u64)) + .saturating_add(T::DbWeight::get().writes((7_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 5158).saturating_mul(n.into())) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `LimitOrders::Orders` (r:1 w:1) + /// Proof: `LimitOrders::Orders` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) + fn cancel_order() -> Weight { + // Proof Size summary in bytes: + // Measured: `66` + // Estimated: `3522` + // Minimum execution time: 13_230_000 picoseconds. + Weight::from_parts(14_822_000, 3522) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `LimitOrders::LimitOrdersEnabled` (r:0 w:1) + /// Proof: `LimitOrders::LimitOrdersEnabled` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + fn set_pallet_status() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 4_006_000 picoseconds. + Weight::from_parts(4_266_000, 0) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `LimitOrders::LimitOrdersEnabled` (r:1 w:0) + /// Proof: `LimitOrders::LimitOrdersEnabled` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:1) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) + /// Storage: `EVMChainId::ChainId` (r:1 w:0) + /// Proof: `EVMChainId::ChainId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `LimitOrders::Orders` (r:100 w:100) + /// Proof: `LimitOrders::Orders` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) + /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubtokenEnabled` (r:1 w:0) + /// Proof: `SubtensorModule::SubtokenEnabled` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Owner` (r:100 w:0) + /// Proof: `SubtensorModule::Owner` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `System::Account` (r:202 w:202) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:1) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `Swap::FeeRate` (r:1 w:0) + /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) + /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetVolume` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetVolume` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyAlpha` (r:100 w:100) + /// Proof: `SubtensorModule::TotalHotkeyAlpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyShares` (r:100 w:0) + /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:100 w:100) + /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::StakingHotkeys` (r:100 w:0) + /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Alpha` (r:100 w:0) + /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::AlphaV2` (r:100 w:100) + /// Proof: `SubtensorModule::AlphaV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTaoFlow` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTaoFlow` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Lock` (r:100 w:0) + /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (r:0 w:100) + /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `n` is `[1, 100]`. + fn execute_orders(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1134 + n * (283 ±0)` + // Estimated: `6148 + n * (5158 ±0)` + // Minimum execution time: 597_854_000 picoseconds. + Weight::from_parts(61_768_457, 6148) + // Standard Error: 136_266 + .saturating_add(Weight::from_parts(520_180_782, 0).saturating_mul(n.into())) + .saturating_add(RocksDbWeight::get().reads(17_u64)) + .saturating_add(RocksDbWeight::get().reads((11_u64).saturating_mul(n.into()))) + .saturating_add(RocksDbWeight::get().writes(10_u64)) + .saturating_add(RocksDbWeight::get().writes((7_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 5158).saturating_mul(n.into())) + } + /// Storage: `LimitOrders::LimitOrdersEnabled` (r:1 w:0) + /// Proof: `LimitOrders::LimitOrdersEnabled` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:1) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) + /// Storage: `EVMChainId::ChainId` (r:1 w:0) + /// Proof: `EVMChainId::ChainId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `LimitOrders::Orders` (r:100 w:100) + /// Proof: `LimitOrders::Orders` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:203 w:203) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) + /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubtokenEnabled` (r:1 w:0) + /// Proof: `SubtensorModule::SubtokenEnabled` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:1) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `Swap::FeeRate` (r:1 w:0) + /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) + /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetVolume` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetVolume` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyAlpha` (r:101 w:101) + /// Proof: `SubtensorModule::TotalHotkeyAlpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeyShares` (r:101 w:0) + /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:101 w:101) + /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::StakingHotkeys` (r:101 w:0) + /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Alpha` (r:101 w:0) + /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::AlphaV2` (r:101 w:101) + /// Proof: `SubtensorModule::AlphaV2` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTaoFlow` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTaoFlow` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Lock` (r:1 w:0) + /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Owner` (r:100 w:0) + /// Proof: `SubtensorModule::Owner` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (r:0 w:101) + /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `n` is `[1, 100]`. + fn execute_batched_orders(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1263 + n * (283 ±0)` + // Estimated: `8727 + n * (5158 ±0)` + // Minimum execution time: 747_334_000 picoseconds. + Weight::from_parts(499_718_496, 8727) + // Standard Error: 74_442 + .saturating_add(Weight::from_parts(259_134_004, 0).saturating_mul(n.into())) + .saturating_add(RocksDbWeight::get().reads(25_u64)) + .saturating_add(RocksDbWeight::get().reads((10_u64).saturating_mul(n.into()))) + .saturating_add(RocksDbWeight::get().writes(15_u64)) + .saturating_add(RocksDbWeight::get().writes((7_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 5158).saturating_mul(n.into())) + } +} diff --git a/pallets/multi-collective/Cargo.toml b/pallets/multi-collective/Cargo.toml new file mode 100644 index 0000000000..171faf9caa --- /dev/null +++ b/pallets/multi-collective/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "pallet-multi-collective" +version = "1.0.0" +authors = ["Bittensor Nucleus Team"] +edition.workspace = true +license = "Apache-2.0" +homepage = "https://bittensor.com" +description = "Membership for named collectives, with per-call origins and optional scheduled rotation." +readme = "README.md" + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { workspace = true, features = ["max-encoded-len"] } +scale-info = { workspace = true, features = ["derive"] } +frame-benchmarking = { workspace = true, optional = true } +frame-system = { workspace = true } +frame-support = { workspace = true } +impl-trait-for-tuples = { workspace = true } +num-traits = { workspace = true } +subtensor-runtime-common = { workspace = true } + +[dev-dependencies] +sp-io = { workspace = true, default-features = true } +sp-core = { workspace = true, default-features = true } +sp-runtime = { workspace = true, default-features = true } + +[features] +default = ["std"] +std = [ + "codec/std", + "scale-info/std", + "frame-benchmarking?/std", + "frame-system/std", + "frame-support/std", + "num-traits/std", + "subtensor-runtime-common/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "subtensor-runtime-common/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime" +] diff --git a/pallets/multi-collective/README.md b/pallets/multi-collective/README.md new file mode 100644 index 0000000000..61d2739ea6 --- /dev/null +++ b/pallets/multi-collective/README.md @@ -0,0 +1,99 @@ +# pallet-multi-collective + +Membership storage for one or more named collectives, keyed by a +runtime-defined `CollectiveId`. Each collective is configured by a +`CollectivesInfo` impl: name, min/max members, optional term duration. + +The pallet only stores membership. Voting, proposing, and tallying are +left to the consumer (e.g. `pallet-referenda` + `pallet-signed-voting`), +which read members through the `CollectiveInspect` trait. + +## Concepts + +| Type | Provided by | Purpose | +| ---- | ----------- | ------- | +| `CollectiveId` | runtime | Enum naming each collective. | +| `CollectivesInfo` | runtime | Returns the static config for each id (name, bounds, term). | +| `CollectiveInfo` | this crate | `{ name, min_members, max_members, term_duration }`. | +| `Members<_>` | this crate | `BoundedVec` per id, sorted by `AccountId`. | + +## Extrinsics + +| Call | Origin | Effect | +| ---- | ------ | ------ | +| `add_member` | `T::AddOrigin` | Insert one member. Fails on `AlreadyMember`, `TooManyMembers`, `CollectiveNotFound`. | +| `remove_member` | `T::RemoveOrigin` | Remove one member. Fails on `NotMember`, `TooFewMembers`, `CollectiveNotFound`. | +| `swap_member` | `T::SwapOrigin` | Atomic remove + insert. Count is preserved, so the per-collective `min_members` / `max_members` bounds are not re-checked; works at either boundary. | +| `set_members` | `T::SetOrigin` | Replace the full list. Sorts the input and rejects `DuplicateAccounts` if any duplicates are present (the input is not silently deduplicated). | +| `force_rotate` | `T::RotateOrigin` | Trigger `OnNewTerm` for a rotating collective on demand. | + +Every mutation fires `T::OnMembersChanged` with the incoming and +outgoing accounts so downstream pallets can react (e.g. clean up +votes). The Subtensor runtime currently wires this to `()`: active +polls snapshot the voter set at creation, so member changes cannot +retroactively invalidate votes, and no cleanup is needed. + +## Rotation + +A collective whose `CollectiveInfo::term_duration` is `Some(d)` rotates +every `d` blocks: `on_initialize` calls `T::OnNewTerm::on_new_term(id)` +when `block_number % d == 0`. The runtime-supplied handler typically +recomputes membership from on-chain data and writes it back through +`set_members`. + +`force_rotate` runs the same hook on demand. Used to bootstrap the +first term (the natural cadence only fires after the first boundary, +which can be days or months in) and as a privileged override during +incidents. Calls against a collective with `term_duration: None` are +rejected with `CollectiveDoesNotRotate`. + +Curated collectives (no term duration) are managed directly via the +membership extrinsics. + +## Integrity check + +`integrity_test` runs at runtime construction and panics on a +misconfigured `CollectivesInfo`: + +- `min_members > T::MaxMembers` (collective can't reach its min) +- `max_members > T::MaxMembers` (storage can't hold the declared max) +- `min_members > max_members` (collective is unreachable) +- `term_duration: Some(0)` (silently disables rotation; use `None` to opt out) + +## Migrations + +Pinned at `StorageVersion::new(0)` to satisfy try-runtime CLI; the +project tracks migration runs through a per-pallet `HasMigrationRun` +storage map (see `pallet-crowdloan`), not via FRAME's `StorageVersion` +bump. Add a `migrations` module and an `on_runtime_upgrade` hook on +the next breaking change to `Members<_>` or any future persisted state. + +## Configuration + +```rust +parameter_types! { + pub const MaxMembers: u32 = 20; +} + +impl pallet_multi_collective::Config for Runtime { + type CollectiveId = CollectiveId; + type Collectives = Collectives; + type AddOrigin = AsEnsureOriginWithArg>; + type RemoveOrigin = AsEnsureOriginWithArg>; + type SwapOrigin = AsEnsureOriginWithArg>; + type SetOrigin = AsEnsureOriginWithArg>; + type RotateOrigin = AsEnsureOriginWithArg>; + type OnMembersChanged = (); + type OnNewTerm = TermManagement; + type MaxMembers = MaxMembers; + type WeightInfo = pallet_multi_collective::weights::SubstrateWeight; +} +``` + +`T::MaxMembers` bounds storage; per-collective `max_members` from +`CollectivesInfo` may be smaller but never larger (enforced by +`integrity_test`). + +## License + +Apache-2.0. diff --git a/pallets/multi-collective/src/benchmarking.rs b/pallets/multi-collective/src/benchmarking.rs new file mode 100644 index 0000000000..7755bea183 --- /dev/null +++ b/pallets/multi-collective/src/benchmarking.rs @@ -0,0 +1,150 @@ +//! Benchmarks for `pallet-multi-collective`. +//! +//! Setup is parameterised through [`Config::BenchmarkHelper`]: the runtime +//! supplies a non-rotatable collective whose bounds allow the pallet to +//! fill and drain it freely, plus a separate rotatable collective for +//! `force_rotate`. +#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)] + +use super::*; +use frame_benchmarking::v2::*; +use frame_system::RawOrigin; + +const SEED: u32 = 0; + +fn fill_members(collective_id: T::CollectiveId, count: u32) -> Vec { + let mut members: Vec = (0..count) + .map(|i| account::("member", i, SEED)) + .collect(); + members.sort(); + + // Bypass `add_member` to avoid paying the per-call binary_search cost + // during setup: we know the list is sorted and unique, so we can + // write the storage directly. + let bounded = + BoundedVec::try_from(members.clone()).expect("benchmark fill must respect MaxMembers"); + Members::::insert(collective_id, bounded); + members +} + +#[benchmarks] +mod benches { + use super::*; + + /// Worst case: pre-fill to `MaxMembers - 1` so the binary_search runs at full depth. + #[benchmark] + fn add_member() { + let collective = T::BenchmarkHelper::collective(); + let max = T::MaxMembers::get(); + let _existing = fill_members::(collective, max.saturating_sub(1)); + let new_member = account::("new", 0, SEED); + + #[extrinsic_call] + add_member(RawOrigin::Root, collective, new_member); + + assert_eq!(Members::::get(collective).len(), max as usize); + } + + /// Worst case: full collective; binary_search at max depth, remove + /// shifts the maximum number of trailing elements. + #[benchmark] + fn remove_member() { + let collective = T::BenchmarkHelper::collective(); + let max = T::MaxMembers::get(); + let members = fill_members::(collective, max); + // Remove the head: `remove(0)` shifts every other element. + let to_remove = members[0].clone(); + + #[extrinsic_call] + remove_member(RawOrigin::Root, collective, to_remove); + + assert_eq!( + Members::::get(collective).len(), + (max as usize).saturating_sub(1), + ); + } + + /// Worst case: full collective; two binary_searches at max depth, + /// then a remove + insert each shifting the maximum trailing slice. + #[benchmark] + fn swap_member() { + let collective = T::BenchmarkHelper::collective(); + let max = T::MaxMembers::get(); + let members = fill_members::(collective, max); + let to_remove = members[0].clone(); + let to_add = account::("new", 0, SEED); + + #[extrinsic_call] + swap_member(RawOrigin::Root, collective, to_remove, to_add); + + assert_eq!(Members::::get(collective).len(), max as usize); + } + + /// Worst case: replace a fully-populated collective with a completely disjoint set + /// of `MaxMembers` new accounts. + #[benchmark] + fn set_members() { + let collective = T::BenchmarkHelper::collective(); + let max = T::MaxMembers::get(); + let _existing = fill_members::(collective, max); + + let new_members: Vec = (0..max) + .map(|i| account::("new", i, SEED)) + .collect(); + + #[extrinsic_call] + set_members(RawOrigin::Root, collective, new_members.clone()); + + assert_eq!(Members::::get(collective).len(), max as usize); + } + + /// `force_rotate` itself does only validation + a hook dispatch; + /// this benchmark measures just the extrinsic-side overhead. The + /// hook's worst-case cost is added separately via + /// `T::OnNewTerm::weight()` in the `#[pallet::weight(...)]` + /// annotation. + #[benchmark] + fn force_rotate() { + let collective = T::BenchmarkHelper::rotatable_collective(); + + #[extrinsic_call] + force_rotate(RawOrigin::Root, collective); + } + + #[benchmark] + fn do_add_member() { + let collective = T::BenchmarkHelper::collective(); + let max = T::MaxMembers::get(); + let _existing = fill_members::(collective, max.saturating_sub(1)); + let new_member = account::("new", 0, SEED); + + #[block] + { + Pallet::::do_add_member(collective, new_member) + .expect("benchmark setup must allow add"); + } + + assert_eq!(Members::::get(collective).len(), max as usize); + } + + #[benchmark] + fn do_remove_member() { + let collective = T::BenchmarkHelper::collective(); + let max = T::MaxMembers::get(); + let members = fill_members::(collective, max); + let to_remove = members[0].clone(); + + #[block] + { + Pallet::::do_remove_member(collective, to_remove) + .expect("benchmark setup must allow remove"); + } + + assert_eq!( + Members::::get(collective).len(), + (max as usize).saturating_sub(1), + ); + } + + impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/pallets/multi-collective/src/lib.rs b/pallets/multi-collective/src/lib.rs new file mode 100644 index 0000000000..93b1a4dd5a --- /dev/null +++ b/pallets/multi-collective/src/lib.rs @@ -0,0 +1,679 @@ +//! # Multi-Collective Pallet +//! +//! Stores the membership of one or more named collectives keyed by a +//! runtime-defined `CollectiveId`. Each collective is configured by a +//! `CollectivesInfo` impl: name, min/max members, optional term duration. +//! +//! ## Membership +//! +//! Members are kept sorted by `AccountId` in a per-collective `BoundedVec`. +//! Four extrinsics mutate the set, each gated by its own origin: +//! - [`Pallet::add_member`] (`T::AddOrigin`) +//! - [`Pallet::remove_member`] (`T::RemoveOrigin`) +//! - [`Pallet::swap_member`] (`T::SwapOrigin`) +//! - [`Pallet::set_members`] (`T::SetOrigin`) +//! +//! Every mutation fires `T::OnMembersChanged` with the incoming and +//! outgoing accounts. +//! +//! ## Rotations +//! +//! Collectives with `CollectiveInfo::term_duration = Some(d)` rotate on +//! schedule: `on_initialize` calls `T::OnNewTerm::on_new_term(id)` whenever +//! `block_number % d == 0`. The runtime-provided handler recomputes the +//! membership and pushes it back through `set_members`. +//! +//! [`Pallet::force_rotate`] (gated by `T::RotateOrigin`) triggers the same +//! hook on demand, for bootstrapping the first term or as a privileged +//! override. +//! +//! ## Inspection +//! +//! Other pallets read membership through [`CollectiveInspect`], implemented +//! by `Pallet` over `Members<_>`. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use alloc::vec::Vec; +use frame_support::{ + dispatch::DispatchResult, + pallet_prelude::*, + traits::{ChangeMembers, EnsureOriginWithArg}, +}; +use frame_system::pallet_prelude::*; +use num_traits::ops::checked::CheckedRem; +pub use pallet::*; +pub use subtensor_runtime_common::OnMembersChanged; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; +pub mod weights; +pub use weights::WeightInfo; + +/// Recommended fixed length for the `Name` parameter of `CollectivesInfo`. +/// The pallet itself does not enforce this, but the runtime's +/// `CollectivesInfo` impl is expected to use `[u8; MAX_COLLECTIVE_NAME_LEN]` +/// so that names round-trip a stable, encodable type. +pub const MAX_COLLECTIVE_NAME_LEN: usize = 32; +type CollectiveName = [u8; MAX_COLLECTIVE_NAME_LEN]; + +#[frame_support::pallet] +#[allow(clippy::expect_used)] +pub mod pallet { + use super::*; + + // Pinned to 0 to satisfy try-runtime CLI's pre/post-upgrade checks. + // The project tracks migrations via a per-pallet `HasMigrationRun` map + // so this value is not bumped on schema changes. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The identifier for a collective. + type CollectiveId: Parameter + MaxEncodedLen + Copy; + + /// Provides per-collective information. + type Collectives: CollectivesInfo, CollectiveName, Id = Self::CollectiveId>; + + /// Required origin for adding a member to a collective. + type AddOrigin: EnsureOriginWithArg; + + /// Required origin for removing a member from a collective. + type RemoveOrigin: EnsureOriginWithArg; + + /// Required origin for swapping a member in a collective. + type SwapOrigin: EnsureOriginWithArg; + + /// Required origin for setting the full member list of a collective. + type SetOrigin: EnsureOriginWithArg; + + /// Required origin for `force_rotate`. + type RotateOrigin: EnsureOriginWithArg; + + /// The receiver of the signal for when the members of a collective have changed. + type OnMembersChanged: OnMembersChanged; + + /// The receiver of the signal for when a new term of a collective has started. + type OnNewTerm: OnNewTerm; + + /// The maximum number of members per collective. + /// + /// This is used for benchmarking. Re-run the benchmarks if this changes. + /// + /// This is enforced in the code; the membership size can not exceed this limit. + #[pallet::constant] + type MaxMembers: Get; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + + /// Helper for setting up cross-pallet state needed by benchmarks. + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper: BenchmarkHelper; + } + + /// Benchmark setup helper. The runtime supplies a non-rotatable + /// collective for member-management benchmarks and a rotatable one + /// for `force_rotate`. + #[cfg(feature = "runtime-benchmarks")] + pub trait BenchmarkHelper { + /// A collective whose `info.max_members` allows reaching `MaxMembers` + /// and whose `info.min_members == 0`, so member-management + /// benchmarks can fill and drain freely. + fn collective() -> T::CollectiveId; + /// A collective whose `CollectiveInfo::term_duration` is `Some`, + /// for the `force_rotate` benchmark. + fn rotatable_collective() -> T::CollectiveId; + } + + /// Members of each collective, kept sorted by `AccountId`. + /// + /// The sorted invariant is maintained by every write path + /// (`add_member`, `remove_member`, `swap_member`, `set_members`) so + /// that membership lookups can use `binary_search` and `set_members` + /// can diff against the previous set with a linear merge. + #[pallet::storage] + pub(super) type Members = StorageMap< + _, + Blake2_128Concat, + T::CollectiveId, + BoundedVec, + ValueQuery, + >; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// An account was added to a collective. + MemberAdded { + /// Collective the account joined. + collective_id: T::CollectiveId, + /// Account that joined. + who: T::AccountId, + }, + /// An account was removed from a collective. + MemberRemoved { + /// Collective the account left. + collective_id: T::CollectiveId, + /// Account that left. + who: T::AccountId, + }, + /// A member of a collective was replaced by another account in + /// a single operation. + MemberSwapped { + /// Collective whose membership changed. + collective_id: T::CollectiveId, + /// Account that left. + removed: T::AccountId, + /// Account that joined in its place. + added: T::AccountId, + }, + /// The full membership of a collective was replaced. + MembersSet { + /// Collective whose membership was replaced. + collective_id: T::CollectiveId, + /// Accounts that became members in this update, sorted. + /// This is the difference against the previous member + /// list, not the full new list. + incoming: Vec, + /// Accounts that stopped being members in this update, + /// sorted. This is the difference against the previous + /// member list. + outgoing: Vec, + }, + } + + #[pallet::error] + pub enum Error { + /// Account is already a member of this collective. + AlreadyMember, + /// Account is not a member of this collective. + NotMember, + /// Adding a member would exceed the maximum for this collective. + TooManyMembers, + /// Removing a member would go below the minimum for this collective. + TooFewMembers, + /// The collective is not recognized. + CollectiveNotFound, + /// Duplicate accounts in member list. + DuplicateAccounts, + /// A rotation was requested for a collective that does not + /// rotate. Such collectives are curated directly through the + /// membership operations and have no rotation hook to trigger. + CollectiveDoesNotRotate, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(n: BlockNumberFor) -> Weight { + // Conservative upper bound for the iteration cost. Matches the + // storage-backed case; static `CollectivesInfo` impls pay a + // smaller CPU cost, so this is a safe overestimate. + let mut weight = Weight::zero().saturating_add(T::DbWeight::get().reads(1)); + + for collective in T::Collectives::collectives() { + if collective + .info + .term_duration + .is_some_and(|td| n.checked_rem(&td).unwrap_or(n).is_zero()) + { + weight.saturating_accrue(T::OnNewTerm::on_new_term(collective.id)); + } + } + + weight + } + + fn integrity_test() { + Pallet::::check_integrity(); + } + + #[cfg(feature = "try-runtime")] + fn try_state( + _n: BlockNumberFor, + ) -> Result<(), frame_support::sp_runtime::TryRuntimeError> { + Pallet::::do_try_state() + } + } + + #[pallet::call] + impl Pallet { + #![deny(clippy::expect_used)] + + /// Add `who` to `collective_id`. + /// + /// Errors: `CollectiveNotFound`, `AlreadyMember`, `TooManyMembers`. + #[pallet::call_index(0)] + #[pallet::weight( + T::WeightInfo::add_member().saturating_add(T::OnMembersChanged::weight()) + )] + pub fn add_member( + origin: OriginFor, + collective_id: T::CollectiveId, + who: T::AccountId, + ) -> DispatchResult { + T::AddOrigin::ensure_origin(origin, &collective_id)?; + Self::do_add_member(collective_id, who)?; + Ok(()) + } + + /// Remove `who` from `collective_id`. Refuses to drop the + /// member count to or below `CollectiveInfo::min_members`. + #[pallet::call_index(1)] + #[pallet::weight( + T::WeightInfo::remove_member().saturating_add(T::OnMembersChanged::weight()) + )] + pub fn remove_member( + origin: OriginFor, + collective_id: T::CollectiveId, + who: T::AccountId, + ) -> DispatchResult { + T::RemoveOrigin::ensure_origin(origin, &collective_id)?; + Self::do_remove_member(collective_id, who)?; + Ok(()) + } + + /// Atomically replace `remove` with `add` in `collective_id`. + /// Member count is preserved, so a swap is allowed even when + /// the collective sits at its `min_members` or `max_members` + /// bound. Swap-with-self is rejected. + #[pallet::call_index(2)] + #[pallet::weight( + T::WeightInfo::swap_member().saturating_add(T::OnMembersChanged::weight()) + )] + pub fn swap_member( + origin: OriginFor, + collective_id: T::CollectiveId, + remove: T::AccountId, + add: T::AccountId, + ) -> DispatchResult { + T::SwapOrigin::ensure_origin(origin, &collective_id)?; + T::Collectives::info(collective_id).ok_or(Error::::CollectiveNotFound)?; + + Members::::try_mutate(collective_id, |members| -> DispatchResult { + let pos_remove = members + .binary_search(&remove) + .map_err(|_| Error::::NotMember)?; + let pos_add = members + .binary_search(&add) + .err() + .ok_or(Error::::AlreadyMember)?; + members.remove(pos_remove); + // After removing index `pos_remove`, every position strictly + // greater than it has shifted down by one. The branch guards + // `pos_add >= 1`, so `saturating_sub` is exact here. + let insert_at = if pos_remove < pos_add { + pos_add.saturating_sub(1) + } else { + pos_add + }; + members + .try_insert(insert_at, add.clone()) + .map_err(|_| Error::::TooManyMembers)?; + Ok(()) + })?; + + T::OnMembersChanged::on_members_changed( + collective_id, + core::slice::from_ref(&add), + core::slice::from_ref(&remove), + ); + Self::deposit_event(Event::MemberSwapped { + collective_id, + removed: remove, + added: add, + }); + Ok(()) + } + + /// Replace the full membership of `collective_id` with `members`. + /// The input may be in any order but must contain no duplicates; + /// the call does not silently deduplicate. + #[pallet::call_index(3)] + #[pallet::weight( + T::WeightInfo::set_members().saturating_add(T::OnMembersChanged::weight()) + )] + pub fn set_members( + origin: OriginFor, + collective_id: T::CollectiveId, + members: Vec, + ) -> DispatchResult { + T::SetOrigin::ensure_origin(origin, &collective_id)?; + Self::do_set_members(collective_id, members)?; + Ok(()) + } + + /// Trigger a rotation of `collective_id` on demand, ahead of its + /// scheduled cadence. Used to bootstrap the first term (the + /// natural cadence only fires after the first term boundary, + /// which can be days or months away) and as a privileged + /// override during incidents. + /// + /// Only valid for collectives that have a configured rotation + /// cadence. Calls against a non-rotating collective fail with + /// `CollectiveDoesNotRotate` rather than silently consuming + /// weight. + #[pallet::call_index(4)] + #[pallet::weight( + T::WeightInfo::force_rotate().saturating_add(T::OnNewTerm::weight()) + )] + pub fn force_rotate( + origin: OriginFor, + collective_id: T::CollectiveId, + ) -> DispatchResultWithPostInfo { + T::RotateOrigin::ensure_origin(origin, &collective_id)?; + let info = T::Collectives::info(collective_id).ok_or(Error::::CollectiveNotFound)?; + ensure!( + info.term_duration.is_some(), + Error::::CollectiveDoesNotRotate + ); + + Ok(Some( + T::WeightInfo::force_rotate() + .saturating_add(T::OnNewTerm::on_new_term(collective_id)), + ) + .into()) + } + } +} + +impl Pallet { + pub fn do_add_member( + collective_id: T::CollectiveId, + who: T::AccountId, + ) -> Result<(), Error> { + let info = T::Collectives::info(collective_id).ok_or(Error::::CollectiveNotFound)?; + + Members::::try_mutate(collective_id, |members| -> Result<(), Error> { + let pos = members + .binary_search(&who) + .err() + .ok_or(Error::::AlreadyMember)?; + if let Some(max) = info.max_members { + ensure!(members.len() < max as usize, Error::::TooManyMembers); + } + members + .try_insert(pos, who.clone()) + .map_err(|_| Error::::TooManyMembers)?; + Ok(()) + })?; + + T::OnMembersChanged::on_members_changed(collective_id, core::slice::from_ref(&who), &[]); + Self::deposit_event(Event::MemberAdded { collective_id, who }); + + Ok(()) + } + + pub fn do_remove_member( + collective_id: T::CollectiveId, + who: T::AccountId, + ) -> Result<(), Error> { + let info = T::Collectives::info(collective_id).ok_or(Error::::CollectiveNotFound)?; + + Members::::try_mutate(collective_id, |members| -> Result<(), Error> { + let pos = members + .binary_search(&who) + .map_err(|_| Error::::NotMember)?; + ensure!( + members.len() > info.min_members as usize, + Error::::TooFewMembers + ); + members.remove(pos); + Ok(()) + })?; + + T::OnMembersChanged::on_members_changed(collective_id, &[], core::slice::from_ref(&who)); + Self::deposit_event(Event::MemberRemoved { collective_id, who }); + + Ok(()) + } + + pub fn do_set_members( + collective_id: T::CollectiveId, + members: Vec, + ) -> Result<(), Error> { + let info = T::Collectives::info(collective_id).ok_or(Error::::CollectiveNotFound)?; + + ensure!( + members.len() >= info.min_members as usize, + Error::::TooFewMembers + ); + ensure!( + members.len() <= T::MaxMembers::get() as usize, + Error::::TooManyMembers + ); + if let Some(max) = info.max_members { + ensure!(members.len() <= max as usize, Error::::TooManyMembers); + } + + let len_before = members.len(); + let mut sorted = members; + sorted.sort(); + sorted.dedup(); + ensure!(sorted.len() == len_before, Error::::DuplicateAccounts); + + let old_members = Members::::get(collective_id); + let bounded = + BoundedVec::try_from(sorted.clone()).map_err(|_| Error::::TooManyMembers)?; + Members::::insert(collective_id, bounded); + + let (incoming, outgoing) = + <() as ChangeMembers>::compute_members_diff_sorted(&sorted, &old_members); + + T::OnMembersChanged::on_members_changed(collective_id, &incoming, &outgoing); + Self::deposit_event(Event::MembersSet { + collective_id, + incoming, + outgoing, + }); + + Ok(()) + } + + /// Validates the `CollectivesInfo` configuration against the + /// pallet's storage cap. Called from the `integrity_test` hook + /// at construction; extracted so tests can drive it directly. + /// + /// Guards against `CollectiveInfo` / `T::MaxMembers` mismatch: a + /// runtime declaring `max_members` (or `min_members`) greater + /// than `T::MaxMembers` would pass the per-collective cap check + /// in `add_member` / `set_members` but then fail the `BoundedVec` + /// bound with a confusing `TooManyMembers` at the storage + /// ceiling. Failing construction here makes the inconsistent + /// config unreachable at runtime. + /// + /// Alternative structural fix (not taken): drop `max_members` + /// from `CollectiveInfo` and expose it via a per-collective + /// method on `CollectivesInfo` computed against `T::MaxMembers` + /// (e.g. `fn max_members_of(id) -> u32`). That eliminates the + /// field mismatch by construction at the cost of a + /// `CollectivesInfo` trait-shape change. + pub fn check_integrity() { + let storage_max = T::MaxMembers::get(); + for collective in T::Collectives::collectives() { + let info = collective.info; + + assert!( + info.min_members <= storage_max, + "CollectiveInfo::min_members ({}) exceeds T::MaxMembers ({}); collective cannot reach its min", + info.min_members, + storage_max, + ); + + if let Some(max) = info.max_members { + assert!( + max <= storage_max, + "CollectiveInfo::max_members ({}) exceeds T::MaxMembers ({}); storage cannot hold this many", + max, + storage_max, + ); + assert!( + info.min_members <= max, + "CollectiveInfo::min_members ({}) exceeds max_members ({}); collective is unreachable", + info.min_members, + max, + ); + } + + // `Some(0)` for term_duration is indistinguishable from "rotate + // every block" at the type level, but the `n % td` check in + // `on_initialize` short-circuits via `checked_rem` and never + // fires. Reject it here rather than let a misconfigured runtime + // silently disable rotations. Use `None` to opt out. + if let Some(td) = info.term_duration { + assert!( + !td.is_zero(), + "CollectiveInfo::term_duration = Some(0) silently disables rotations; use None to opt out", + ); + } + } + } + + /// Storage-state invariants checked by `try-runtime`. Iterates the + /// `Members` map and verifies, for every entry: + /// + /// - the member list is strictly sorted ascending (no duplicates, + /// matching the invariant relied on by `binary_search` and the + /// linear-merge diff in `set_members`); + /// - the `collective_id` is registered in `T::Collectives`, so no + /// orphan rows survive a misconfigured runtime upgrade; + /// - the member count fits the per-collective `info.max_members`, + /// in addition to the type-level `T::MaxMembers` bound that + /// `BoundedVec` already enforces. + /// + /// `info.min_members` is intentionally not asserted here: a + /// freshly registered collective has no `Members` entry until its + /// first mutation, which would trip a strict lower-bound check. + #[cfg(any(feature = "try-runtime", test))] + pub fn do_try_state() -> Result<(), frame_support::sp_runtime::TryRuntimeError> { + for (collective_id, members) in Members::::iter() { + ensure!( + members.windows(2).all(|w| matches!(w, [a, b] if a < b)), + "Members storage is not strictly sorted ascending" + ); + + let info = T::Collectives::info(collective_id) + .ok_or("Members entry references an unregistered collective")?; + + if let Some(max) = info.max_members { + ensure!( + members.len() as u32 <= max, + "Member count exceeds CollectiveInfo::max_members" + ); + } + } + + Ok(()) + } +} + +// Detailed information about a collective. +pub struct CollectiveInfo { + pub name: Name, + /// Minimum number of members for a collective. + pub min_members: u32, + /// Maximum number of members for a collective. + pub max_members: Option, + /// The duration of the term for a collective. + pub term_duration: Option, +} + +/// Collective groups the information of a collective with its corresponding identifier. +pub struct Collective { + /// Identifier of the collective. + pub id: Id, + /// Information about the collective. + pub info: CollectiveInfo, +} + +/// Information on the collectives. +pub trait CollectivesInfo { + /// The identifier for a collective. + type Id: Parameter + MaxEncodedLen + Copy + Ord + PartialOrd + Send + Sync + 'static; + + /// Return the sorted iterable list of known collectives. + fn collectives() -> impl Iterator>; + + /// Return the list of identifiers of the known collectives. + fn collective_ids() -> impl Iterator { + Self::collectives().map(|c| c.id) + } + + /// Return the collective info for collective `id`, by default this just looks it up in `Self::collectives()`. + fn info(id: Self::Id) -> Option> { + Self::collectives().find(|c| c.id == id).map(|c| c.info) + } +} + +/// Handler for when a new term of a collective has started. +pub trait OnNewTerm { + /// A new term of a collective has started. Returns the actual weight + /// consumed so `on_initialize` can accumulate per-block hook weight + /// across all rotating collectives. + fn on_new_term(collective_id: CollectiveId) -> Weight; + + /// Worst-case upper bound on `on_new_term`'s weight, used to + /// pre-charge `force_rotate`. + fn weight() -> Weight; +} + +#[impl_trait_for_tuples::impl_for_tuples(10)] +impl OnNewTerm for Tuple { + // `for_tuples!` mutates `weight` inline; clippy can't see the expansion. + #[allow(clippy::let_and_return)] + fn on_new_term(collective_id: CollectiveId) -> Weight { + let mut weight = Weight::zero(); + for_tuples!( #( weight = weight.saturating_add(Tuple::on_new_term(collective_id.clone())); )* ); + weight + } + + fn weight() -> Weight { + #[allow(clippy::let_and_return)] + let mut weight = Weight::zero(); + for_tuples!( #( weight.saturating_accrue(Tuple::weight()); )* ); + weight + } +} + +/// Trait for inspecting a collective. +pub trait CollectiveInspect { + /// Return the members of a collective. + fn members_of(collective_id: CollectiveId) -> Vec; + + /// Return true once the collective's membership storage is initialized. + fn is_initialized(collective_id: CollectiveId) -> bool; + + /// Return true if an account is a member of a collective. + fn is_member(collective_id: CollectiveId, who: &AccountId) -> bool; + + /// Return the number of members of a collective. + fn member_count(collective_id: CollectiveId) -> u32; +} + +impl CollectiveInspect for Pallet { + fn members_of(collective_id: T::CollectiveId) -> Vec { + Members::::get(collective_id).to_vec() + } + + fn is_initialized(collective_id: T::CollectiveId) -> bool { + Members::::contains_key(collective_id) + } + + fn is_member(collective_id: T::CollectiveId, who: &T::AccountId) -> bool { + Members::::get(collective_id).binary_search(who).is_ok() + } + + fn member_count(collective_id: T::CollectiveId) -> u32 { + Members::::get(collective_id).len() as u32 + } +} diff --git a/pallets/multi-collective/src/mock.rs b/pallets/multi-collective/src/mock.rs new file mode 100644 index 0000000000..b2e5e88262 --- /dev/null +++ b/pallets/multi-collective/src/mock.rs @@ -0,0 +1,322 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::unwrap_used, + clippy::expect_used, + clippy::indexing_slicing +)] + +use core::cell::RefCell; + +use frame_support::{ + derive_impl, + pallet_prelude::*, + parameter_types, + sp_runtime::{BuildStorage, traits::IdentityLookup}, + traits::AsEnsureOriginWithArg, +}; +use frame_system::EnsureRoot; +use sp_core::U256; + +use crate::{ + self as pallet_multi_collective, Collective, CollectiveInfo, CollectivesInfo, OnMembersChanged, + OnNewTerm, +}; + +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test { + System: frame_system = 1, + MultiCollective: pallet_multi_collective = 2, + } +); + +// --- CollectiveId enum --- + +#[derive( + Copy, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Debug, + Encode, + Decode, + DecodeWithMemTracking, + MaxEncodedLen, + TypeInfo, +)] +pub enum CollectiveId { + Alpha, + Beta, + Gamma, + Delta, + /// Intentionally NOT returned by `TestCollectives::collectives()`; used + /// to exercise the `CollectiveNotFound` error path in extrinsics. + Unknown, +} + +// --- CollectivesInfo impl --- + +pub fn name_bytes(s: &[u8]) -> [u8; 32] { + let mut n = [0u8; 32]; + let len = s.len().min(32); + n[..len].copy_from_slice(&s[..len]); + n +} + +pub struct TestCollectives; + +// Optional override used by the integrity-test panic tests. When set, +// `TestCollectives::collectives()` returns the override's output instead of +// the default config. A function pointer is used (not a Vec) so the type +// stays `Copy`. +thread_local! { + static COLLECTIVES_OVERRIDE: RefCell< + Option Vec>>, + > = const { RefCell::new(None) }; +} + +fn default_collectives() -> Vec> { + vec![ + Collective { + id: CollectiveId::Alpha, + info: CollectiveInfo { + name: name_bytes(b"alpha"), + min_members: 0, + max_members: Some(5), + term_duration: None, + }, + }, + Collective { + id: CollectiveId::Beta, + info: CollectiveInfo { + name: name_bytes(b"beta"), + min_members: 2, + max_members: Some(3), + term_duration: Some(100), + }, + }, + Collective { + id: CollectiveId::Gamma, + info: CollectiveInfo { + name: name_bytes(b"gamma"), + min_members: 0, + max_members: None, + term_duration: None, + }, + }, + Collective { + id: CollectiveId::Delta, + info: CollectiveInfo { + name: name_bytes(b"delta"), + min_members: 1, + max_members: Some(32), + term_duration: Some(50), + }, + }, + ] +} + +fn effective_collectives() -> Vec> { + let override_fn = COLLECTIVES_OVERRIDE.with(|o| *o.borrow()); + match override_fn { + Some(f) => f(), + None => default_collectives(), + } +} + +/// Run `f` with `TestCollectives` temporarily returning the output of +/// `override_fn`. An RAII guard clears the override when `f` returns *or +/// panics*, so a `#[should_panic]` integrity test cannot leak state onto +/// other tests running on the same thread. +pub fn with_collectives_override( + override_fn: fn() -> Vec>, + f: impl FnOnce() -> R, +) -> R { + struct Guard; + impl Drop for Guard { + fn drop(&mut self) { + COLLECTIVES_OVERRIDE.with(|o| *o.borrow_mut() = None); + } + } + + COLLECTIVES_OVERRIDE.with(|o| *o.borrow_mut() = Some(override_fn)); + let _guard = Guard; + f() +} + +impl CollectivesInfo for TestCollectives { + type Id = CollectiveId; + + fn collectives() -> impl Iterator> { + effective_collectives().into_iter() + } +} + +// --- Recording stubs for the pallet's two hooks --- +// +// `OnNewTerm` has no event counterpart; the rotation tests need the log to +// observe firings. `OnMembersChanged` is observable indirectly through the +// pallet's events, but the events do not show what was passed to the hook, +// so the recorder lets the hook-payload tests pin the exact arguments. + +thread_local! { + static NEW_TERM_LOG: RefCell> = const { RefCell::new(Vec::new()) }; + static NEW_TERM_WEIGHT: RefCell = const { RefCell::new(Weight::zero()) }; + static MEMBERS_CHANGED_LOG: RefCell> = + const { RefCell::new(Vec::new()) }; +} + +pub struct TestOnNewTerm; + +impl OnNewTerm for TestOnNewTerm { + fn on_new_term(id: CollectiveId) -> Weight { + NEW_TERM_LOG.with(|log| log.borrow_mut().push(id)); + NEW_TERM_WEIGHT.with(|w| *w.borrow()) + } + + fn weight() -> Weight { + NEW_TERM_WEIGHT.with(|w| *w.borrow()) + } +} + +/// Drain and return the recorded `OnNewTerm` calls since the last drain. +pub fn take_new_term_log() -> Vec { + NEW_TERM_LOG.with(|log| log.borrow_mut().drain(..).collect()) +} + +/// Set the weight that `TestOnNewTerm::on_new_term` reports back. Used by +/// `force_rotate` to assert that the post-info weight is the static +/// `WeightInfo::force_rotate()` plus the actual hook weight. +pub fn set_new_term_weight(weight: Weight) { + NEW_TERM_WEIGHT.with(|w| *w.borrow_mut() = weight); +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct MembersChangedCall { + pub collective_id: CollectiveId, + pub incoming: Vec, + pub outgoing: Vec, +} + +pub struct TestOnMembersChanged; + +impl OnMembersChanged for TestOnMembersChanged { + fn on_members_changed(collective_id: CollectiveId, incoming: &[U256], outgoing: &[U256]) { + MEMBERS_CHANGED_LOG.with(|log| { + log.borrow_mut().push(MembersChangedCall { + collective_id, + incoming: incoming.to_vec(), + outgoing: outgoing.to_vec(), + }) + }); + } + + fn weight() -> Weight { + Weight::zero() + } +} + +/// Drain and return the recorded `OnMembersChanged` calls since the last drain. +pub fn take_members_changed_log() -> Vec { + MEMBERS_CHANGED_LOG.with(|log| log.borrow_mut().drain(..).collect()) +} + +/// Returns the `pallet_multi_collective::Event` values recorded in +/// `System::events()` so far, in insertion order. +pub fn multi_collective_events() -> Vec> { + System::events() + .into_iter() + .filter_map(|r| match r.event { + RuntimeEvent::MultiCollective(e) => Some(e), + _ => None, + }) + .collect() +} + +// --- frame_system --- + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type Block = Block; + type AccountId = U256; + type Lookup = IdentityLookup; +} + +// --- pallet_multi_collective --- + +parameter_types! { + pub const MaxMembers: u32 = 32; +} + +impl pallet_multi_collective::Config for Test { + type CollectiveId = CollectiveId; + type Collectives = TestCollectives; + type AddOrigin = AsEnsureOriginWithArg>; + type RemoveOrigin = AsEnsureOriginWithArg>; + type SwapOrigin = AsEnsureOriginWithArg>; + type SetOrigin = AsEnsureOriginWithArg>; + type RotateOrigin = AsEnsureOriginWithArg>; + type OnMembersChanged = TestOnMembersChanged; + type OnNewTerm = TestOnNewTerm; + type MaxMembers = MaxMembers; + type WeightInfo = (); + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = TestBenchmarkHelper; +} + +#[cfg(feature = "runtime-benchmarks")] +pub struct TestBenchmarkHelper; + +#[cfg(feature = "runtime-benchmarks")] +impl pallet_multi_collective::BenchmarkHelper for TestBenchmarkHelper { + fn collective() -> CollectiveId { + // Gamma: max_members = None, min_members = 0 → can fill to MaxMembers + // and drain to empty without tripping the per-collective bounds. + CollectiveId::Gamma + } + + fn rotatable_collective() -> CollectiveId { + // Beta has term_duration = Some(100). + CollectiveId::Beta + } +} + +// --- Test externality builder --- + +/// Build a fresh `TestExternalities` for the mock runtime. Used directly +/// by `impl_benchmark_test_suite!`; `TestState::build_and_execute` wraps +/// this with the per-test bootstrap unit tests rely on. +pub fn new_test_ext() -> sp_io::TestExternalities { + RuntimeGenesisConfig::default() + .build_storage() + .unwrap() + .into() +} + +pub struct TestState; + +impl TestState { + pub fn build_and_execute(test: impl FnOnce()) { + let mut ext = new_test_ext(); + + ext.execute_with(|| { + // System::events() only records events from block >= 1, so + // setting the block first means each test starts with an empty + // events buffer. + System::set_block_number(1); + let _ = take_new_term_log(); + let _ = take_members_changed_log(); + set_new_term_weight(Weight::zero()); + test(); + }); + } +} + +/// Advance to block `n`, invoking `on_finalize(k-1)` + `on_initialize(k)` for +/// each block `k` from the current block+1 up to and including `n`. +pub fn run_to_block(n: u64) { + System::run_to_block::(n); +} diff --git a/pallets/multi-collective/src/tests.rs b/pallets/multi-collective/src/tests.rs new file mode 100644 index 0000000000..43eff7b4d9 --- /dev/null +++ b/pallets/multi-collective/src/tests.rs @@ -0,0 +1,1617 @@ +#![allow(clippy::unwrap_used, clippy::expect_used)] + +use frame_support::{BoundedVec, assert_noop, assert_ok, traits::Hooks, weights::Weight}; +use sp_core::U256; +use sp_runtime::DispatchError; + +use crate::{ + Collective, CollectiveInfo, CollectiveInspect, Error, Event as CollectiveEvent, OnNewTerm, + Pallet as MultiCollective, mock::*, +}; + +#[test] +fn add_member_happy_path() { + TestState::build_and_execute(|| { + let mid = U256::from(5); + let head = U256::from(2); + let tail = U256::from(8); + let between = U256::from(4); + + // Exercises the four insertion positions that `binary_search` can + // return: empty list, before the first element, after the last, + // and into the middle. A regression replacing the sorted insert + // with `push` would only be caught by the head and middle cases. + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + mid, + )); + assert_eq!( + MultiCollective::::members_of(CollectiveId::Alpha), + vec![mid] + ); + assert!(MultiCollective::::is_member( + CollectiveId::Alpha, + &mid + )); + + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + head, + )); + assert_eq!( + MultiCollective::::members_of(CollectiveId::Alpha), + vec![head, mid] + ); + + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + tail, + )); + assert_eq!( + MultiCollective::::members_of(CollectiveId::Alpha), + vec![head, mid, tail] + ); + + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + between, + )); + assert_eq!( + MultiCollective::::members_of(CollectiveId::Alpha), + vec![head, between, mid, tail] + ); + + assert_eq!( + MultiCollective::::member_count(CollectiveId::Alpha), + 4 + ); + + assert_eq!( + multi_collective_events(), + vec![ + CollectiveEvent::MemberAdded { + collective_id: CollectiveId::Alpha, + who: mid, + }, + CollectiveEvent::MemberAdded { + collective_id: CollectiveId::Alpha, + who: head, + }, + CollectiveEvent::MemberAdded { + collective_id: CollectiveId::Alpha, + who: tail, + }, + CollectiveEvent::MemberAdded { + collective_id: CollectiveId::Alpha, + who: between, + }, + ] + ); + }); +} + +#[test] +fn add_member_requires_origin() { + TestState::build_and_execute(|| { + let alice = U256::from(1); + let caller = U256::from(999); + + assert_noop!( + MultiCollective::::add_member( + RuntimeOrigin::signed(caller), + CollectiveId::Alpha, + alice, + ), + DispatchError::BadOrigin + ); + + assert!(MultiCollective::::members_of(CollectiveId::Alpha).is_empty()); + assert!(multi_collective_events().is_empty()); + }); +} + +#[test] +fn add_member_fails_for_unknown_collective() { + TestState::build_and_execute(|| { + let alice = U256::from(1); + + assert_noop!( + MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Unknown, + alice, + ), + Error::::CollectiveNotFound + ); + + assert!(multi_collective_events().is_empty()); + }); +} + +#[test] +fn add_member_rejects_duplicate() { + TestState::build_and_execute(|| { + let alice = U256::from(1); + + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + alice, + )); + + assert_noop!( + MultiCollective::::add_member(RuntimeOrigin::root(), CollectiveId::Alpha, alice,), + Error::::AlreadyMember + ); + + // Only one MemberAdded event; the failing call produced nothing. + assert_eq!( + multi_collective_events(), + vec![CollectiveEvent::MemberAdded { + collective_id: CollectiveId::Alpha, + who: alice, + }] + ); + assert_eq!( + MultiCollective::::member_count(CollectiveId::Alpha), + 1 + ); + }); +} + +#[test] +fn add_member_respects_info_max() { + TestState::build_and_execute(|| { + // Alpha declares max_members = Some(5). Fill it exactly to capacity. + for i in 1..=5u32 { + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + U256::from(i), + )); + } + assert_eq!( + MultiCollective::::member_count(CollectiveId::Alpha), + 5 + ); + + assert_noop!( + MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + U256::from(6), + ), + Error::::TooManyMembers + ); + + assert_eq!( + MultiCollective::::member_count(CollectiveId::Alpha), + 5 + ); + // Exactly five events; nothing from the failing 6th. + assert_eq!(multi_collective_events().len(), 5); + }); +} + +#[test] +fn add_member_respects_storage_max_when_info_max_none() { + TestState::build_and_execute(|| { + // Gamma's `info.max_members` is None; only `T::MaxMembers = 32` applies. + for i in 1..=32u32 { + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Gamma, + U256::from(i), + )); + } + assert_eq!( + MultiCollective::::member_count(CollectiveId::Gamma), + 32 + ); + + // 33rd add fails via `try_insert` (BoundedVec bound) rather than the info cap. + assert_noop!( + MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Gamma, + U256::from(33), + ), + Error::::TooManyMembers + ); + + assert_eq!( + MultiCollective::::member_count(CollectiveId::Gamma), + 32 + ); + assert_eq!(multi_collective_events().len(), 32); + }); +} + +#[test] +fn remove_member_happy_path() { + TestState::build_and_execute(|| { + let alice = U256::from(1); + let bob = U256::from(2); + let charlie = U256::from(3); + + for who in [alice, bob, charlie] { + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + who, + )); + } + + // Remove from the middle. + assert_ok!(MultiCollective::::remove_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + bob, + )); + + assert_eq!( + MultiCollective::::members_of(CollectiveId::Alpha), + vec![alice, charlie] + ); + assert!(!MultiCollective::::is_member( + CollectiveId::Alpha, + &bob + )); + assert_eq!( + MultiCollective::::member_count(CollectiveId::Alpha), + 2 + ); + + // Remove from the head. A swap-remove would leave the list + // unsorted (`[charlie, ...]` shifting via swap), so asserting + // that the remaining tail stays in order discriminates against + // that regression. + assert_ok!(MultiCollective::::remove_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + alice, + )); + + assert_eq!( + MultiCollective::::members_of(CollectiveId::Alpha), + vec![charlie] + ); + assert!(!MultiCollective::::is_member( + CollectiveId::Alpha, + &alice + )); + assert_eq!( + MultiCollective::::member_count(CollectiveId::Alpha), + 1 + ); + + assert_eq!( + multi_collective_events().last(), + Some(&CollectiveEvent::MemberRemoved { + collective_id: CollectiveId::Alpha, + who: alice, + }) + ); + }); +} + +#[test] +fn remove_member_requires_origin() { + TestState::build_and_execute(|| { + let alice = U256::from(1); + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + alice, + )); + + assert_noop!( + MultiCollective::::remove_member( + RuntimeOrigin::signed(U256::from(999)), + CollectiveId::Alpha, + alice, + ), + DispatchError::BadOrigin + ); + + assert!(MultiCollective::::is_member( + CollectiveId::Alpha, + &alice + )); + }); +} + +#[test] +fn remove_member_fails_for_unknown_collective() { + TestState::build_and_execute(|| { + assert_noop!( + MultiCollective::::remove_member( + RuntimeOrigin::root(), + CollectiveId::Unknown, + U256::from(1), + ), + Error::::CollectiveNotFound + ); + + assert!(multi_collective_events().is_empty()); + }); +} + +#[test] +fn remove_member_rejects_non_member() { + TestState::build_and_execute(|| { + assert_noop!( + MultiCollective::::remove_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + U256::from(1), + ), + Error::::NotMember + ); + + assert!(multi_collective_events().is_empty()); + }); +} + +#[test] +fn remove_member_respects_min() { + TestState::build_and_execute(|| { + // Beta declares min_members = 2. Seed exactly to the floor. + let alice = U256::from(1); + let bob = U256::from(2); + for who in [alice, bob] { + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Beta, + who, + )); + } + + assert_noop!( + MultiCollective::::remove_member( + RuntimeOrigin::root(), + CollectiveId::Beta, + alice, + ), + Error::::TooFewMembers + ); + + assert_eq!(MultiCollective::::member_count(CollectiveId::Beta), 2); + }); +} + +#[test] +fn remove_member_allows_down_to_min() { + TestState::build_and_execute(|| { + // Beta has min_members = 2; seed with one above. + let alice = U256::from(1); + let bob = U256::from(2); + let charlie = U256::from(3); + for who in [alice, bob, charlie] { + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Beta, + who, + )); + } + + // Removing once leaves the collective at min_members; the check is + // `len() > min_members` so post-removal len == min_members is allowed. + assert_ok!(MultiCollective::::remove_member( + RuntimeOrigin::root(), + CollectiveId::Beta, + charlie, + )); + + assert_eq!(MultiCollective::::member_count(CollectiveId::Beta), 2); + assert!(!MultiCollective::::is_member( + CollectiveId::Beta, + &charlie + )); + + assert_eq!( + multi_collective_events().last(), + Some(&CollectiveEvent::MemberRemoved { + collective_id: CollectiveId::Beta, + who: charlie, + }) + ); + }); +} + +#[test] +fn swap_member_happy_path() { + TestState::build_and_execute(|| { + let alice = U256::from(1); + let bob = U256::from(2); + let charlie = U256::from(3); + let dave = U256::from(4); + let zara = U256::from(10); + + for who in [alice, bob, charlie] { + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + who, + )); + } + + // Swap the middle member for an account that sorts to the tail. + assert_ok!(MultiCollective::::swap_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + bob, + dave, + )); + + // Members are kept sorted: dave (4) goes after charlie (3). + assert_eq!( + MultiCollective::::members_of(CollectiveId::Alpha), + vec![alice, charlie, dave] + ); + assert!(!MultiCollective::::is_member( + CollectiveId::Alpha, + &bob + )); + assert!(MultiCollective::::is_member( + CollectiveId::Alpha, + &dave + )); + + assert_eq!( + multi_collective_events().last(), + Some(&CollectiveEvent::MemberSwapped { + collective_id: CollectiveId::Alpha, + removed: bob, + added: dave, + }) + ); + + // Swap the head member for an account that sorts to the tail. + // A swap-remove regression on the remove side would leave the + // resulting list unsorted, so this exercises both sides of the + // sorted invariant. + assert_ok!(MultiCollective::::swap_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + alice, + zara, + )); + assert_eq!( + MultiCollective::::members_of(CollectiveId::Alpha), + vec![charlie, dave, zara] + ); + }); +} + +#[test] +fn swap_member_requires_origin() { + TestState::build_and_execute(|| { + let alice = U256::from(1); + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + alice, + )); + + assert_noop!( + MultiCollective::::swap_member( + RuntimeOrigin::signed(U256::from(999)), + CollectiveId::Alpha, + alice, + U256::from(2), + ), + DispatchError::BadOrigin + ); + + assert_eq!( + MultiCollective::::members_of(CollectiveId::Alpha), + vec![alice] + ); + }); +} + +#[test] +fn swap_member_fails_for_unknown_collective() { + TestState::build_and_execute(|| { + assert_noop!( + MultiCollective::::swap_member( + RuntimeOrigin::root(), + CollectiveId::Unknown, + U256::from(1), + U256::from(2), + ), + Error::::CollectiveNotFound + ); + + assert!(multi_collective_events().is_empty()); + }); +} + +#[test] +fn swap_member_rejects_missing_remove() { + TestState::build_and_execute(|| { + assert_noop!( + MultiCollective::::swap_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + U256::from(1), + U256::from(2), + ), + Error::::NotMember + ); + + assert!(multi_collective_events().is_empty()); + }); +} + +#[test] +fn swap_member_rejects_existing_add() { + TestState::build_and_execute(|| { + let alice = U256::from(1); + let bob = U256::from(2); + + for who in [alice, bob] { + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + who, + )); + } + + assert_noop!( + MultiCollective::::swap_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + alice, + bob, + ), + Error::::AlreadyMember + ); + + // Both still present, in their original order. + assert_eq!( + MultiCollective::::members_of(CollectiveId::Alpha), + vec![alice, bob] + ); + }); +} + +#[test] +fn swap_member_rejects_self_swap() { + TestState::build_and_execute(|| { + let alice = U256::from(1); + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + alice, + )); + + // `remove` matches a member, so `NotMember` doesn't fire; the next + // check (`!contains(add)`) rejects because add is already present + // (it is `remove` itself). "Swap with self" is a no-op the pallet + // refuses. + assert_noop!( + MultiCollective::::swap_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + alice, + alice, + ), + Error::::AlreadyMember + ); + + assert_eq!( + MultiCollective::::members_of(CollectiveId::Alpha), + vec![alice] + ); + }); +} + +/// Beta has `min_members = 2, max_members = 3`. Swap is count-invariant +/// and skips both bounds checks, so it must succeed at either end. +/// Setup walks the collective from min to max via `add_member`, then +/// swaps once at each bound. +#[test] +fn swap_member_works_at_bounds() { + TestState::build_and_execute(|| { + let alice = U256::from(1); + let bob = U256::from(2); + let carol = U256::from(3); + let dave = U256::from(4); + let erin = U256::from(5); + + for who in [alice, bob] { + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Beta, + who, + )); + } + + // At min: swap alice for carol. + assert_ok!(MultiCollective::::swap_member( + RuntimeOrigin::root(), + CollectiveId::Beta, + alice, + carol, + )); + assert_eq!(MultiCollective::::member_count(CollectiveId::Beta), 2); + assert!(!MultiCollective::::is_member( + CollectiveId::Beta, + &alice + )); + assert!(MultiCollective::::is_member( + CollectiveId::Beta, + &carol + )); + + // Grow to max, then at max: swap carol for dave. + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Beta, + dave, + )); + assert_eq!(MultiCollective::::member_count(CollectiveId::Beta), 3); + + assert_ok!(MultiCollective::::swap_member( + RuntimeOrigin::root(), + CollectiveId::Beta, + carol, + erin, + )); + assert_eq!(MultiCollective::::member_count(CollectiveId::Beta), 3); + assert!(!MultiCollective::::is_member( + CollectiveId::Beta, + &carol + )); + assert!(MultiCollective::::is_member( + CollectiveId::Beta, + &erin + )); + }); +} + +#[test] +fn set_members_replaces_list() { + TestState::build_and_execute(|| { + let a = U256::from(1); + let b = U256::from(2); + let c = U256::from(3); + let d = U256::from(4); + let e = U256::from(5); + + for who in [a, b] { + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + who, + )); + } + + assert_ok!(MultiCollective::::set_members( + RuntimeOrigin::root(), + CollectiveId::Alpha, + vec![c, d, e], + )); + + assert_eq!( + MultiCollective::::members_of(CollectiveId::Alpha), + vec![c, d, e] + ); + assert!(!MultiCollective::::is_member(CollectiveId::Alpha, &a)); + assert!(!MultiCollective::::is_member(CollectiveId::Alpha, &b)); + + assert_eq!( + multi_collective_events().last(), + Some(&CollectiveEvent::MembersSet { + collective_id: CollectiveId::Alpha, + outgoing: vec![a, b], + incoming: vec![c, d, e], + }) + ); + }); +} + +#[test] +fn set_members_handles_overlap() { + TestState::build_and_execute(|| { + let a = U256::from(1); + let b = U256::from(2); + let c = U256::from(3); + let d = U256::from(4); + + for who in [a, b, c] { + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + who, + )); + } + + // [b, c, d] overlaps with the old [a, b, c]: b and c stay, a goes out, + // d comes in. Final storage reflects the new list verbatim. + assert_ok!(MultiCollective::::set_members( + RuntimeOrigin::root(), + CollectiveId::Alpha, + vec![b, c, d], + )); + + assert_eq!( + MultiCollective::::members_of(CollectiveId::Alpha), + vec![b, c, d] + ); + + assert_eq!( + multi_collective_events().last(), + Some(&CollectiveEvent::MembersSet { + collective_id: CollectiveId::Alpha, + outgoing: vec![a], + incoming: vec![d], + }) + ); + }); +} + +#[test] +fn set_members_requires_origin() { + TestState::build_and_execute(|| { + assert_noop!( + MultiCollective::::set_members( + RuntimeOrigin::signed(U256::from(999)), + CollectiveId::Alpha, + vec![U256::from(1)], + ), + DispatchError::BadOrigin + ); + + assert!(MultiCollective::::members_of(CollectiveId::Alpha).is_empty()); + assert!(multi_collective_events().is_empty()); + }); +} + +#[test] +fn set_members_fails_for_unknown_collective() { + TestState::build_and_execute(|| { + assert_noop!( + MultiCollective::::set_members( + RuntimeOrigin::root(), + CollectiveId::Unknown, + vec![U256::from(1)], + ), + Error::::CollectiveNotFound + ); + + assert!(multi_collective_events().is_empty()); + }); +} + +#[test] +fn set_members_rejects_too_few() { + TestState::build_and_execute(|| { + // Beta declares min_members = 2. + assert_noop!( + MultiCollective::::set_members( + RuntimeOrigin::root(), + CollectiveId::Beta, + vec![U256::from(1)], + ), + Error::::TooFewMembers + ); + + assert!(MultiCollective::::members_of(CollectiveId::Beta).is_empty()); + assert!(multi_collective_events().is_empty()); + }); +} + +#[test] +fn set_members_rejects_too_many_via_info() { + TestState::build_and_execute(|| { + // Beta declares max_members = Some(3); four accounts is one over. + let list: Vec = (1..=4u32).map(U256::from).collect(); + assert_noop!( + MultiCollective::::set_members(RuntimeOrigin::root(), CollectiveId::Beta, list,), + Error::::TooManyMembers + ); + + assert!(MultiCollective::::members_of(CollectiveId::Beta).is_empty()); + assert!(multi_collective_events().is_empty()); + }); +} + +#[test] +fn set_members_rejects_too_many_via_storage() { + TestState::build_and_execute(|| { + // Gamma's info.max_members is None; only T::MaxMembers = 32 applies. + // 33 accounts exceed the BoundedVec bound, caught by try_from. + let list: Vec = (1..=33u32).map(U256::from).collect(); + assert_noop!( + MultiCollective::::set_members(RuntimeOrigin::root(), CollectiveId::Gamma, list,), + Error::::TooManyMembers + ); + + assert!(MultiCollective::::members_of(CollectiveId::Gamma).is_empty()); + }); +} + +#[test] +fn set_members_rejects_duplicates() { + TestState::build_and_execute(|| { + let a = U256::from(1); + let b = U256::from(2); + + assert_noop!( + MultiCollective::::set_members( + RuntimeOrigin::root(), + CollectiveId::Alpha, + vec![a, b, a], + ), + Error::::DuplicateAccounts + ); + + assert!(MultiCollective::::members_of(CollectiveId::Alpha).is_empty()); + }); +} + +/// Setting a list identical to the current membership still emits a +/// `MembersSet` event; the pallet doesn't short-circuit no-op sets. +/// Pinned so downstream consumers know they must tolerate empty-diff calls. +#[test] +fn set_members_noop_still_fires_event() { + TestState::build_and_execute(|| { + let a = U256::from(1); + let b = U256::from(2); + + for who in [a, b] { + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + who, + )); + } + + assert_ok!(MultiCollective::::set_members( + RuntimeOrigin::root(), + CollectiveId::Alpha, + vec![a, b], + )); + + assert_eq!( + MultiCollective::::members_of(CollectiveId::Alpha), + vec![a, b] + ); + + assert_eq!( + multi_collective_events().last(), + Some(&CollectiveEvent::MembersSet { + collective_id: CollectiveId::Alpha, + incoming: vec![], + outgoing: vec![], + }) + ); + }); +} + +#[test] +fn on_initialize_no_rotation_when_term_duration_none() { + TestState::build_and_execute(|| { + // Alpha (td=None) and Gamma (td=None) must never appear in the log + // regardless of how many blocks pass. + run_to_block(300); + + let log = take_new_term_log(); + assert!( + !log.contains(&CollectiveId::Alpha), + "Alpha has term_duration = None; should never rotate" + ); + assert!( + !log.contains(&CollectiveId::Gamma), + "Gamma has term_duration = None; should never rotate" + ); + }); +} + +#[test] +fn on_initialize_no_rotation_between_boundaries() { + TestState::build_and_execute(|| { + // Earliest boundary is Delta's at block 50. Before that, nothing fires. + run_to_block(49); + assert!(take_new_term_log().is_empty()); + }); +} + +#[test] +fn on_initialize_fires_rotation_at_modulo_boundary() { + TestState::build_and_execute(|| { + // Delta (td=50) first fires at block 50. The "no rotation between + // boundaries" property is covered by + // `on_initialize_no_rotation_between_boundaries`. + run_to_block(50); + assert_eq!(take_new_term_log(), vec![CollectiveId::Delta]); + }); +} + +#[test] +fn on_initialize_fires_all_matching_collectives() { + TestState::build_and_execute(|| { + // Advance through the first shared boundary at block 100. Delta fires + // at 50, then both Beta and Delta fire at 100. Iteration order in + // `TestCollectives` is [Alpha, Beta, Gamma, Delta], so within block + // 100 the log gets Beta before Delta. + run_to_block(100); + + assert_eq!( + take_new_term_log(), + vec![ + CollectiveId::Delta, // block 50 + CollectiveId::Beta, // block 100 + CollectiveId::Delta, // block 100 + ] + ); + + // Next cadence: only Delta at 150, both again at 200. + run_to_block(150); + assert_eq!(take_new_term_log(), vec![CollectiveId::Delta]); + + run_to_block(200); + assert_eq!( + take_new_term_log(), + vec![CollectiveId::Beta, CollectiveId::Delta] + ); + }); +} + +#[test] +fn force_rotate_routes_through_on_new_term() { + TestState::build_and_execute(|| { + // Beta has term_duration = Some(100), so it's eligible. + assert_ok!(MultiCollective::::force_rotate( + RuntimeOrigin::root(), + CollectiveId::Beta, + )); + assert_eq!(take_new_term_log(), vec![CollectiveId::Beta]); + }); +} + +#[test] +fn force_rotate_requires_origin() { + TestState::build_and_execute(|| { + assert_noop!( + MultiCollective::::force_rotate( + RuntimeOrigin::signed(U256::from(1)), + CollectiveId::Beta, + ), + DispatchError::BadOrigin, + ); + assert!(take_new_term_log().is_empty()); + }); +} + +#[test] +fn force_rotate_rejects_non_rotating_collective() { + TestState::build_and_execute(|| { + // Alpha has `term_duration: None`. + assert_noop!( + MultiCollective::::force_rotate(RuntimeOrigin::root(), CollectiveId::Alpha,), + Error::::CollectiveDoesNotRotate, + ); + assert!(take_new_term_log().is_empty()); + }); +} + +#[test] +fn force_rotate_rejects_unknown_collective() { + TestState::build_and_execute(|| { + assert_noop!( + MultiCollective::::force_rotate(RuntimeOrigin::root(), CollectiveId::Unknown,), + Error::::CollectiveNotFound, + ); + assert!(take_new_term_log().is_empty()); + }); +} + +#[test] +fn inspect_is_member_basic() { + TestState::build_and_execute(|| { + let alice = U256::from(1); + let mallory = U256::from(999); + + // Empty collective: no membership. + assert!(!MultiCollective::::is_member( + CollectiveId::Alpha, + &alice + )); + + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + alice, + )); + + assert!(MultiCollective::::is_member( + CollectiveId::Alpha, + &alice + )); + assert!(!MultiCollective::::is_member( + CollectiveId::Alpha, + &mallory + )); + // Membership is per-collective; alice isn't in Beta. + assert!(!MultiCollective::::is_member( + CollectiveId::Beta, + &alice + )); + }); +} + +#[test] +fn inspect_member_count_matches_mutations() { + TestState::build_and_execute(|| { + let a = U256::from(1); + let b = U256::from(2); + let c = U256::from(3); + let d = U256::from(4); + + assert_eq!( + MultiCollective::::member_count(CollectiveId::Alpha), + 0 + ); + + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + a, + )); + assert_eq!( + MultiCollective::::member_count(CollectiveId::Alpha), + 1 + ); + + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + b, + )); + assert_eq!( + MultiCollective::::member_count(CollectiveId::Alpha), + 2 + ); + + // Swap is count-invariant. + assert_ok!(MultiCollective::::swap_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + a, + c, + )); + assert_eq!( + MultiCollective::::member_count(CollectiveId::Alpha), + 2 + ); + + // Remove decrements by one. + assert_ok!(MultiCollective::::remove_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + b, + )); + assert_eq!( + MultiCollective::::member_count(CollectiveId::Alpha), + 1 + ); + + // `set_members` replaces wholesale; count reflects the new list length. + assert_ok!(MultiCollective::::set_members( + RuntimeOrigin::root(), + CollectiveId::Alpha, + vec![a, b, c, d], + )); + assert_eq!( + MultiCollective::::member_count(CollectiveId::Alpha), + 4 + ); + }); +} + +#[test] +fn inspect_of_unknown_collective_returns_empty() { + TestState::build_and_execute(|| { + // `Unknown` is not registered in TestCollectives::collectives(). + // `Members` storage uses ValueQuery and returns an empty BoundedVec by + // default, so all three reads succeed without error or panic. + assert_eq!( + MultiCollective::::members_of(CollectiveId::Unknown), + Vec::::new() + ); + assert!(!MultiCollective::::is_member( + CollectiveId::Unknown, + &U256::from(1) + )); + assert_eq!( + MultiCollective::::member_count(CollectiveId::Unknown), + 0 + ); + }); +} + +// `integrity_test_passes_on_valid_config` is implicit: the mock's +// auto-generated `__construct_runtime_integrity_test::runtime_integrity_tests` +// runs `integrity_test()` against the default `TestCollectives` on every +// `cargo test`. Listed in test output as `mock::...runtime_integrity_tests`. + +fn bad_min_exceeds_storage() -> Vec> { + vec![Collective { + id: CollectiveId::Alpha, + info: CollectiveInfo { + name: name_bytes(b"bad"), + // T::MaxMembers = 32 in the mock; 100 exceeds storage capacity. + min_members: 100, + max_members: Some(200), + term_duration: None, + }, + }] +} + +fn bad_max_exceeds_storage() -> Vec> { + vec![Collective { + id: CollectiveId::Alpha, + info: CollectiveInfo { + name: name_bytes(b"bad"), + min_members: 0, + // T::MaxMembers = 32; max_members = 100 is declaratively larger. + max_members: Some(100), + term_duration: None, + }, + }] +} + +fn bad_min_exceeds_info_max() -> Vec> { + vec![Collective { + id: CollectiveId::Alpha, + info: CollectiveInfo { + name: name_bytes(b"bad"), + // min > max: the collective can never satisfy both. + min_members: 5, + max_members: Some(3), + term_duration: None, + }, + }] +} + +fn bad_term_duration_zero() -> Vec> { + vec![Collective { + id: CollectiveId::Alpha, + info: CollectiveInfo { + name: name_bytes(b"bad"), + min_members: 0, + max_members: Some(5), + // Some(0) silently disables rotations; integrity_test rejects it. + term_duration: Some(0), + }, + }] +} + +#[test] +#[should_panic(expected = "min_members (100) exceeds T::MaxMembers (32)")] +fn integrity_test_panics_on_min_exceeds_storage_max() { + with_collectives_override(bad_min_exceeds_storage, || { + as Hooks>::integrity_test(); + }); +} + +#[test] +#[should_panic(expected = "max_members (100) exceeds T::MaxMembers (32)")] +fn integrity_test_panics_on_max_exceeds_storage_max() { + with_collectives_override(bad_max_exceeds_storage, || { + as Hooks>::integrity_test(); + }); +} + +#[test] +#[should_panic(expected = "min_members (5) exceeds max_members (3)")] +fn integrity_test_panics_on_min_exceeds_info_max() { + with_collectives_override(bad_min_exceeds_info_max, || { + as Hooks>::integrity_test(); + }); +} + +#[test] +#[should_panic(expected = "silently disables rotations")] +fn integrity_test_panics_on_term_duration_zero() { + with_collectives_override(bad_term_duration_zero, || { + as Hooks>::integrity_test(); + }); +} + +// `OnMembersChanged` payload tests. The pallet's events show what changed +// in storage but not what was passed to the hook, so an argument-order +// regression (e.g. swapping `incoming` and `outgoing`) would not be +// caught by the event assertions alone. + +#[test] +fn on_members_changed_payload_for_add_member() { + TestState::build_and_execute(|| { + let alice = U256::from(1); + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + alice, + )); + assert_eq!( + take_members_changed_log(), + vec![MembersChangedCall { + collective_id: CollectiveId::Alpha, + incoming: vec![alice], + outgoing: vec![], + }] + ); + }); +} + +#[test] +fn on_members_changed_payload_for_remove_member() { + TestState::build_and_execute(|| { + let alice = U256::from(1); + let bob = U256::from(2); + for who in [alice, bob] { + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + who, + )); + } + let _ = take_members_changed_log(); + + assert_ok!(MultiCollective::::remove_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + bob, + )); + assert_eq!( + take_members_changed_log(), + vec![MembersChangedCall { + collective_id: CollectiveId::Alpha, + incoming: vec![], + outgoing: vec![bob], + }] + ); + }); +} + +#[test] +fn on_members_changed_payload_for_swap_member() { + TestState::build_and_execute(|| { + let alice = U256::from(1); + let bob = U256::from(2); + for who in [alice, bob] { + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + who, + )); + } + let _ = take_members_changed_log(); + + let carol = U256::from(3); + assert_ok!(MultiCollective::::swap_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + alice, + carol, + )); + assert_eq!( + take_members_changed_log(), + vec![MembersChangedCall { + collective_id: CollectiveId::Alpha, + incoming: vec![carol], + outgoing: vec![alice], + }] + ); + }); +} + +#[test] +fn on_members_changed_payload_for_set_members() { + TestState::build_and_execute(|| { + let a = U256::from(1); + let b = U256::from(2); + let c = U256::from(3); + let d = U256::from(4); + for who in [a, b, c] { + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + who, + )); + } + let _ = take_members_changed_log(); + + assert_ok!(MultiCollective::::set_members( + RuntimeOrigin::root(), + CollectiveId::Alpha, + vec![b, c, d], + )); + assert_eq!( + take_members_changed_log(), + vec![MembersChangedCall { + collective_id: CollectiveId::Alpha, + incoming: vec![d], + outgoing: vec![a], + }] + ); + }); +} + +// `do_try_state` direct tests. The extrinsics maintain the invariants by +// construction, so corrupting `Members` storage manually is the only way +// to exercise each failure branch. + +fn write_raw_members(id: CollectiveId, members: Vec) { + let bounded = BoundedVec::try_from(members).expect("test fixture must fit MaxMembers"); + crate::pallet::Members::::insert(id, bounded); +} + +#[test] +fn try_state_passes_on_valid_storage() { + TestState::build_and_execute(|| { + for who in [U256::from(1), U256::from(2)] { + assert_ok!(MultiCollective::::add_member( + RuntimeOrigin::root(), + CollectiveId::Alpha, + who, + )); + } + assert!(MultiCollective::::do_try_state().is_ok()); + }); +} + +#[test] +fn try_state_rejects_unsorted_storage() { + TestState::build_and_execute(|| { + write_raw_members(CollectiveId::Alpha, vec![U256::from(2), U256::from(1)]); + assert!(MultiCollective::::do_try_state().is_err()); + }); +} + +#[test] +fn try_state_rejects_orphan_collective_row() { + TestState::build_and_execute(|| { + // `Unknown` is reachable via the storage map's `Blake2_128Concat` + // hash but is not registered in `TestCollectives::collectives()`. + write_raw_members(CollectiveId::Unknown, vec![U256::from(1)]); + assert!(MultiCollective::::do_try_state().is_err()); + }); +} + +#[test] +fn try_state_rejects_count_exceeding_info_max() { + TestState::build_and_execute(|| { + // Beta declares max_members = 3; four entries fit the BoundedVec + // bound (T::MaxMembers = 32) but violate the per-collective cap. + let four: Vec = (1..=4u32).map(U256::from).collect(); + write_raw_members(CollectiveId::Beta, four); + assert!(MultiCollective::::do_try_state().is_err()); + }); +} + +/// `set_members` sorts its input before writing. Without this step, +/// downstream `binary_search` and `compute_members_diff_sorted` calls +/// would silently observe an unsorted storage entry; pinning the sort +/// here guards against a regression that drops the `sorted.sort()` call. +#[test] +fn set_members_sorts_input() { + TestState::build_and_execute(|| { + let a = U256::from(1); + let b = U256::from(2); + let c = U256::from(3); + + assert_ok!(MultiCollective::::set_members( + RuntimeOrigin::root(), + CollectiveId::Alpha, + vec![c, a, b], + )); + + assert_eq!( + MultiCollective::::members_of(CollectiveId::Alpha), + vec![a, b, c] + ); + }); +} + +/// `force_rotate` returns `Some(actual_weight)` equal to +/// `WeightInfo::force_rotate() + OnNewTerm::on_new_term(...)`. The mock's +/// `WeightInfo` is `()`, whose generated impl reports the pallet's base +/// dispatch cost, so the post-info weight should include that static cost +/// plus the hook's reported cost. +#[test] +fn force_rotate_returns_post_info_weight() { + TestState::build_and_execute(|| { + let hook_weight = Weight::from_parts(123_456, 0); + set_new_term_weight(hook_weight); + + let post = MultiCollective::::force_rotate(RuntimeOrigin::root(), CollectiveId::Beta) + .expect("force_rotate succeeds for Beta"); + + assert_eq!( + post.actual_weight, + Some( + <::WeightInfo as crate::WeightInfo>::force_rotate() + .saturating_add(hook_weight) + ) + ); + }); +} + +/// The pallet ships a tuple impl of `OnNewTerm` so a runtime can fan a +/// rotation out to multiple handlers. The mock wires a single impl, so +/// without this test the tuple expansion is not exercised by `cargo test`. +#[test] +fn on_new_term_tuple_impl_dispatches_to_each_member() { + TestState::build_and_execute(|| { + set_new_term_weight(Weight::from_parts(7, 0)); + + let combined = <(TestOnNewTerm, TestOnNewTerm) as OnNewTerm>::on_new_term( + CollectiveId::Beta, + ); + + assert_eq!(combined, Weight::from_parts(14, 0)); + assert_eq!( + take_new_term_log(), + vec![CollectiveId::Beta, CollectiveId::Beta] + ); + + let weight = <(TestOnNewTerm, TestOnNewTerm) as OnNewTerm>::weight(); + assert_eq!(weight, Weight::from_parts(14, 0)); + }); +} + +#[test] +fn do_add_member_inserts_and_emits_event() { + TestState::build_and_execute(|| { + let who = U256::from(7); + + assert_ok!(MultiCollective::::do_add_member( + CollectiveId::Alpha, + who, + )); + + assert_eq!( + MultiCollective::::members_of(CollectiveId::Alpha), + vec![who] + ); + assert_eq!( + take_members_changed_log(), + vec![MembersChangedCall { + collective_id: CollectiveId::Alpha, + incoming: vec![who], + outgoing: vec![], + }] + ); + assert_eq!( + multi_collective_events().last().expect("event emitted"), + &CollectiveEvent::MemberAdded { + collective_id: CollectiveId::Alpha, + who, + }, + ); + }); +} + +#[test] +fn do_add_member_errors_on_already_member() { + TestState::build_and_execute(|| { + let who = U256::from(7); + assert_ok!(MultiCollective::::do_add_member( + CollectiveId::Alpha, + who, + )); + assert!(matches!( + MultiCollective::::do_add_member(CollectiveId::Alpha, who), + Err(Error::::AlreadyMember), + )); + }); +} + +#[test] +fn do_add_member_errors_on_unknown_collective() { + TestState::build_and_execute(|| { + assert!(matches!( + MultiCollective::::do_add_member(CollectiveId::Unknown, U256::from(1)), + Err(Error::::CollectiveNotFound), + )); + }); +} + +#[test] +fn do_add_member_errors_when_max_members_reached() { + TestState::build_and_execute(|| { + // Alpha caps at 5 members. + for i in 0..5u32 { + assert_ok!(MultiCollective::::do_add_member( + CollectiveId::Alpha, + U256::from(i), + )); + } + assert!(matches!( + MultiCollective::::do_add_member(CollectiveId::Alpha, U256::from(99)), + Err(Error::::TooManyMembers), + )); + }); +} + +#[test] +fn do_remove_member_removes_and_emits_event() { + TestState::build_and_execute(|| { + let who = U256::from(7); + assert_ok!(MultiCollective::::do_add_member( + CollectiveId::Alpha, + who, + )); + let _ = take_members_changed_log(); + + assert_ok!(MultiCollective::::do_remove_member( + CollectiveId::Alpha, + who, + )); + + assert!(MultiCollective::::members_of(CollectiveId::Alpha).is_empty()); + assert_eq!( + take_members_changed_log(), + vec![MembersChangedCall { + collective_id: CollectiveId::Alpha, + incoming: vec![], + outgoing: vec![who], + }] + ); + assert_eq!( + multi_collective_events().last().expect("event emitted"), + &CollectiveEvent::MemberRemoved { + collective_id: CollectiveId::Alpha, + who, + }, + ); + }); +} + +#[test] +fn do_remove_member_errors_on_non_member() { + TestState::build_and_execute(|| { + assert!(matches!( + MultiCollective::::do_remove_member(CollectiveId::Alpha, U256::from(7)), + Err(Error::::NotMember), + )); + }); +} + +#[test] +fn do_remove_member_respects_min_members_floor() { + TestState::build_and_execute(|| { + let a = U256::from(1); + let b = U256::from(2); + assert_ok!(MultiCollective::::set_members( + RuntimeOrigin::root(), + CollectiveId::Beta, + vec![a, b], + )); + + // Beta has min_members = 2; dropping below the floor must error. + assert!(matches!( + MultiCollective::::do_remove_member(CollectiveId::Beta, a), + Err(Error::::TooFewMembers), + )); + }); +} + +#[test] +fn do_remove_member_errors_on_unknown_collective() { + TestState::build_and_execute(|| { + assert!(matches!( + MultiCollective::::do_remove_member(CollectiveId::Unknown, U256::from(1)), + Err(Error::::CollectiveNotFound), + )); + }); +} diff --git a/pallets/multi-collective/src/weights.rs b/pallets/multi-collective/src/weights.rs new file mode 100644 index 0000000000..325cb3954c --- /dev/null +++ b/pallets/multi-collective/src/weights.rs @@ -0,0 +1,207 @@ + +//! Autogenerated weights for `pallet_multi_collective` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 49.1.0 +//! DATE: 2026-05-25, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `runnervmg397c`, CPU: `AMD EPYC 7763 64-Core Processor` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` + +// Executed Command: +// /home/runner/work/subtensor/subtensor/target/production/node-subtensor +// benchmark +// pallet +// --runtime=/home/runner/work/subtensor/subtensor/target/production/wbuild/node-subtensor-runtime/node_subtensor_runtime.compact.compressed.wasm +// --genesis-builder=runtime +// --genesis-builder-preset=benchmark +// --wasm-execution=compiled +// --pallet=pallet_multi_collective +// --extrinsic=* +// --steps=50 +// --repeat=20 +// --no-storage-info +// --no-min-squares +// --no-median-slopes +// --output=/tmp/tmp.8vKpHuHTSt +// --template=/home/runner/work/subtensor/subtensor/.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] +#![allow(dead_code)] + +use frame_support::{traits::Get, weights::{Weight, constants::ParityDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_multi_collective`. +pub trait WeightInfo { + fn add_member() -> Weight; + fn remove_member() -> Weight; + fn swap_member() -> Weight; + fn set_members() -> Weight; + fn force_rotate() -> Weight; + fn do_add_member() -> Weight; + fn do_remove_member() -> Weight; +} + +/// Weights for `pallet_multi_collective` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `MultiCollective::Members` (r:1 w:1) + /// Proof: `MultiCollective::Members` (`max_values`: None, `max_size`: Some(2067), added: 4542, mode: `MaxEncodedLen`) + fn add_member() -> Weight { + // Proof Size summary in bytes: + // Measured: `2104` + // Estimated: `5532` + // Minimum execution time: 13_816_000 picoseconds. + Weight::from_parts(14_247_000, 5532) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `MultiCollective::Members` (r:1 w:1) + /// Proof: `MultiCollective::Members` (`max_values`: None, `max_size`: Some(2067), added: 4542, mode: `MaxEncodedLen`) + fn remove_member() -> Weight { + // Proof Size summary in bytes: + // Measured: `2137` + // Estimated: `5532` + // Minimum execution time: 13_575_000 picoseconds. + Weight::from_parts(13_976_000, 5532) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `MultiCollective::Members` (r:1 w:1) + /// Proof: `MultiCollective::Members` (`max_values`: None, `max_size`: Some(2067), added: 4542, mode: `MaxEncodedLen`) + fn swap_member() -> Weight { + // Proof Size summary in bytes: + // Measured: `2137` + // Estimated: `5532` + // Minimum execution time: 13_796_000 picoseconds. + Weight::from_parts(14_497_000, 5532) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `MultiCollective::Members` (r:1 w:1) + /// Proof: `MultiCollective::Members` (`max_values`: None, `max_size`: Some(2067), added: 4542, mode: `MaxEncodedLen`) + fn set_members() -> Weight { + // Proof Size summary in bytes: + // Measured: `2137` + // Estimated: `5532` + // Minimum execution time: 21_450_000 picoseconds. + Weight::from_parts(22_663_000, 5532) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `MultiCollective::Members` (r:1 w:0) + /// Proof: `MultiCollective::Members` (`max_values`: None, `max_size`: Some(2067), added: 4542, mode: `MaxEncodedLen`) + fn force_rotate() -> Weight { + // Proof Size summary in bytes: + // Measured: `42` + // Estimated: `5532` + // Minimum execution time: 22_021_000 picoseconds. + Weight::from_parts(22_632_000, 5532) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: `MultiCollective::Members` (r:1 w:1) + /// Proof: `MultiCollective::Members` (`max_values`: None, `max_size`: Some(2067), added: 4542, mode: `MaxEncodedLen`) + fn do_add_member() -> Weight { + // Proof Size summary in bytes: + // Measured: `2104` + // Estimated: `5532` + // Minimum execution time: 10_830_000 picoseconds. + Weight::from_parts(11_292_000, 5532) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `MultiCollective::Members` (r:1 w:1) + /// Proof: `MultiCollective::Members` (`max_values`: None, `max_size`: Some(2067), added: 4542, mode: `MaxEncodedLen`) + fn do_remove_member() -> Weight { + // Proof Size summary in bytes: + // Measured: `2137` + // Estimated: `5532` + // Minimum execution time: 10_590_000 picoseconds. + Weight::from_parts(11_071_000, 5532) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `MultiCollective::Members` (r:1 w:1) + /// Proof: `MultiCollective::Members` (`max_values`: None, `max_size`: Some(2067), added: 4542, mode: `MaxEncodedLen`) + fn add_member() -> Weight { + // Proof Size summary in bytes: + // Measured: `2104` + // Estimated: `5532` + // Minimum execution time: 13_816_000 picoseconds. + Weight::from_parts(14_247_000, 5532) + .saturating_add(ParityDbWeight::get().reads(1_u64)) + .saturating_add(ParityDbWeight::get().writes(1_u64)) + } + /// Storage: `MultiCollective::Members` (r:1 w:1) + /// Proof: `MultiCollective::Members` (`max_values`: None, `max_size`: Some(2067), added: 4542, mode: `MaxEncodedLen`) + fn remove_member() -> Weight { + // Proof Size summary in bytes: + // Measured: `2137` + // Estimated: `5532` + // Minimum execution time: 13_575_000 picoseconds. + Weight::from_parts(13_976_000, 5532) + .saturating_add(ParityDbWeight::get().reads(1_u64)) + .saturating_add(ParityDbWeight::get().writes(1_u64)) + } + /// Storage: `MultiCollective::Members` (r:1 w:1) + /// Proof: `MultiCollective::Members` (`max_values`: None, `max_size`: Some(2067), added: 4542, mode: `MaxEncodedLen`) + fn swap_member() -> Weight { + // Proof Size summary in bytes: + // Measured: `2137` + // Estimated: `5532` + // Minimum execution time: 13_796_000 picoseconds. + Weight::from_parts(14_497_000, 5532) + .saturating_add(ParityDbWeight::get().reads(1_u64)) + .saturating_add(ParityDbWeight::get().writes(1_u64)) + } + /// Storage: `MultiCollective::Members` (r:1 w:1) + /// Proof: `MultiCollective::Members` (`max_values`: None, `max_size`: Some(2067), added: 4542, mode: `MaxEncodedLen`) + fn set_members() -> Weight { + // Proof Size summary in bytes: + // Measured: `2137` + // Estimated: `5532` + // Minimum execution time: 21_450_000 picoseconds. + Weight::from_parts(22_663_000, 5532) + .saturating_add(ParityDbWeight::get().reads(1_u64)) + .saturating_add(ParityDbWeight::get().writes(1_u64)) + } + /// Storage: `MultiCollective::Members` (r:1 w:0) + /// Proof: `MultiCollective::Members` (`max_values`: None, `max_size`: Some(2067), added: 4542, mode: `MaxEncodedLen`) + fn force_rotate() -> Weight { + // Proof Size summary in bytes: + // Measured: `42` + // Estimated: `5532` + // Minimum execution time: 22_021_000 picoseconds. + Weight::from_parts(22_632_000, 5532) + .saturating_add(ParityDbWeight::get().reads(1_u64)) + } + /// Storage: `MultiCollective::Members` (r:1 w:1) + /// Proof: `MultiCollective::Members` (`max_values`: None, `max_size`: Some(2067), added: 4542, mode: `MaxEncodedLen`) + fn do_add_member() -> Weight { + // Proof Size summary in bytes: + // Measured: `2104` + // Estimated: `5532` + // Minimum execution time: 10_830_000 picoseconds. + Weight::from_parts(11_292_000, 5532) + .saturating_add(ParityDbWeight::get().reads(1_u64)) + .saturating_add(ParityDbWeight::get().writes(1_u64)) + } + /// Storage: `MultiCollective::Members` (r:1 w:1) + /// Proof: `MultiCollective::Members` (`max_values`: None, `max_size`: Some(2067), added: 4542, mode: `MaxEncodedLen`) + fn do_remove_member() -> Weight { + // Proof Size summary in bytes: + // Measured: `2137` + // Estimated: `5532` + // Minimum execution time: 10_590_000 picoseconds. + Weight::from_parts(11_071_000, 5532) + .saturating_add(ParityDbWeight::get().reads(1_u64)) + .saturating_add(ParityDbWeight::get().writes(1_u64)) + } +} diff --git a/pallets/proxy/src/weights.rs b/pallets/proxy/src/weights.rs index 38ae8c69ac..3a1f7a3775 100644 --- a/pallets/proxy/src/weights.rs +++ b/pallets/proxy/src/weights.rs @@ -2,9 +2,9 @@ //! Autogenerated weights for `pallet_subtensor_proxy` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 49.1.0 -//! DATE: 2026-05-31, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-06-15, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `runnervm3jyl0`, CPU: `AMD EPYC 9V74 80-Core Processor` +//! HOSTNAME: `runnervm1li68`, CPU: `AMD EPYC 9V74 80-Core Processor` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` // Executed Command: @@ -22,7 +22,7 @@ // --no-storage-info // --no-min-squares // --no-median-slopes -// --output=/tmp/tmp.dW1NaIslV8 +// --output=/tmp/tmp.p1bMVWhQG1 // --template=/home/runner/work/subtensor/subtensor/.maintain/frame-weight-template.hbs #![cfg_attr(rustfmt, rustfmt_skip)] @@ -66,10 +66,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `637 + p * (37 ±0)` // Estimated: `4254 + p * (37 ±0)` - // Minimum execution time: 23_374_000 picoseconds. - Weight::from_parts(24_151_161, 4254) - // Standard Error: 3_337 - .saturating_add(Weight::from_parts(96_756, 0).saturating_mul(p.into())) + // Minimum execution time: 23_305_000 picoseconds. + Weight::from_parts(24_344_176, 4254) + // Standard Error: 2_859 + .saturating_add(Weight::from_parts(61_607, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) .saturating_add(Weight::from_parts(0, 37).saturating_mul(p.into())) @@ -92,12 +92,12 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `894 + a * (68 ±0) + p * (37 ±0)` // Estimated: `8615 + a * (68 ±0) + p * (37 ±0)` - // Minimum execution time: 47_420_000 picoseconds. - Weight::from_parts(48_607_796, 8615) - // Standard Error: 1_429 - .saturating_add(Weight::from_parts(261_812, 0).saturating_mul(a.into())) - // Standard Error: 5_727 - .saturating_add(Weight::from_parts(54_276, 0).saturating_mul(p.into())) + // Minimum execution time: 48_211_000 picoseconds. + Weight::from_parts(49_327_900, 8615) + // Standard Error: 1_615 + .saturating_add(Weight::from_parts(229_917, 0).saturating_mul(a.into())) + // Standard Error: 6_471 + .saturating_add(Weight::from_parts(52_466, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) .saturating_add(Weight::from_parts(0, 68).saturating_mul(a.into())) @@ -109,16 +109,14 @@ impl WeightInfo for SubstrateWeight { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) /// The range of component `a` is `[0, 74]`. /// The range of component `p` is `[1, 19]`. - fn remove_announcement(a: u32, p: u32, ) -> Weight { + fn remove_announcement(a: u32, _p: u32, ) -> Weight { // Proof Size summary in bytes: // Measured: `299 + a * (68 ±0)` // Estimated: `8615` - // Minimum execution time: 23_295_000 picoseconds. - Weight::from_parts(24_193_917, 8615) - // Standard Error: 886 - .saturating_add(Weight::from_parts(211_445, 0).saturating_mul(a.into())) - // Standard Error: 3_552 - .saturating_add(Weight::from_parts(20_637, 0).saturating_mul(p.into())) + // Minimum execution time: 23_335_000 picoseconds. + Weight::from_parts(24_441_792, 8615) + // Standard Error: 1_160 + .saturating_add(Weight::from_parts(199_923, 0).saturating_mul(a.into())) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -132,12 +130,12 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `299 + a * (68 ±0)` // Estimated: `8615` - // Minimum execution time: 23_334_000 picoseconds. - Weight::from_parts(24_226_028, 8615) - // Standard Error: 891 - .saturating_add(Weight::from_parts(211_311, 0).saturating_mul(a.into())) - // Standard Error: 3_572 - .saturating_add(Weight::from_parts(19_850, 0).saturating_mul(p.into())) + // Minimum execution time: 23_325_000 picoseconds. + Weight::from_parts(24_119_725, 8615) + // Standard Error: 1_004 + .saturating_add(Weight::from_parts(199_684, 0).saturating_mul(a.into())) + // Standard Error: 4_022 + .saturating_add(Weight::from_parts(14_188, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -153,12 +151,12 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `308 + a * (68 ±0) + p * (37 ±0)` // Estimated: `8615` - // Minimum execution time: 30_775_000 picoseconds. - Weight::from_parts(29_900_605, 8615) - // Standard Error: 2_656 - .saturating_add(Weight::from_parts(245_681, 0).saturating_mul(a.into())) - // Standard Error: 10_638 - .saturating_add(Weight::from_parts(108_442, 0).saturating_mul(p.into())) + // Minimum execution time: 30_786_000 picoseconds. + Weight::from_parts(31_264_086, 8615) + // Standard Error: 1_063 + .saturating_add(Weight::from_parts(201_071, 0).saturating_mul(a.into())) + // Standard Error: 4_257 + .saturating_add(Weight::from_parts(48_234, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -169,10 +167,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `119 + p * (37 ±0)` // Estimated: `4254` - // Minimum execution time: 22_393_000 picoseconds. - Weight::from_parts(23_099_598, 4254) - // Standard Error: 1_922 - .saturating_add(Weight::from_parts(76_038, 0).saturating_mul(p.into())) + // Minimum execution time: 22_033_000 picoseconds. + Weight::from_parts(23_091_198, 4254) + // Standard Error: 1_876 + .saturating_add(Weight::from_parts(71_914, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -185,10 +183,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `119 + p * (37 ±0)` // Estimated: `4254` - // Minimum execution time: 23_635_000 picoseconds. - Weight::from_parts(24_761_635, 4254) - // Standard Error: 2_310 - .saturating_add(Weight::from_parts(58_413, 0).saturating_mul(p.into())) + // Minimum execution time: 23_985_000 picoseconds. + Weight::from_parts(25_137_108, 4254) + // Standard Error: 2_461 + .saturating_add(Weight::from_parts(63_781, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -199,10 +197,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `119 + p * (37 ±0)` // Estimated: `4254` - // Minimum execution time: 23_715_000 picoseconds. - Weight::from_parts(24_632_652, 4254) - // Standard Error: 2_253 - .saturating_add(Weight::from_parts(48_858, 0).saturating_mul(p.into())) + // Minimum execution time: 24_056_000 picoseconds. + Weight::from_parts(25_056_188, 4254) + // Standard Error: 2_438 + .saturating_add(Weight::from_parts(41_155, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -213,10 +211,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `139` // Estimated: `4254` - // Minimum execution time: 23_865_000 picoseconds. - Weight::from_parts(24_892_331, 4254) - // Standard Error: 2_007 - .saturating_add(Weight::from_parts(21_649, 0).saturating_mul(p.into())) + // Minimum execution time: 24_766_000 picoseconds. + Weight::from_parts(25_774_219, 4254) + // Standard Error: 2_177 + .saturating_add(Weight::from_parts(29_107, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -227,10 +225,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `156 + p * (37 ±0)` // Estimated: `4254` - // Minimum execution time: 22_854_000 picoseconds. - Weight::from_parts(23_802_763, 4254) - // Standard Error: 2_166 - .saturating_add(Weight::from_parts(41_019, 0).saturating_mul(p.into())) + // Minimum execution time: 23_315_000 picoseconds. + Weight::from_parts(24_401_670, 4254) + // Standard Error: 1_977 + .saturating_add(Weight::from_parts(40_473, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -244,8 +242,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `412` // Estimated: `8615` - // Minimum execution time: 42_413_000 picoseconds. - Weight::from_parts(43_264_000, 8615) + // Minimum execution time: 42_824_000 picoseconds. + Weight::from_parts(43_955_000, 8615) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -258,10 +256,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `119 + p * (37 ±0)` // Estimated: `4254` - // Minimum execution time: 11_487_000 picoseconds. - Weight::from_parts(12_050_045, 4254) - // Standard Error: 1_620 - .saturating_add(Weight::from_parts(45_828, 0).saturating_mul(p.into())) + // Minimum execution time: 11_817_000 picoseconds. + Weight::from_parts(12_428_329, 4254) + // Standard Error: 1_464 + .saturating_add(Weight::from_parts(38_133, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -282,10 +280,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `637 + p * (37 ±0)` // Estimated: `4254 + p * (37 ±0)` - // Minimum execution time: 23_374_000 picoseconds. - Weight::from_parts(24_151_161, 4254) - // Standard Error: 3_337 - .saturating_add(Weight::from_parts(96_756, 0).saturating_mul(p.into())) + // Minimum execution time: 23_305_000 picoseconds. + Weight::from_parts(24_344_176, 4254) + // Standard Error: 2_859 + .saturating_add(Weight::from_parts(61_607, 0).saturating_mul(p.into())) .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) .saturating_add(Weight::from_parts(0, 37).saturating_mul(p.into())) @@ -308,12 +306,12 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `894 + a * (68 ±0) + p * (37 ±0)` // Estimated: `8615 + a * (68 ±0) + p * (37 ±0)` - // Minimum execution time: 47_420_000 picoseconds. - Weight::from_parts(48_607_796, 8615) - // Standard Error: 1_429 - .saturating_add(Weight::from_parts(261_812, 0).saturating_mul(a.into())) - // Standard Error: 5_727 - .saturating_add(Weight::from_parts(54_276, 0).saturating_mul(p.into())) + // Minimum execution time: 48_211_000 picoseconds. + Weight::from_parts(49_327_900, 8615) + // Standard Error: 1_615 + .saturating_add(Weight::from_parts(229_917, 0).saturating_mul(a.into())) + // Standard Error: 6_471 + .saturating_add(Weight::from_parts(52_466, 0).saturating_mul(p.into())) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) .saturating_add(Weight::from_parts(0, 68).saturating_mul(a.into())) @@ -325,16 +323,14 @@ impl WeightInfo for () { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) /// The range of component `a` is `[0, 74]`. /// The range of component `p` is `[1, 19]`. - fn remove_announcement(a: u32, p: u32, ) -> Weight { + fn remove_announcement(a: u32, _p: u32, ) -> Weight { // Proof Size summary in bytes: // Measured: `299 + a * (68 ±0)` // Estimated: `8615` - // Minimum execution time: 23_295_000 picoseconds. - Weight::from_parts(24_193_917, 8615) - // Standard Error: 886 - .saturating_add(Weight::from_parts(211_445, 0).saturating_mul(a.into())) - // Standard Error: 3_552 - .saturating_add(Weight::from_parts(20_637, 0).saturating_mul(p.into())) + // Minimum execution time: 23_335_000 picoseconds. + Weight::from_parts(24_441_792, 8615) + // Standard Error: 1_160 + .saturating_add(Weight::from_parts(199_923, 0).saturating_mul(a.into())) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -348,12 +344,12 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `299 + a * (68 ±0)` // Estimated: `8615` - // Minimum execution time: 23_334_000 picoseconds. - Weight::from_parts(24_226_028, 8615) - // Standard Error: 891 - .saturating_add(Weight::from_parts(211_311, 0).saturating_mul(a.into())) - // Standard Error: 3_572 - .saturating_add(Weight::from_parts(19_850, 0).saturating_mul(p.into())) + // Minimum execution time: 23_325_000 picoseconds. + Weight::from_parts(24_119_725, 8615) + // Standard Error: 1_004 + .saturating_add(Weight::from_parts(199_684, 0).saturating_mul(a.into())) + // Standard Error: 4_022 + .saturating_add(Weight::from_parts(14_188, 0).saturating_mul(p.into())) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -369,12 +365,12 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `308 + a * (68 ±0) + p * (37 ±0)` // Estimated: `8615` - // Minimum execution time: 30_775_000 picoseconds. - Weight::from_parts(29_900_605, 8615) - // Standard Error: 2_656 - .saturating_add(Weight::from_parts(245_681, 0).saturating_mul(a.into())) - // Standard Error: 10_638 - .saturating_add(Weight::from_parts(108_442, 0).saturating_mul(p.into())) + // Minimum execution time: 30_786_000 picoseconds. + Weight::from_parts(31_264_086, 8615) + // Standard Error: 1_063 + .saturating_add(Weight::from_parts(201_071, 0).saturating_mul(a.into())) + // Standard Error: 4_257 + .saturating_add(Weight::from_parts(48_234, 0).saturating_mul(p.into())) .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -385,10 +381,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `119 + p * (37 ±0)` // Estimated: `4254` - // Minimum execution time: 22_393_000 picoseconds. - Weight::from_parts(23_099_598, 4254) - // Standard Error: 1_922 - .saturating_add(Weight::from_parts(76_038, 0).saturating_mul(p.into())) + // Minimum execution time: 22_033_000 picoseconds. + Weight::from_parts(23_091_198, 4254) + // Standard Error: 1_876 + .saturating_add(Weight::from_parts(71_914, 0).saturating_mul(p.into())) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -401,10 +397,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `119 + p * (37 ±0)` // Estimated: `4254` - // Minimum execution time: 23_635_000 picoseconds. - Weight::from_parts(24_761_635, 4254) - // Standard Error: 2_310 - .saturating_add(Weight::from_parts(58_413, 0).saturating_mul(p.into())) + // Minimum execution time: 23_985_000 picoseconds. + Weight::from_parts(25_137_108, 4254) + // Standard Error: 2_461 + .saturating_add(Weight::from_parts(63_781, 0).saturating_mul(p.into())) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -415,10 +411,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `119 + p * (37 ±0)` // Estimated: `4254` - // Minimum execution time: 23_715_000 picoseconds. - Weight::from_parts(24_632_652, 4254) - // Standard Error: 2_253 - .saturating_add(Weight::from_parts(48_858, 0).saturating_mul(p.into())) + // Minimum execution time: 24_056_000 picoseconds. + Weight::from_parts(25_056_188, 4254) + // Standard Error: 2_438 + .saturating_add(Weight::from_parts(41_155, 0).saturating_mul(p.into())) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -429,10 +425,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `139` // Estimated: `4254` - // Minimum execution time: 23_865_000 picoseconds. - Weight::from_parts(24_892_331, 4254) - // Standard Error: 2_007 - .saturating_add(Weight::from_parts(21_649, 0).saturating_mul(p.into())) + // Minimum execution time: 24_766_000 picoseconds. + Weight::from_parts(25_774_219, 4254) + // Standard Error: 2_177 + .saturating_add(Weight::from_parts(29_107, 0).saturating_mul(p.into())) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -443,10 +439,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `156 + p * (37 ±0)` // Estimated: `4254` - // Minimum execution time: 22_854_000 picoseconds. - Weight::from_parts(23_802_763, 4254) - // Standard Error: 2_166 - .saturating_add(Weight::from_parts(41_019, 0).saturating_mul(p.into())) + // Minimum execution time: 23_315_000 picoseconds. + Weight::from_parts(24_401_670, 4254) + // Standard Error: 1_977 + .saturating_add(Weight::from_parts(40_473, 0).saturating_mul(p.into())) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -460,8 +456,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `412` // Estimated: `8615` - // Minimum execution time: 42_413_000 picoseconds. - Weight::from_parts(43_264_000, 8615) + // Minimum execution time: 42_824_000 picoseconds. + Weight::from_parts(43_955_000, 8615) .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -474,10 +470,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `119 + p * (37 ±0)` // Estimated: `4254` - // Minimum execution time: 11_487_000 picoseconds. - Weight::from_parts(12_050_045, 4254) - // Standard Error: 1_620 - .saturating_add(Weight::from_parts(45_828, 0).saturating_mul(p.into())) + // Minimum execution time: 11_817_000 picoseconds. + Weight::from_parts(12_428_329, 4254) + // Standard Error: 1_464 + .saturating_add(Weight::from_parts(38_133, 0).saturating_mul(p.into())) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } diff --git a/pallets/registry/Cargo.toml b/pallets/registry/Cargo.toml deleted file mode 100644 index 08d774884a..0000000000 --- a/pallets/registry/Cargo.toml +++ /dev/null @@ -1,62 +0,0 @@ -[package] -name = "pallet-registry" -version = "4.0.0-dev" -description = "Simplified identity system for network participants." -authors = ["Bittensor Nucleus Team"] -homepage = "https://bittensor.com" -edition.workspace = true -license = "Unlicense" -publish = false -repository = "https://github.com/opentensor/subtensor" - -[lints] -workspace = true - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] - -[dependencies] -subtensor-macros.workspace = true -codec = { workspace = true, features = ["derive", "max-encoded-len"] } -scale-info = { workspace = true, features = ["derive"] } -frame-benchmarking = { workspace = true, optional = true } -frame-support.workspace = true -frame-system.workspace = true -sp-runtime.workspace = true -sp-std.workspace = true -enumflags2.workspace = true -sp-core.workspace = true -sp-io.workspace = true -pallet-balances.workspace = true -subtensor-runtime-common.workspace = true - -[features] -default = ["std"] -std = [ - "codec/std", - "frame-benchmarking?/std", - "frame-support/std", - "frame-system/std", - "scale-info/std", - "sp-std/std", - "sp-runtime/std", - "enumflags2/std", - "sp-io/std", - "pallet-balances/std", - "subtensor-runtime-common/std", - "sp-core/std", -] -runtime-benchmarks = [ - "frame-benchmarking/runtime-benchmarks", - "frame-support/runtime-benchmarks", - "frame-system/runtime-benchmarks", - "sp-runtime/runtime-benchmarks", - "pallet-balances/runtime-benchmarks", - "subtensor-runtime-common/runtime-benchmarks", -] -try-runtime = [ - "frame-support/try-runtime", - "frame-system/try-runtime", - "sp-runtime/try-runtime", - "pallet-balances/try-runtime", -] diff --git a/pallets/registry/src/benchmarking.rs b/pallets/registry/src/benchmarking.rs deleted file mode 100644 index 244ffe2599..0000000000 --- a/pallets/registry/src/benchmarking.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! Benchmarking setup -#![cfg(feature = "runtime-benchmarks")] -#![allow( - clippy::arithmetic_side_effects, - clippy::expect_used, - clippy::unwrap_used -)] -use super::*; - -#[allow(unused)] -use crate::Pallet as Registry; -use frame_benchmarking::v2::*; -use frame_support::traits::{Get, tokens::fungible::Mutate}; -use frame_system::RawOrigin; -use sp_std::vec; - -fn assert_last_event( - generic_event: ::RuntimeEvent, -) { - frame_system::Pallet::::assert_last_event(generic_event.into()); -} - -// This creates an `IdentityInfo` object with `num_fields` extra fields. -// All data is pre-populated with some arbitrary bytes. -fn create_identity_info(_num_fields: u32) -> IdentityInfo { - let data = Data::Raw( - vec![0; 32] - .try_into() - .expect("size does not exceed 64; qed"), - ); - - IdentityInfo { - additional: Default::default(), - display: data.clone(), - legal: data.clone(), - web: data.clone(), - riot: data.clone(), - email: data.clone(), - pgp_fingerprint: Some([0; 20]), - image: data.clone(), - twitter: data, - } -} - -#[benchmarks(where BalanceOf: From)] -mod benchmarks { - use super::*; - - #[benchmark] - fn set_identity() { - // The target user - let caller: T::AccountId = whitelisted_caller(); - let deposit = T::InitialDeposit::get() * 10u64.into(); - let _ = T::Currency::set_balance(&caller, deposit); - - #[extrinsic_call] - _( - RawOrigin::Signed(caller.clone()), - caller.clone(), - Box::new(create_identity_info::(0)), - ); - - assert_last_event::(Event::::IdentitySet { who: caller }.into()); - } - - #[benchmark] - fn clear_identity() { - // The target user - let caller: T::AccountId = whitelisted_caller(); - let _ = T::Currency::set_balance(&caller, T::InitialDeposit::get() * 10u64.into()); - - Registry::::set_identity( - RawOrigin::Signed(caller.clone()).into(), - caller.clone(), - Box::new(create_identity_info::(0)), - ) - .unwrap(); - - #[extrinsic_call] - _(RawOrigin::Signed(caller.clone()), caller.clone()); - - assert_last_event::(Event::::IdentityDissolved { who: caller }.into()); - } - - impl_benchmark_test_suite!(Registry, crate::mock::new_test_ext(), crate::mock::Test); -} diff --git a/pallets/registry/src/lib.rs b/pallets/registry/src/lib.rs deleted file mode 100644 index f3b76bc529..0000000000 --- a/pallets/registry/src/lib.rs +++ /dev/null @@ -1,213 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std)] - -#[cfg(test)] -pub mod mock; -#[cfg(test)] -mod tests; - -mod benchmarking; -pub mod types; -pub mod weights; - -pub use pallet::*; -pub use types::*; -pub use weights::WeightInfo; - -use frame_support::traits::tokens::{ - Precision, - fungible::{self, MutateHold as _}, -}; -use sp_runtime::{Saturating, traits::Zero}; -use sp_std::boxed::Box; - -type BalanceOf = - <::Currency as fungible::Inspect<::AccountId>>::Balance; - -#[deny(missing_docs)] -#[frame_support::pallet] -#[allow(clippy::expect_used)] -pub mod pallet { - use super::*; - use frame_support::{pallet_prelude::*, traits::tokens::fungible}; - use frame_system::pallet_prelude::*; - - #[pallet::pallet] - #[pallet::without_storage_info] - pub struct Pallet(_); - - // Configure the pallet by specifying the parameters and types on which it depends. - #[pallet::config] - pub trait Config: frame_system::Config { - /// Currency type that will be used to place deposits on neurons - #[allow(deprecated)] - type Currency: fungible::Mutate - + fungible::MutateHold; - - /// Weight information for extrinsics in this pallet. - type WeightInfo: WeightInfo; - - /// Interface to allow other pallets to control who can register identities - type CanRegister: crate::CanRegisterIdentity; - - /// Configuration fields - /// Maximum user-configured additional fields - #[pallet::constant] - type MaxAdditionalFields: Get; - - /// The amount held on deposit for a registered identity - #[pallet::constant] - type InitialDeposit: Get>; - - /// The amount held on deposit per additional field for a registered identity. - #[pallet::constant] - type FieldDeposit: Get>; - - /// Reasons for putting funds on hold. - type RuntimeHoldReason: From; - } - - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - /// Emitted when a user registers an identity - IdentitySet { - /// The account that registered the identity - who: T::AccountId, - }, - /// Emitted when a user dissolves an identity - IdentityDissolved { - /// The account that dissolved the identity - who: T::AccountId, - }, - } - - #[pallet::error] - pub enum Error { - /// Account attempted to register an identity but does not meet the requirements. - CannotRegister, - /// Account passed too many additional fields to their identity - TooManyFieldsInIdentityInfo, - /// Account doesn't have a registered identity - NotRegistered, - } - - /// Enum to hold reasons for putting funds on hold. - #[pallet::composite_enum] - pub enum HoldReason { - /// Funds are held for identity registration - RegistryIdentity, - } - - /// Identity data by account - #[pallet::storage] - #[pallet::getter(fn identity_of)] - pub(super) type IdentityOf = StorageMap< - _, - Twox64Concat, - T::AccountId, - Registration, T::MaxAdditionalFields>, - OptionQuery, - >; - - #[pallet::call] - impl Pallet { - #![deny(clippy::expect_used)] - - /// Register an identity for an account. This will overwrite any existing identity. - #[pallet::call_index(0)] - #[pallet::weight(( - T::WeightInfo::set_identity(), - DispatchClass::Normal - ))] - pub fn set_identity( - origin: OriginFor, - identified: T::AccountId, - info: Box>, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - ensure!( - T::CanRegister::can_register(&who, &identified), - Error::::CannotRegister - ); - - let extra_fields = info.additional.len() as u32; - ensure!( - extra_fields <= T::MaxAdditionalFields::get(), - Error::::TooManyFieldsInIdentityInfo - ); - - let fd = >::from(extra_fields).saturating_mul(T::FieldDeposit::get()); - let mut id = match >::get(&identified) { - Some(mut id) => { - id.info = *info; - id - } - None => Registration { - info: *info, - deposit: Zero::zero(), - }, - }; - - let old_deposit = id.deposit; - id.deposit = T::InitialDeposit::get().saturating_add(fd); - if id.deposit > old_deposit { - T::Currency::hold( - &HoldReason::RegistryIdentity.into(), - &who, - id.deposit.saturating_sub(old_deposit), - )?; - } - if old_deposit > id.deposit { - let release_res = T::Currency::release( - &HoldReason::RegistryIdentity.into(), - &who, - old_deposit.saturating_sub(id.deposit), - Precision::BestEffort, - ); - debug_assert!(release_res.is_ok_and( - |released_amount| released_amount == old_deposit.saturating_sub(id.deposit) - )); - } - - >::insert(&identified, id); - Self::deposit_event(Event::IdentitySet { who: identified }); - - Ok(()) - } - - /// Clear the identity of an account. - #[pallet::call_index(1)] - #[pallet::weight(T::WeightInfo::clear_identity())] - pub fn clear_identity( - origin: OriginFor, - identified: T::AccountId, - ) -> DispatchResultWithPostInfo { - let who = ensure_signed(origin)?; - - let id = >::take(&identified).ok_or(Error::::NotRegistered)?; - let deposit = id.total_deposit(); - - let release_res = T::Currency::release( - &HoldReason::RegistryIdentity.into(), - &who, - deposit, - Precision::BestEffort, - ); - debug_assert!(release_res.is_ok_and(|released_amount| released_amount == deposit)); - - Self::deposit_event(Event::IdentityDissolved { who: identified }); - - Ok(().into()) - } - } -} -// Interfaces to interact with other pallets -pub trait CanRegisterIdentity { - fn can_register(who: &AccountId, identified: &AccountId) -> bool; -} - -impl CanRegisterIdentity for () { - fn can_register(_: &A, _: &A) -> bool { - false - } -} diff --git a/pallets/registry/src/mock.rs b/pallets/registry/src/mock.rs deleted file mode 100644 index 32957c40bb..0000000000 --- a/pallets/registry/src/mock.rs +++ /dev/null @@ -1,81 +0,0 @@ -#![allow(clippy::expect_used)] -use crate as pallet_registry; -use frame_support::{derive_impl, parameter_types}; -use sp_core::U256; -use sp_runtime::{BuildStorage, traits::IdentityLookup}; -use subtensor_runtime_common::TaoBalance; - -type Block = frame_system::mocking::MockBlock; - -// Configure a mock runtime to test the pallet. -frame_support::construct_runtime!( - pub enum Test - { - System: frame_system = 1, - Balances: pallet_balances = 2, - Registry: pallet_registry = 3, - } -); - -#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] -impl frame_system::Config for Test { - type Block = Block; - type AccountId = U256; - type AccountData = pallet_balances::AccountData; - type Lookup = IdentityLookup; -} - -parameter_types! { - pub const ExistentialDeposit: TaoBalance = TaoBalance::new(1); -} - -#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] -impl pallet_balances::Config for Test { - type AccountStore = System; - type Balance = TaoBalance; - type ExistentialDeposit = ExistentialDeposit; -} - -parameter_types! { - pub const MaxAdditionalFields: u32 = 16; - pub const InitialDeposit: TaoBalance = TaoBalance::new(100); - pub const FieldDeposit: TaoBalance = TaoBalance::new(10); -} - -pub struct CanRegister; -impl pallet_registry::CanRegisterIdentity for CanRegister { - fn can_register(who: &U256, identified: &U256) -> bool { - who == identified - } -} - -impl pallet_registry::Config for Test { - type Currency = Balances; - type WeightInfo = (); - type MaxAdditionalFields = MaxAdditionalFields; - type CanRegister = CanRegister; - type InitialDeposit = InitialDeposit; - type FieldDeposit = FieldDeposit; - type RuntimeHoldReason = RuntimeHoldReason; -} - -pub fn new_test_ext() -> sp_io::TestExternalities { - let mut t = frame_system::GenesisConfig::::default() - .build_storage() - .expect("system storage should build ok"); - pallet_balances::GenesisConfig:: { - balances: vec![ - (U256::from(1), 10.into()), - (U256::from(2), 10.into()), - (U256::from(3), 10.into()), - (U256::from(4), 10.into()), - (U256::from(5), 3.into()), - ], - dev_accounts: None, - } - .assimilate_storage(&mut t) - .expect("balances storage should build ok"); - let mut ext = sp_io::TestExternalities::new(t); - ext.execute_with(|| System::set_block_number(1)); - ext -} diff --git a/pallets/registry/src/tests.rs b/pallets/registry/src/tests.rs deleted file mode 100644 index d233fe0783..0000000000 --- a/pallets/registry/src/tests.rs +++ /dev/null @@ -1 +0,0 @@ -// Testing diff --git a/pallets/registry/src/types.rs b/pallets/registry/src/types.rs deleted file mode 100644 index 0e5cbe3332..0000000000 --- a/pallets/registry/src/types.rs +++ /dev/null @@ -1,483 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; -use enumflags2::{BitFlags, bitflags}; -use frame_support::{ - BoundedVec, CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound, - traits::{ConstU32, Get}, -}; -use scale_info::{ - Path, Type, TypeInfo, TypeParameter, - build::{Fields, Variants}, - meta_type, -}; -use sp_runtime::{ - RuntimeDebug, - traits::{AppendZerosInput, Zero}, -}; -use sp_std::{fmt::Debug, iter::once, ops::Add, prelude::*}; -use subtensor_macros::freeze_struct; - -/// Either underlying data blob if it is at most 32 bytes, or a hash of it. If the data is greater -/// than 32-bytes then it will be truncated when encoding. -/// -/// Can also be `None`. -#[derive(Clone, Eq, PartialEq, RuntimeDebug, DecodeWithMemTracking, MaxEncodedLen)] -pub enum Data { - /// No data here. - None, - /// The data is stored directly. - Raw(BoundedVec>), - /// Only the Blake2 hash of the data is stored. The preimage of the hash may be retrieved - /// through some hash-lookup service. - BlakeTwo256([u8; 32]), - /// Only the SHA2-256 hash of the data is stored. The preimage of the hash may be retrieved - /// through some hash-lookup service. - Sha256([u8; 32]), - /// Only the Keccak-256 hash of the data is stored. The preimage of the hash may be retrieved - /// through some hash-lookup service. - Keccak256([u8; 32]), - /// Only the SHA3-256 hash of the data is stored. The preimage of the hash may be retrieved - /// through some hash-lookup service. - ShaThree256([u8; 32]), -} - -impl Data { - pub fn is_none(&self) -> bool { - self == &Data::None - } -} - -impl Decode for Data { - fn decode(input: &mut I) -> sp_std::result::Result { - let b = input.read_byte()?; - Ok(match b { - 0 => Data::None, - n @ 1..=65 => { - let mut r: BoundedVec<_, _> = vec![0u8; (n as usize).saturating_sub(1)] - .try_into() - .map_err(|_| codec::Error::from("bounded vec length exceeds limit"))?; - input.read(&mut r[..])?; - Data::Raw(r) - } - 66 => Data::BlakeTwo256(<[u8; 32]>::decode(input)?), - 67 => Data::Sha256(<[u8; 32]>::decode(input)?), - 68 => Data::Keccak256(<[u8; 32]>::decode(input)?), - 69 => Data::ShaThree256(<[u8; 32]>::decode(input)?), - _ => return Err(codec::Error::from("invalid leading byte")), - }) - } -} - -impl Encode for Data { - fn encode(&self) -> Vec { - match self { - Data::None => vec![0u8; 1], - Data::Raw(x) => { - let l = x.len().min(64) as u8; - let mut r = vec![l.saturating_add(1)]; - r.extend_from_slice(&x[..]); - r - } - Data::BlakeTwo256(h) => once(66u8).chain(h.iter().cloned()).collect(), - Data::Sha256(h) => once(67u8).chain(h.iter().cloned()).collect(), - Data::Keccak256(h) => once(68u8).chain(h.iter().cloned()).collect(), - Data::ShaThree256(h) => once(69u8).chain(h.iter().cloned()).collect(), - } - } -} -impl codec::EncodeLike for Data {} - -/// Add a Raw variant with the given index and a fixed sized byte array -macro_rules! data_raw_variants { - ($variants:ident, $(($index:literal, $size:literal)),* ) => { - $variants - $( - .variant(concat!("Raw", stringify!($size)), |v| v - .index($index) - .fields(Fields::unnamed().field(|f| f.ty::<[u8; $size]>())) - ) - )* - } -} - -impl TypeInfo for Data { - type Identity = Self; - - fn type_info() -> Type { - let variants = Variants::new().variant("None", |v| v.index(0)); - - // create a variant for all sizes of Raw data from 0-32 - let variants = data_raw_variants!( - variants, - (1, 0), - (2, 1), - (3, 2), - (4, 3), - (5, 4), - (6, 5), - (7, 6), - (8, 7), - (9, 8), - (10, 9), - (11, 10), - (12, 11), - (13, 12), - (14, 13), - (15, 14), - (16, 15), - (17, 16), - (18, 17), - (19, 18), - (20, 19), - (21, 20), - (22, 21), - (23, 22), - (24, 23), - (25, 24), - (26, 25), - (27, 26), - (28, 27), - (29, 28), - (30, 29), - (31, 30), - (32, 31), - (33, 32), - (34, 33), - (35, 34), - (36, 35), - (37, 36), - (38, 37), - (39, 38), - (40, 39), - (41, 40), - (42, 41), - (43, 42), - (44, 43), - (45, 44), - (46, 45), - (47, 46), - (48, 47), - (49, 48), - (50, 49), - (51, 50), - (52, 51), - (53, 52), - (54, 53), - (55, 54), - (56, 55), - (57, 56), - (58, 57), - (59, 58), - (60, 59), - (61, 60), - (62, 61), - (63, 62), - (64, 63), - (65, 64) - ); - - let variants = variants - .variant("BlakeTwo256", |v| { - v.index(66) - .fields(Fields::unnamed().field(|f| f.ty::<[u8; 32]>())) - }) - .variant("Sha256", |v| { - v.index(67) - .fields(Fields::unnamed().field(|f| f.ty::<[u8; 32]>())) - }) - .variant("Keccak256", |v| { - v.index(68) - .fields(Fields::unnamed().field(|f| f.ty::<[u8; 32]>())) - }) - .variant("ShaThree256", |v| { - v.index(69) - .fields(Fields::unnamed().field(|f| f.ty::<[u8; 32]>())) - }); - - Type::builder() - .path(Path::new("Data", module_path!())) - .variant(variants) - } -} - -impl Default for Data { - fn default() -> Self { - Self::None - } -} - -/// The fields that we use to identify the owner of an account with. Each corresponds to a field -/// in the `IdentityInfo` struct. -#[bitflags] -#[repr(u64)] -#[derive(Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo)] -pub enum IdentityField { - Display = 0b0000000000000000000000000000000000000000000000000000000000000001, - Legal = 0b0000000000000000000000000000000000000000000000000000000000000010, - Web = 0b0000000000000000000000000000000000000000000000000000000000000100, - Riot = 0b0000000000000000000000000000000000000000000000000000000000001000, - Email = 0b0000000000000000000000000000000000000000000000000000000000010000, - PgpFingerprint = 0b0000000000000000000000000000000000000000000000000000000000100000, - Image = 0b0000000000000000000000000000000000000000000000000000000001000000, - Twitter = 0b0000000000000000000000000000000000000000000000000000000010000000, -} - -/// Wrapper type for `BitFlags` that implements `Codec`. -#[derive(Clone, Copy, PartialEq, Default, RuntimeDebug)] -pub struct IdentityFields(pub BitFlags); - -impl MaxEncodedLen for IdentityFields { - fn max_encoded_len() -> usize { - u64::max_encoded_len() - } -} - -impl Eq for IdentityFields {} -impl Encode for IdentityFields { - fn using_encoded R>(&self, f: F) -> R { - self.0.bits().using_encoded(f) - } -} -impl Decode for IdentityFields { - fn decode(input: &mut I) -> sp_std::result::Result { - let field = u64::decode(input)?; - Ok(Self( - >::from_bits(field).map_err(|_| "invalid value")?, - )) - } -} -impl TypeInfo for IdentityFields { - type Identity = Self; - - fn type_info() -> Type { - Type::builder() - .path(Path::new("BitFlags", module_path!())) - .type_params(vec![TypeParameter::new( - "T", - Some(meta_type::()), - )]) - .composite(Fields::unnamed().field(|f| f.ty::().type_name("IdentityField"))) - } -} - -/// Information concerning the identity of the controller of an account. -/// -/// NOTE: This should be stored at the end of the storage item to facilitate the addition of extra -/// fields in a backwards compatible way through a specialized `Decode` impl. -#[freeze_struct("4015f12f49280ee")] -#[derive( - CloneNoBound, - Encode, - Decode, - DecodeWithMemTracking, - Eq, - MaxEncodedLen, - PartialEqNoBound, - RuntimeDebugNoBound, - TypeInfo, -)] -#[codec(mel_bound())] -#[derive(frame_support::DefaultNoBound)] -#[scale_info(skip_type_params(FieldLimit))] -pub struct IdentityInfo> { - /// Additional fields of the identity that are not catered for with the struct's explicit - /// fields. - pub additional: BoundedVec<(Data, Data), FieldLimit>, - - /// A reasonable display name for the controller of the account. This should be whatever it is - /// that it is typically known as and should not be confusable with other entities, given - /// reasonable context. - /// - /// Stored as UTF-8. - pub display: Data, - - /// The full legal name in the local jurisdiction of the entity. This might be a bit - /// long-winded. - /// - /// Stored as UTF-8. - pub legal: Data, - - /// A representative website held by the controller of the account. - /// - /// NOTE: `https://` is automatically prepended. - /// - /// Stored as UTF-8. - pub web: Data, - - /// The Riot/Matrix handle held by the controller of the account. - /// - /// Stored as UTF-8. - pub riot: Data, - - /// The email address of the controller of the account. - /// - /// Stored as UTF-8. - pub email: Data, - - /// The PGP/GPG public key of the controller of the account. - pub pgp_fingerprint: Option<[u8; 20]>, - - /// A graphic image representing the controller of the account. Should be a company, - /// organization or project logo or a headshot in the case of a human. - pub image: Data, - - /// The Twitter identity. The leading `@` character may be elided. - pub twitter: Data, -} - -impl> IdentityInfo { - pub fn fields(&self) -> IdentityFields { - let mut res = >::empty(); - if !self.display.is_none() { - res.insert(IdentityField::Display); - } - if !self.legal.is_none() { - res.insert(IdentityField::Legal); - } - if !self.web.is_none() { - res.insert(IdentityField::Web); - } - if !self.riot.is_none() { - res.insert(IdentityField::Riot); - } - if !self.email.is_none() { - res.insert(IdentityField::Email); - } - if self.pgp_fingerprint.is_some() { - res.insert(IdentityField::PgpFingerprint); - } - if !self.image.is_none() { - res.insert(IdentityField::Image); - } - if !self.twitter.is_none() { - res.insert(IdentityField::Twitter); - } - IdentityFields(res) - } -} - -/// Information concerning the identity of the controller of an account. -/// -/// NOTE: This is stored separately primarily to facilitate the addition of extra fields in a -/// backwards compatible way through a specialized `Decode` impl. -#[freeze_struct("797b69e82710bb21")] -#[derive( - CloneNoBound, Encode, Eq, MaxEncodedLen, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo, -)] -#[codec(mel_bound())] -#[scale_info(skip_type_params(MaxAdditionalFields))] -pub struct Registration< - Balance: Encode + Decode + MaxEncodedLen + Copy + Clone + Debug + Eq + PartialEq, - MaxAdditionalFields: Get, -> { - /// Amount held on deposit for this information. - pub deposit: Balance, - - /// Information on the identity. - pub info: IdentityInfo, -} - -impl< - Balance: Encode + Decode + MaxEncodedLen + Copy + Clone + Debug + Eq + PartialEq + Zero + Add, - MaxAdditionalFields: Get, -> Registration -{ - pub(crate) fn total_deposit(&self) -> Balance { - self.deposit - } -} - -impl< - Balance: Encode + Decode + MaxEncodedLen + Copy + Clone + Debug + Eq + PartialEq, - MaxAdditionalFields: Get, -> Decode for Registration -{ - fn decode(input: &mut I) -> sp_std::result::Result { - let (deposit, info) = Decode::decode(&mut AppendZerosInput::new(input))?; - Ok(Self { deposit, info }) - } -} - -#[cfg(test)] -#[allow(clippy::indexing_slicing, clippy::unwrap_used)] -mod tests { - use super::*; - - #[test] - fn manual_data_type_info() { - let mut registry = scale_info::Registry::new(); - let type_id = registry.register_type(&scale_info::meta_type::()); - let registry: scale_info::PortableRegistry = registry.into(); - let type_info = registry.resolve(type_id.id).unwrap(); - - let check_type_info = |data: &Data| { - let variant_name = match data { - Data::None => "None".to_string(), - Data::BlakeTwo256(_) => "BlakeTwo256".to_string(), - Data::Sha256(_) => "Sha256".to_string(), - Data::Keccak256(_) => "Keccak256".to_string(), - Data::ShaThree256(_) => "ShaThree256".to_string(), - Data::Raw(bytes) => format!("Raw{}", bytes.len()), - }; - if let scale_info::TypeDef::Variant(variant) = &type_info.type_def { - let variant = variant - .variants - .iter() - .find(|v| v.name == variant_name) - .unwrap_or_else(|| panic!("Expected to find variant {variant_name}")); - - let field_arr_len = variant - .fields - .first() - .and_then(|f| registry.resolve(f.ty.id)) - .map(|ty| { - if let scale_info::TypeDef::Array(arr) = &ty.type_def { - arr.len - } else { - panic!("Should be an array type") - } - }) - .unwrap_or(0); - - let encoded = data.encode(); - assert_eq!(encoded[0], variant.index); - assert_eq!(encoded.len() as u32 - 1, field_arr_len); - } else { - panic!("Should be a variant type") - }; - }; - - let mut data = vec![ - Data::None, - Data::BlakeTwo256(Default::default()), - Data::Sha256(Default::default()), - Data::Keccak256(Default::default()), - Data::ShaThree256(Default::default()), - ]; - - // A Raw instance for all possible sizes of the Raw data - for n in 0..64 { - data.push(Data::Raw(vec![0u8; n as usize].try_into().unwrap())) - } - - for d in data.iter() { - check_type_info(d); - } - } -} diff --git a/pallets/registry/src/weights.rs b/pallets/registry/src/weights.rs deleted file mode 100644 index b927be26ad..0000000000 --- a/pallets/registry/src/weights.rs +++ /dev/null @@ -1,107 +0,0 @@ - -//! Autogenerated weights for `pallet_registry` -//! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 49.1.0 -//! DATE: 2026-03-23, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `runnervm46oaq`, CPU: `AMD EPYC 7763 64-Core Processor` -//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` - -// Executed Command: -// /home/runner/work/subtensor/subtensor/target/production/node-subtensor -// benchmark -// pallet -// --runtime -// /home/runner/work/subtensor/subtensor/target/production/wbuild/node-subtensor-runtime/node_subtensor_runtime.compact.compressed.wasm -// --genesis-builder=runtime -// --genesis-builder-preset=benchmark -// --wasm-execution=compiled -// --pallet -// pallet_registry -// --extrinsic -// * -// --steps -// 50 -// --repeat -// 20 -// --no-storage-info -// --no-min-squares -// --no-median-slopes -// --output=/tmp/tmp.SfIpjZbmqj -// --template=/home/runner/work/subtensor/subtensor/.maintain/frame-weight-template.hbs - -#![cfg_attr(rustfmt, rustfmt_skip)] -#![allow(unused_parens)] -#![allow(unused_imports)] -#![allow(missing_docs)] -#![allow(dead_code)] - -use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; -use core::marker::PhantomData; - -/// Weight functions needed for `pallet_registry`. -pub trait WeightInfo { - fn set_identity() -> Weight; - fn clear_identity() -> Weight; -} - -/// Weights for `pallet_registry` using the Substrate node and recommended hardware. -pub struct SubstrateWeight(PhantomData); -impl WeightInfo for SubstrateWeight { - /// Storage: `Registry::IdentityOf` (r:1 w:1) - /// Proof: `Registry::IdentityOf` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Balances::Holds` (r:1 w:1) - /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(99), added: 2574, mode: `MaxEncodedLen`) - fn set_identity() -> Weight { - // Proof Size summary in bytes: - // Measured: `6` - // Estimated: `3564` - // Minimum execution time: 50_534_000 picoseconds. - Weight::from_parts(51_626_000, 3564) - .saturating_add(T::DbWeight::get().reads(2_u64)) - .saturating_add(T::DbWeight::get().writes(2_u64)) - } - /// Storage: `Registry::IdentityOf` (r:1 w:1) - /// Proof: `Registry::IdentityOf` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Balances::Holds` (r:1 w:1) - /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(99), added: 2574, mode: `MaxEncodedLen`) - fn clear_identity() -> Weight { - // Proof Size summary in bytes: - // Measured: `382` - // Estimated: `3847` - // Minimum execution time: 42_379_000 picoseconds. - Weight::from_parts(43_501_000, 3847) - .saturating_add(T::DbWeight::get().reads(2_u64)) - .saturating_add(T::DbWeight::get().writes(2_u64)) - } -} - -// For backwards compatibility and tests. -impl WeightInfo for () { - /// Storage: `Registry::IdentityOf` (r:1 w:1) - /// Proof: `Registry::IdentityOf` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Balances::Holds` (r:1 w:1) - /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(99), added: 2574, mode: `MaxEncodedLen`) - fn set_identity() -> Weight { - // Proof Size summary in bytes: - // Measured: `6` - // Estimated: `3564` - // Minimum execution time: 50_534_000 picoseconds. - Weight::from_parts(51_626_000, 3564) - .saturating_add(RocksDbWeight::get().reads(2_u64)) - .saturating_add(RocksDbWeight::get().writes(2_u64)) - } - /// Storage: `Registry::IdentityOf` (r:1 w:1) - /// Proof: `Registry::IdentityOf` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Balances::Holds` (r:1 w:1) - /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(99), added: 2574, mode: `MaxEncodedLen`) - fn clear_identity() -> Weight { - // Proof Size summary in bytes: - // Measured: `382` - // Estimated: `3847` - // Minimum execution time: 42_379_000 picoseconds. - Weight::from_parts(43_501_000, 3847) - .saturating_add(RocksDbWeight::get().reads(2_u64)) - .saturating_add(RocksDbWeight::get().writes(2_u64)) - } -} diff --git a/pallets/subtensor/Cargo.toml b/pallets/subtensor/Cargo.toml index 99ba71629f..27f86564d7 100644 --- a/pallets/subtensor/Cargo.toml +++ b/pallets/subtensor/Cargo.toml @@ -160,6 +160,9 @@ runtime-benchmarks = [ "pallet-subtensor-utility/runtime-benchmarks", "pallet-shield/runtime-benchmarks", "subtensor-runtime-common/runtime-benchmarks", + "subtensor-swap-interface/runtime-benchmarks", + "libsecp256k1/hmac", + "libsecp256k1/static-context", ] pow-faucet = [] fast-runtime = ["subtensor-runtime-common/fast-runtime"] diff --git a/pallets/subtensor/runtime-api/src/lib.rs b/pallets/subtensor/runtime-api/src/lib.rs index ced3956b53..2b95d59dc3 100644 --- a/pallets/subtensor/runtime-api/src/lib.rs +++ b/pallets/subtensor/runtime-api/src/lib.rs @@ -1,5 +1,6 @@ #![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; +use alloc::collections::BTreeMap; use alloc::vec::Vec; use codec::Compact; use pallet_subtensor::rpc_info::{ @@ -8,7 +9,7 @@ use pallet_subtensor::rpc_info::{ metagraph::{Metagraph, SelectiveMetagraph}, neuron_info::{NeuronInfo, NeuronInfoLite}, show_subnet::SubnetState, - stake_info::StakeInfo, + stake_info::{StakeAvailability, StakeInfo}, subnet_info::{ SubnetHyperparams, SubnetHyperparamsV2, SubnetHyperparamsV3, SubnetInfo, SubnetInfov2, }, @@ -59,12 +60,14 @@ sp_api::decl_runtime_apis! { fn get_selective_mechagraph(netuid: NetUid, subid: MechId, metagraph_indexes: Vec) -> Option>; fn get_subnet_to_prune() -> Option; fn get_subnet_account_id(netuid: NetUid) -> Option; + fn get_next_epoch_start_block(netuid: NetUid) -> Option; } pub trait StakeInfoRuntimeApi { fn get_stake_info_for_coldkey( coldkey_account: AccountId32 ) -> Vec>; fn get_stake_info_for_coldkeys( coldkey_accounts: Vec ) -> Vec<(AccountId32, Vec>)>; fn get_stake_info_for_hotkey_coldkey_netuid( hotkey_account: AccountId32, coldkey_account: AccountId32, netuid: NetUid ) -> Option>; + fn get_stake_availability_for_coldkeys( coldkey_accounts: Vec, netuids: Option> ) -> BTreeMap>; fn get_stake_fee( origin: Option<(AccountId32, NetUid)>, origin_coldkey_account: AccountId32, destination: Option<(AccountId32, NetUid)>, destination_coldkey_account: AccountId32, amount: u64 ) -> u64; fn get_coldkey_lock(coldkey: AccountId32, netuid: NetUid) -> Option; fn get_hotkey_conviction(hotkey: AccountId32, netuid: NetUid) -> U64F64; diff --git a/pallets/subtensor/src/benchmarks.rs b/pallets/subtensor/src/benchmarks.rs index 1dd62bab0b..37db141bc8 100644 --- a/pallets/subtensor/src/benchmarks.rs +++ b/pallets/subtensor/src/benchmarks.rs @@ -5,19 +5,19 @@ use crate::Pallet as Subtensor; use crate::staking::lock::LockState; use crate::*; -use codec::Compact; +use codec::{Compact, Encode}; use frame_benchmarking::v2::*; use frame_support::{StorageDoubleMap, assert_ok}; use frame_system::{RawOrigin, pallet_prelude::BlockNumberFor}; pub use pallet::*; -use sp_core::H256; +use sp_core::{H160, H256, ecdsa}; use sp_runtime::{ BoundedVec, Percent, traits::{BlakeTwo256, Hash}, }; use sp_std::collections::btree_set::BTreeSet; use sp_std::vec; -use substrate_fixed::types::{U64F64, U96F32}; +use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance}; use subtensor_swap_interface::SwapHandler; @@ -85,6 +85,67 @@ mod pallet_benchmarks { ); } + fn benchmark_evm_secret_key() -> libsecp256k1::SecretKey { + let seed = [42u8; 32]; + + match libsecp256k1::SecretKey::parse(&seed) { + Ok(secret_key) => secret_key, + Err(_) => panic!("benchmark EVM secret key must be valid"), + } + } + + fn evm_key_from_secret_key(secret_key: &libsecp256k1::SecretKey) -> H160 { + let public_key = libsecp256k1::PublicKey::from_secret_key(secret_key); + let uncompressed = public_key.serialize(); + + let public_key_without_prefix = match uncompressed.get(1..) { + Some(public_key_without_prefix) => public_key_without_prefix, + None => panic!("uncompressed secp256k1 public key must contain a prefix byte"), + }; + + let hashed_public_key = sp_io::hashing::keccak_256(public_key_without_prefix); + + let evm_key_bytes = match hashed_public_key.get(12..) { + Some(evm_key_bytes) => evm_key_bytes, + None => panic!("keccak256 hash must be 32 bytes"), + }; + + H160::from_slice(evm_key_bytes) + } + + fn signature_for_associate_evm_key( + hotkey: &T::AccountId, + block_number: u64, + secret_key: &libsecp256k1::SecretKey, + ) -> ecdsa::Signature { + let block_hash = sp_io::hashing::keccak_256(block_number.encode().as_ref()); + + let mut message = hotkey.encode(); + message.extend_from_slice(&block_hash); + + let message_hash = Subtensor::::hash_message_eip191(message); + let secp_message = libsecp256k1::Message::parse(&message_hash); + + let (secp_signature, recovery_id) = libsecp256k1::sign(&secp_message, secret_key); + + let mut signature = [0u8; 65]; + let serialized_signature = secp_signature.serialize(); + + let signature_bytes = match signature.get_mut(..64) { + Some(signature_bytes) => signature_bytes, + None => panic!("benchmark ECDSA signature buffer must contain 64 signature bytes"), + }; + signature_bytes.copy_from_slice(&serialized_signature); + + let recovery_id_byte = match signature.get_mut(64) { + Some(recovery_id_byte) => recovery_id_byte, + None => panic!("benchmark ECDSA signature buffer must contain a recovery id byte"), + }; + *recovery_id_byte = recovery_id.serialize(); + + ecdsa::Signature::from_raw(signature) + } + #[benchmark] fn register() { let netuid = NetUid::from(1); @@ -440,19 +501,15 @@ mod pallet_benchmarks { salt.clone(), version_key, )); - let commit_block = Subtensor::::get_current_block_as_u64(); assert_ok!(Subtensor::::commit_weights( RawOrigin::Signed(hotkey.clone()).into(), netuid, commit_hash, )); - let (first_reveal_block, _) = Subtensor::::get_reveal_blocks(netuid, commit_block); - let reveal_block: BlockNumberFor = first_reveal_block - .try_into() - .ok() - .expect("can't convert to block number"); - frame_system::Pallet::::set_block_number(reveal_block); + // Advance the epoch counter into the commit's reveal window. + let reveal_period = Subtensor::::get_reveal_period(netuid); + SubnetEpochIndex::::mutate(netuid, |e| *e = e.saturating_add(reveal_period)); #[extrinsic_call] _( @@ -676,7 +733,6 @@ mod pallet_benchmarks { let mut salts_list = Vec::new(); let mut version_keys = Vec::new(); - let commit_block = Subtensor::::get_current_block_as_u64(); for i in 0..num_commits { let uids = vec![0u16]; let values = vec![i as u16]; @@ -704,12 +760,9 @@ mod pallet_benchmarks { version_keys.push(version_key_i); } - let (first_reveal_block, _) = Subtensor::::get_reveal_blocks(netuid, commit_block); - let reveal_block: BlockNumberFor = first_reveal_block - .try_into() - .ok() - .expect("can't convert to block number"); - frame_system::Pallet::::set_block_number(reveal_block); + // Advance the epoch counter into the reveal window for these commits. + let reveal_period = Subtensor::::get_reveal_period(netuid); + SubnetEpochIndex::::mutate(netuid, |e| *e = e.saturating_add(reveal_period)); #[extrinsic_call] _( @@ -863,6 +916,7 @@ mod pallet_benchmarks { add_balance_to_coldkey_account::(&coldkey.clone(), initial_balance); add_lock::(&coldkey, netuid); + // Price = 0.01 let tao_reserve = TaoBalance::from(1_000_000_000_000_u64); let alpha_in = AlphaBalance::from(100_000_000_000_000_u64); set_reserves::(netuid, tao_reserve, alpha_in); @@ -877,7 +931,7 @@ mod pallet_benchmarks { // by swapping 100 TAO let current_price = T::SwapInterface::current_alpha_price(netuid); let limit = current_price - .saturating_mul(U96F32::saturating_from_num(1_001_000_000)) + .saturating_mul(U64F64::saturating_from_num(1_001_000_000)) .saturating_to_num::() .into(); let amount_to_be_staked = TaoBalance::from(100_000_000_000_u64); @@ -934,8 +988,6 @@ mod pallet_benchmarks { let _ = Subtensor::::create_account_if_non_existent(&coldkey, &destination); - StakingOperationRateLimiter::::remove((origin.clone(), coldkey.clone(), netuid)); - #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), @@ -966,6 +1018,7 @@ mod pallet_benchmarks { let hotkey: T::AccountId = account("Alice", 0, seed); Subtensor::::set_burn(netuid, benchmark_registration_burn()); + // Price = 0.01 let tao_reserve = TaoBalance::from(1_000_000_000_000_u64); let alpha_in = AlphaBalance::from(100_000_000_000_000_u64); set_reserves::(netuid, tao_reserve, alpha_in); @@ -991,8 +1044,6 @@ mod pallet_benchmarks { let amount_unstaked = AlphaBalance::from(30_000_000_000_u64); - StakingOperationRateLimiter::::remove((hotkey.clone(), coldkey.clone(), netuid)); - #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), @@ -1049,12 +1100,10 @@ mod pallet_benchmarks { let current_price = T::SwapInterface::current_alpha_price(netuid); let limit = current_price - .saturating_mul(U96F32::saturating_from_num(500_000_000)) + .saturating_mul(U64F64::saturating_from_num(999_900_000)) .saturating_to_num::() .into(); - StakingOperationRateLimiter::::remove((hotkey.clone(), coldkey.clone(), netuid)); - #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), @@ -1118,8 +1167,6 @@ mod pallet_benchmarks { allow )); - StakingOperationRateLimiter::::remove((hot.clone(), coldkey.clone(), netuid1)); - #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), @@ -1172,8 +1219,6 @@ mod pallet_benchmarks { let _ = Subtensor::::create_account_if_non_existent(&dest, &hot); - StakingOperationRateLimiter::::remove((hot.clone(), coldkey.clone(), netuid)); - #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), @@ -1229,8 +1274,6 @@ mod pallet_benchmarks { let alpha_to_swap = Subtensor::::get_stake_for_hotkey_and_coldkey_on_subnet(&hot, &coldkey, netuid1); - StakingOperationRateLimiter::::remove((hot.clone(), coldkey.clone(), netuid1)); - #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), @@ -1584,8 +1627,6 @@ mod pallet_benchmarks { staked_amt )); - StakingOperationRateLimiter::::remove((hotkey.clone(), coldkey.clone(), netuid)); - #[extrinsic_call] _(RawOrigin::Signed(coldkey), hotkey); } @@ -1627,7 +1668,7 @@ mod pallet_benchmarks { // by swapping 1 TAO let current_price = T::SwapInterface::current_alpha_price(netuid); let limit = current_price - .saturating_mul(U96F32::saturating_from_num(500_000_000)) + .saturating_mul(U64F64::saturating_from_num(500_000_000)) .saturating_to_num::() .into(); let staked_amt = TaoBalance::from(1_000_000_000_u64); @@ -1640,8 +1681,6 @@ mod pallet_benchmarks { staked_amt )); - StakingOperationRateLimiter::::remove((hotkey.clone(), coldkey.clone(), netuid)); - #[extrinsic_call] _( RawOrigin::Signed(coldkey.clone()), @@ -2148,6 +2187,117 @@ mod pallet_benchmarks { ); } + #[benchmark] + fn associate_evm_key() { + let netuid = NetUid::from(1); + let tempo: u16 = 1; + + let coldkey: T::AccountId = account("Test", 0, 1); + let hotkey: T::AccountId = account("Alice", 0, 1); + + Subtensor::::init_new_network(netuid, tempo); + SubtokenEnabled::::insert(netuid, true); + Subtensor::::set_network_registration_allowed(netuid, true); + Subtensor::::set_max_allowed_uids(netuid, 4096); + Subtensor::::set_burn(netuid, benchmark_registration_burn()); + + seed_swap_reserves::(netuid); + fund_for_registration::(netuid, &coldkey); + + assert_ok!(Subtensor::::burned_register( + RawOrigin::Signed(coldkey.clone()).into(), + netuid, + hotkey.clone() + )); + + let uid = match Subtensor::::get_uid_for_net_and_hotkey(netuid, &hotkey) { + Ok(uid) => uid, + Err(_) => panic!("registered benchmark hotkey must have a uid"), + }; + + // No existing association means `block_associated` is treated as 0. + // Move the block forward enough to satisfy: + // now - 0 >= T::EvmKeyAssociateRateLimit::get() + let benchmark_block_number = T::EvmKeyAssociateRateLimit::get().saturating_add(1); + + let benchmark_block: BlockNumberFor = match benchmark_block_number.try_into() { + Ok(benchmark_block) => benchmark_block, + Err(_) => panic!("benchmark block number must fit into BlockNumberFor"), + }; + + frame_system::Pallet::::set_block_number(benchmark_block); + + let block_number = Subtensor::::get_current_block_as_u64(); + + let evm_secret_key = benchmark_evm_secret_key(); + let evm_key = evm_key_from_secret_key(&evm_secret_key); + + let signature = + signature_for_associate_evm_key::(&hotkey, block_number, &evm_secret_key); + + #[extrinsic_call] + _( + RawOrigin::Signed(hotkey.clone()), + netuid, + evm_key, + block_number, + signature, + ); + + assert_eq!( + AssociatedEvmAddress::::get(netuid, uid), + Some((evm_key, block_number)) + ); + } + + #[benchmark] + fn set_tempo() { + let netuid = NetUid::from(1); + let coldkey: T::AccountId = account("Owner", 0, 1); + + Subtensor::::init_new_network(netuid, 1u16); + SubnetOwner::::insert(netuid, coldkey.clone()); + SubtokenEnabled::::insert(netuid, true); + Subtensor::::set_commit_reveal_weights_enabled(netuid, false); + Subtensor::::set_admin_freeze_window(0); + + #[extrinsic_call] + _(RawOrigin::Signed(coldkey.clone()), netuid, MIN_TEMPO); + } + + #[benchmark] + fn set_activity_cutoff_factor() { + let netuid = NetUid::from(1); + let coldkey: T::AccountId = account("Owner", 0, 1); + + Subtensor::::init_new_network(netuid, 1u16); + SubnetOwner::::insert(netuid, coldkey.clone()); + SubtokenEnabled::::insert(netuid, true); + Subtensor::::set_admin_freeze_window(0); + + #[extrinsic_call] + _( + RawOrigin::Signed(coldkey.clone()), + netuid, + INITIAL_ACTIVITY_CUTOFF_FACTOR_MILLI, + ); + } + + #[benchmark] + fn trigger_epoch() { + let netuid = NetUid::from(1); + let coldkey: T::AccountId = account("Owner", 0, 1); + + Subtensor::::init_new_network(netuid, 1u16); + SubnetOwner::::insert(netuid, coldkey.clone()); + SubtokenEnabled::::insert(netuid, true); + Subtensor::::set_commit_reveal_weights_enabled(netuid, false); + Subtensor::::set_admin_freeze_window(0); + + #[extrinsic_call] + _(RawOrigin::Signed(coldkey.clone()), netuid); + } + impl_benchmark_test_suite!( Subtensor, crate::tests::mock::new_test_ext(1), diff --git a/pallets/subtensor/src/coinbase/block_step.rs b/pallets/subtensor/src/coinbase/block_step.rs index fac924ccf4..00f1ac16a9 100644 --- a/pallets/subtensor/src/coinbase/block_step.rs +++ b/pallets/subtensor/src/coinbase/block_step.rs @@ -36,9 +36,11 @@ impl Pallet { } fn try_set_pending_children(block_number: u64) { + // Called *after* `run_coinbase` has advanced `LastEpochBlock` for any + // subnet whose epoch slot fired this block — `should_run_epoch` is no + // longer true. Detect "epoch just fired" by `LastEpochBlock == block`. for netuid in Self::get_all_subnet_netuids() { - if Self::should_run_epoch(netuid, block_number) { - // Set pending children on the epoch. + if LastEpochBlock::::get(netuid) == block_number { Self::do_set_pending_children(netuid); } } @@ -77,8 +79,18 @@ impl Pallet { } pub fn reveal_crv3_commits() { - let netuids: Vec = Self::get_all_subnet_netuids(); - for netuid in netuids.into_iter().filter(|netuid| *netuid != NetUid::ROOT) { + let current_block = Self::get_current_block_as_u64(); + let subnets: Vec = Self::get_all_subnet_netuids() + .into_iter() + .filter(|netuid| *netuid != NetUid::ROOT) + .collect(); + // Subnets whose epoch is due this block but deferred by the per-block cap. + let deferred = Self::epochs_deferred_this_block(&subnets, current_block); + + for netuid in subnets.into_iter() { + if deferred.contains(&netuid) { + continue; + } // Reveal matured weights. if let Err(e) = Self::reveal_crv3_commits_for_subnet(netuid) { log::warn!("Failed to reveal commits for subnet {netuid} due to error: {e:?}"); diff --git a/pallets/subtensor/src/coinbase/mod.rs b/pallets/subtensor/src/coinbase/mod.rs index c51bf58d1d..5184e2e3c0 100644 --- a/pallets/subtensor/src/coinbase/mod.rs +++ b/pallets/subtensor/src/coinbase/mod.rs @@ -7,3 +7,4 @@ pub mod root; pub mod run_coinbase; pub mod subnet_emissions; pub mod tao; +pub mod tempo_control; diff --git a/pallets/subtensor/src/coinbase/reveal_commits.rs b/pallets/subtensor/src/coinbase/reveal_commits.rs index 3d43cfba29..a5cddd6856 100644 --- a/pallets/subtensor/src/coinbase/reveal_commits.rs +++ b/pallets/subtensor/src/coinbase/reveal_commits.rs @@ -38,8 +38,9 @@ impl Pallet { /// The `reveal_crv3_commits` function is run at the very beginning of epoch `n`, pub fn reveal_crv3_commits_for_subnet(netuid: NetUid) -> dispatch::DispatchResult { let reveal_period = Self::get_reveal_period(netuid); - let cur_block = Self::get_current_block_as_u64(); - let cur_epoch = Self::get_epoch_index(netuid, cur_block); + // If the subnet is deferred past this block the + // commits are taken once here and the later block(s) become no-ops. + let cur_epoch = Self::current_epoch_with_lookahead(netuid); // Weights revealed must have been committed during epoch `cur_epoch - reveal_period`. let reveal_epoch = cur_epoch.saturating_sub(reveal_period); diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index b64043a4f5..d339e0ec83 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -18,7 +18,7 @@ use super::*; use crate::CommitmentsInterface; use safe_math::*; -use substrate_fixed::types::{I64F64, U96F32}; +use substrate_fixed::types::{I64F64, U64F64}; use subtensor_runtime_common::{AlphaBalance, NetUid, NetUidStorageIndex, TaoBalance, Token}; use subtensor_swap_interface::SwapHandler; @@ -213,8 +213,6 @@ impl Pallet { Self::finalize_all_subnet_root_dividends(netuid); // --- Perform the cleanup before removing the network. - // Will handle it in dissolve network PR. - T::SwapInterface::dissolve_all_liquidity_providers(netuid).map_err(|e| e.error)?; Self::destroy_alpha_in_out_stakes(netuid)?; T::SwapInterface::clear_protocol_liquidity(netuid)?; T::CommitmentsInterface::purge_netuid(netuid); @@ -284,6 +282,10 @@ impl Pallet { MaxAllowedUids::::remove(netuid); ImmunityPeriod::::remove(netuid); ActivityCutoff::::remove(netuid); + ActivityCutoffFactorMilli::::remove(netuid); + LastEpochBlock::::remove(netuid); + PendingEpochAt::::remove(netuid); + SubnetEpochIndex::::remove(netuid); MinAllowedWeights::::remove(netuid); RegistrationsThisInterval::::remove(netuid); POWRegistrationsThisInterval::::remove(netuid); @@ -302,7 +304,6 @@ impl Pallet { SubnetEmaProtocolFlow::::remove(netuid); SubnetExcessTao::::remove(netuid); SubnetRootSellTao::::remove(netuid); - SubnetTaoProvided::::remove(netuid); // --- 13. Token / mechanism / registration toggles. TokenSymbol::::remove(netuid); @@ -462,21 +463,6 @@ impl Pallet { TransactionKeyLastBlock::::remove((hot, netuid, name)); } } - // StakingOperationRateLimiter NMAP: (hot, cold, netuid) → bool - { - let to_rm: sp_std::vec::Vec<(T::AccountId, T::AccountId)> = - StakingOperationRateLimiter::::iter() - .filter_map( - |((hot, cold, n), _)| { - if n == netuid { Some((hot, cold)) } else { None } - }, - ) - .collect(); - for (hot, cold) in to_rm { - StakingOperationRateLimiter::::remove((hot, cold, netuid)); - } - } - // --- 22. Subnet leasing: remove mapping and any lease-scoped state linked to this netuid. if let Some(lease_id) = SubnetUidToLeaseId::::take(netuid) { SubnetLeases::::remove(lease_id); @@ -607,7 +593,7 @@ impl Pallet { let current_block: u64 = Self::get_current_block_as_u64(); let mut candidate_netuid: Option = None; - let mut candidate_price: U96F32 = U96F32::saturating_from_num(u128::MAX); + let mut candidate_price: U64F64 = U64F64::saturating_from_num(u128::MAX); let mut candidate_timestamp: u64 = u64::MAX; for (netuid, added) in NetworksAdded::::iter() { @@ -622,7 +608,7 @@ impl Pallet { continue; } - let price: U96F32 = Self::get_moving_alpha_price(netuid); + let price: U64F64 = Self::get_moving_alpha_price(netuid); // If tie on price, earliest registration wins. if price < candidate_price diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index a6c988feea..d42f90ec98 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -1,9 +1,9 @@ use super::*; use crate::coinbase::tao::CreditOf; -use alloc::collections::BTreeMap; +use alloc::collections::{BTreeMap, BTreeSet}; use frame_support::traits::Imbalance; use safe_math::*; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::{U64F64, U96F32}; use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; use subtensor_swap_interface::SwapHandler; @@ -85,13 +85,16 @@ impl Pallet { let tao_to_swap_with: TaoBalance = tou64!(excess_tao.get(netuid_i).unwrap_or(&asfloat!(0))).into(); + // Inject tao and alpha into protocol liquidity. In theorry, it may not always + // be a success (returned values are 0s) in case of high liquidity disbalance + let (actual_injected_tao, actual_injected_alpha) = + T::SwapInterface::adjust_protocol_liquidity(*netuid_i, tao_in_i, alpha_in_i); + // Clear per-block pool-side emission counters up front so a subnet // disabled this block does not display stale values from an earlier block. SubnetExcessTao::::insert(*netuid_i, TaoBalance::ZERO); SubnetTaoInEmission::::insert(*netuid_i, TaoBalance::ZERO); - T::SwapInterface::adjust_protocol_liquidity(*netuid_i, tao_in_i, alpha_in_i); - if tao_to_swap_with > TaoBalance::ZERO { // Turn excess_tao portion of credit into TaoBalance on subnet account match Self::spend_tao(&subnet_account_id, remaining_credit, tao_to_swap_with) { @@ -128,37 +131,34 @@ impl Pallet { } // Inject Alpha in. - let alpha_in_i = - AlphaBalance::from(tou64!(*alpha_in.get(netuid_i).unwrap_or(&asfloat!(0)))); - SubnetAlphaInEmission::::insert(*netuid_i, alpha_in_i); + SubnetAlphaInEmission::::insert(*netuid_i, actual_injected_alpha); // Mint alpha and resolve to alpha reserve - Self::resolve_to_alpha_in(Self::mint_alpha(*netuid_i, alpha_in_i)); + Self::resolve_to_alpha_in(Self::mint_alpha(*netuid_i, actual_injected_alpha)); // Inject TAO in. - let injected_tao: TaoBalance = - tou64!(*tao_in.get(netuid_i).unwrap_or(&asfloat!(0))).into(); - if !injected_tao.is_zero() { - match Self::spend_tao(&subnet_account_id, remaining_credit, injected_tao) { + if !actual_injected_tao.is_zero() { + match Self::spend_tao(&subnet_account_id, remaining_credit, actual_injected_tao) + { Ok(remainder) => { remaining_credit = remainder; - SubnetTaoInEmission::::insert(*netuid_i, injected_tao); + SubnetTaoInEmission::::insert(*netuid_i, actual_injected_tao); SubnetTAO::::mutate(*netuid_i, |total| { - *total = total.saturating_add(injected_tao); + *total = total.saturating_add(actual_injected_tao); }); TotalStake::::mutate(|total| { - *total = total.saturating_add(injected_tao); + *total = total.saturating_add(actual_injected_tao); }); // Record emission injection as protocol inflow. - Self::record_protocol_inflow(*netuid_i, injected_tao); + Self::record_protocol_inflow(*netuid_i, actual_injected_tao); } Err(remainder) => { remaining_credit = remainder; let remaining_balance = remaining_credit.peek(); log::error!( - "Failed to spend credit: injected_tao = {injected_tao:?}, netuid_i = {netuid_i:?}, remaining_balance = {remaining_balance:?}" + "Failed to spend credit: injected_tao = {actual_injected_tao:?}, netuid_i = {netuid_i:?}, remaining_balance = {remaining_balance:?}" ); } } @@ -203,7 +203,8 @@ impl Pallet { log::debug!("alpha_emission_i: {alpha_emission_i:?}"); // Get subnet price. - let price_i: U96F32 = T::SwapInterface::current_alpha_price(netuid_i.into()); + let price_i: U96F32 = + U96F32::saturating_from_num(T::SwapInterface::current_alpha_price(netuid_i.into())); log::debug!("price_i: {price_i:?}"); let mut tao_in_i: U96F32 = tao_emission_i; @@ -318,6 +319,29 @@ impl Pallet { } } + /// Subnets whose epoch slot is due *this* block but is deferred by the per-block + /// cap (`MaxEpochsPerBlock`). + pub fn epochs_deferred_this_block(subnets: &[NetUid], current_block: u64) -> BTreeSet { + let cap = T::MaxEpochsPerBlock::get(); + let mut deferred: BTreeSet = BTreeSet::new(); + let mut epochs_run_this_block: u32 = 0; + + for &netuid in subnets.iter() { + if !Self::should_run_epoch(netuid, current_block) { + continue; + } + // Per-block cap — due subnets beyond the limit are deferred. + if epochs_run_this_block >= cap { + deferred.insert(netuid); + continue; + } + if Self::is_epoch_input_state_consistent(netuid) { + epochs_run_this_block = epochs_run_this_block.saturating_add(1); + } + } + deferred + } + pub fn drain_pending( subnets: &[NetUid], current_block: u64, @@ -327,19 +351,35 @@ impl Pallet { NetUid, (AlphaBalance, AlphaBalance, AlphaBalance, AlphaBalance), > = BTreeMap::new(); - // --- Drain pending emissions for all subnets hat are at their tempo. - // Run the epoch for *all* subnets, even if we don't emit anything. + // Per-block cap on number of epochs that may run; the rest are deferred 1 block forward + // by setting `PendingEpochAt`. + let mut epochs_run_this_block: u32 = 0; + for &netuid in subnets.iter() { - // Increment blocks since last step. + // Increment blocks since last *successful* step (existing semantics). BlocksSinceLastStep::::mutate(netuid, |total| *total = total.saturating_add(1)); - // Run the epoch if applicable. - if Self::should_run_epoch(netuid, current_block) - && Self::is_epoch_input_state_consistent(netuid) - { - // Restart counters. + if !Self::should_run_epoch(netuid, current_block) { + continue; + } + + // Per-block cap — defer if already at limit. + if epochs_run_this_block >= T::MaxEpochsPerBlock::get() { + let next_block = current_block.saturating_add(1); + PendingEpochAt::::insert(netuid, next_block); + Self::deposit_event(Event::EpochDeferred { + netuid, + from_block: current_block, + to_block: next_block, + }); + continue; + } + + if Self::is_epoch_input_state_consistent(netuid) { + // Reset blocks-since counter; LastMechansimStepBlock is written + // post-distribute (see the caller), so bonds masking can read the + // previous successful run. BlocksSinceLastStep::::insert(netuid, 0); - LastMechansimStepBlock::::insert(netuid, current_block); // Get and drain the subnet pending emission. let pending_server_alpha = PendingServerEmission::::get(netuid); @@ -366,11 +406,24 @@ impl Pallet { owner_cut, ), ); + epochs_run_this_block = epochs_run_this_block.saturating_add(1); // Reserved for potential future enhancements. // Ownership update logic based on conviction is currently inactive by design. // Self::change_subnet_owner_if_needed(netuid); + } else { + // Schedule advances below; execution skipped. Pending emissions accumulate + // and will be drained by the next successful epoch. + Self::deposit_event(Event::EpochSkipped { + netuid, + block: current_block, + }); } + + // Advance the schedule unconditionally — the slot is consumed. + LastEpochBlock::::insert(netuid, current_block); + PendingEpochAt::::insert(netuid, 0); + SubnetEpochIndex::::mutate(netuid, |idx| *idx = idx.saturating_add(1)); } emissions_to_distribute } @@ -381,6 +434,7 @@ impl Pallet { (AlphaBalance, AlphaBalance, AlphaBalance, AlphaBalance), >, ) { + let current_block = Self::get_current_block_as_u64(); for ( &netuid, &(pending_server_alpha, pending_validator_alpha, pending_root_alpha, pending_owner_cut), @@ -394,18 +448,19 @@ impl Pallet { pending_root_alpha, pending_owner_cut, ); + LastMechansimStepBlock::::insert(netuid, current_block); } } pub fn get_network_root_sell_flag(subnets_to_emit_to: &[NetUid]) -> bool { - let total_ema_price: U96F32 = subnets_to_emit_to + let total_ema_price: U64F64 = subnets_to_emit_to .iter() .map(|netuid| Self::get_moving_alpha_price(*netuid)) .sum(); // If the total EMA price is less than or equal to 1 // then we WILL NOT root sell. - total_ema_price > U96F32::saturating_from_num(1) + total_ema_price > U64F64::saturating_from_num(1) } pub fn calculate_dividends_and_incentives( @@ -1009,28 +1064,57 @@ impl Pallet { /// # Returns /// * `bool` - True if the epoch should run, false otherwise. pub fn should_run_epoch(netuid: NetUid, current_block: u64) -> bool { - Self::blocks_until_next_epoch(netuid, Self::get_tempo(netuid), current_block) == 0 + let tempo = Self::get_tempo(netuid); + if tempo == 0 { + return false; + } + let pending = PendingEpochAt::::get(netuid); + if pending > 0 && current_block >= pending { + return true; + } + if BlocksSinceLastStep::::get(netuid) > MAX_TEMPO as u64 { + return true; + } + let last = LastEpochBlock::::get(netuid); + let blocks_since = current_block.saturating_sub(last); + blocks_since >= tempo as u64 } - /// Helper function which returns the number of blocks remaining before we will run the epoch on this - /// network. Networks run their epoch when (block_number + netuid + 1 ) % (tempo + 1) = 0 - /// tempo | netuid | # first epoch block - /// 1 0 0 - /// 1 1 1 - /// 2 0 1 - /// 2 1 0 - /// 100 0 99 - /// 100 1 98 - /// Special case: tempo = 0, the network never runs. - /// - pub fn blocks_until_next_epoch(netuid: NetUid, tempo: u16, block_number: u64) -> u64 { + /// Returns the number of blocks remaining before the next automatic epoch under the + /// stateful scheduler (period `tempo`, anchored on `LastEpochBlock`). Does NOT account for: + /// - `PendingEpochAt` (owner-triggered manual fire — could happen sooner), + /// - `BlocksSinceLastStep > MAX_TEMPO` safety-net, + /// - per-block-cap defer (could push the actual fire one or more blocks later) + /// Used by the admin-freeze-window predicate and external tooling. Returns `u64::MAX` when + /// `tempo == 0` (legacy defensive short-circuit). + pub fn blocks_until_next_auto_epoch(netuid: NetUid, tempo: u16, block_number: u64) -> u64 { if tempo == 0 { return u64::MAX; } - let netuid_plus_one = (u16::from(netuid) as u64).saturating_add(1); - let tempo_plus_one = (tempo as u64).saturating_add(1); - let adjusted_block = block_number.wrapping_add(netuid_plus_one); - let remainder = adjusted_block.checked_rem(tempo_plus_one).unwrap_or(0); - (tempo as u64).saturating_sub(remainder) + let last = LastEpochBlock::::get(netuid); + // Period is `tempo`: next firing at `last + tempo`. + let next_auto = last.saturating_add(tempo as u64); + next_auto.saturating_sub(block_number) + } + + /// Returns the absolute block number at which the next epoch is expected to fire for the + /// given subnet, considering both the automatic schedule (`LastEpochBlock + tempo`) and + /// any owner-triggered `PendingEpochAt`. Returns `None` if `tempo == 0` (subnet does not run). + /// Does NOT account for the per-block cap deferral or the `BlocksSinceLastStep > MAX_TEMPO` + /// safety-net (which can fire earlier under extreme drift). + pub fn get_next_epoch_start_block(netuid: NetUid) -> Option { + let tempo = Self::get_tempo(netuid); + if tempo == 0 { + return None; + } + let last = LastEpochBlock::::get(netuid); + let auto_next = last.saturating_add(tempo as u64); + + let pending = PendingEpochAt::::get(netuid); + if pending > 0 { + Some(auto_next.min(pending)) + } else { + Some(auto_next) + } } } diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index 63dbf36e5a..6ff188f362 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -5,6 +5,19 @@ use substrate_fixed::transcendental::{exp, ln}; use substrate_fixed::types::{I32F32, I64F64, U64F64, U96F32}; impl Pallet { + /// Returns the subnets that are eligible to receive emissions. + /// + /// # Parameters + /// - `subnets`: Candidate subnet IDs to evaluate in order. + /// + /// # Returns + /// A vector containing the candidate subnet IDs that are non-root, have + /// started emissions, have subtokens enabled, and currently allow network + /// registration. + /// + /// AI-readable: This output is passed to `get_shares_flow`, so changing these + /// eligibility rules also changes which subnet user TAO flow EMAs and protocol + /// flow EMAs are advanced during emission sharing. pub fn get_subnets_to_emit_to(subnets: &[NetUid]) -> Vec { // Filter out root subnet. // Filter out subnets with no first emission block number. @@ -246,8 +259,13 @@ impl Pallet { let zero = I64F64::saturating_from_num(0); // Always update both EMAs (keeps protocol EMA warm for when toggled on). - // Fixes #2667: protocol EMA accumulator was only drained when enabled, - // causing a shock on toggle. + // Note: + // User TAO EMAs are updated every time this method runs because get_ema_flow() + // is called before the NetTaoFlowEnabled branch. Protocol EMAs are different: + // update_ema_protocol_flow() is only called while NetTaoFlowEnabled is true. + // If net flow is disabled, protocol flow keeps accumulating in SubnetProtocolFlow + // and SubnetEmaProtocolFlow is not advanced/reset, so toggling net flow back on + // applies stale accumulated protocol flow in the next EMA update. let subnet_emas: Vec<(NetUid, I64F64, I64F64)> = subnets_to_emit_to .iter() .map(|netuid| { diff --git a/pallets/subtensor/src/coinbase/tao.rs b/pallets/subtensor/src/coinbase/tao.rs index 33dbda57fb..0dee496c3b 100644 --- a/pallets/subtensor/src/coinbase/tao.rs +++ b/pallets/subtensor/src/coinbase/tao.rs @@ -273,6 +273,11 @@ impl Pallet { credit: CreditOf, part: BalanceOf, ) -> Result, CreditOf> { + // Reject overspending. + if credit.peek() < part { + return Err(credit); + } + let (to_spend, remainder) = credit.split(part); match ::Currency::resolve(coldkey, to_spend) { diff --git a/pallets/subtensor/src/coinbase/tempo_control.rs b/pallets/subtensor/src/coinbase/tempo_control.rs new file mode 100644 index 0000000000..c43c53019e --- /dev/null +++ b/pallets/subtensor/src/coinbase/tempo_control.rs @@ -0,0 +1,116 @@ +use super::*; +use crate::Error; +use frame_support::pallet_prelude::DispatchResult; +use sp_runtime::DispatchError; +use subtensor_runtime_common::NetUid; + +use crate::system::pallet_prelude::OriginFor; +use crate::utils::rate_limiting::{Hyperparameter, TransactionType}; + +impl Pallet { + /// Owner-side `set_tempo` implementation. + pub fn do_set_tempo(origin: OriginFor, netuid: NetUid, tempo: u16) -> DispatchResult { + let who = Self::ensure_subnet_owner(origin, netuid)?; + + ensure!( + (MIN_TEMPO..=MAX_TEMPO).contains(&tempo), + Error::::TempoOutOfBounds + ); + + Self::ensure_admin_window_open(netuid)?; + + let tx = TransactionType::TempoUpdate; + ensure!( + tx.passes_rate_limit_on_subnet::(&who, netuid), + Error::::TxRateLimitExceeded + ); + + let now = Self::get_current_block_as_u64(); + + Self::apply_tempo_with_cycle_reset(netuid, tempo); + + tx.set_last_block_on_subnet::(&who, netuid, now); + Ok(()) + } + + /// `set_activity_cutoff_factor` implementation. Callable by the subnet owner + /// (subject to admin freeze window + rate limit) or by root (bypasses both). + pub fn do_set_activity_cutoff_factor( + origin: OriginFor, + netuid: NetUid, + factor_milli: u32, + ) -> DispatchResult { + let maybe_who = Self::ensure_subnet_owner_or_root(origin, netuid)?; + + ensure!( + (MIN_ACTIVITY_CUTOFF_FACTOR_MILLI..=MAX_ACTIVITY_CUTOFF_FACTOR_MILLI) + .contains(&factor_milli), + Error::::ActivityCutoffFactorMilliOutOfBounds + ); + + let tx = TransactionType::OwnerHyperparamUpdate(Hyperparameter::ActivityCutoffFactorMilli); + + // Admin freeze window and per-owner rate limit apply only to the subnet + // owner. Root bypasses both as a governance override. + if let Some(who) = maybe_who.as_ref() { + Self::ensure_admin_window_open(netuid)?; + ensure!( + tx.passes_rate_limit_on_subnet::(who, netuid), + Error::::TxRateLimitExceeded + ); + } + + Self::set_activity_cutoff_factor_milli(netuid, factor_milli); + + if let Some(who) = maybe_who.as_ref() { + let now = Self::get_current_block_as_u64(); + tx.set_last_block_on_subnet::(who, netuid, now); + } + + Ok(()) + } + + /// Owner-side `trigger_epoch` implementation. + /// Schedules the triggered epoch to fire after `AdminFreezeWindow` blocks; that + /// countdown engages the freeze window for the subnet via `is_in_admin_freeze_window`. + pub fn do_trigger_epoch(origin: OriginFor, netuid: NetUid) -> Result<(), DispatchError> { + let who = Self::ensure_subnet_owner(origin, netuid)?; + + // Block triggering to avoid breaking CRv3 reveal + ensure!( + !Self::get_commit_reveal_weights_enabled(netuid), + Error::::DynamicTempoBlockedByCommitReveal + ); + + // No `ensure_admin_window_open` here: trigger *defines* the next epoch. + ensure!( + PendingEpochAt::::get(netuid) == 0, + Error::::EpochTriggerAlreadyPending + ); + + let now = Self::get_current_block_as_u64(); + let window = AdminFreezeWindow::::get() as u64; + + let tempo = Self::get_tempo(netuid); + let remaining = Self::blocks_until_next_auto_epoch(netuid, tempo, now); + ensure!(remaining >= window, Error::::AutoEpochAlreadyImminent); + + let tx = TransactionType::OwnerHyperparamUpdate(Hyperparameter::TriggerEpoch); + ensure!( + tx.passes_rate_limit_on_subnet::(&who, netuid), + Error::::TxRateLimitExceeded + ); + + let fires_at = now.saturating_add(window); + + PendingEpochAt::::insert(netuid, fires_at); + tx.set_last_block_on_subnet::(&who, netuid, now); + + Self::deposit_event(Event::EpochTriggered { + netuid, + by: who, + fires_at, + }); + Ok(()) + } +} diff --git a/pallets/subtensor/src/epoch/run_epoch.rs b/pallets/subtensor/src/epoch/run_epoch.rs index 962c5bbbb4..cbfdc5a0fd 100644 --- a/pallets/subtensor/src/epoch/run_epoch.rs +++ b/pallets/subtensor/src/epoch/run_epoch.rs @@ -169,7 +169,7 @@ impl Pallet { log::trace!("tempo: {tempo:?}"); // Get activity cutoff. - let activity_cutoff: u64 = Self::get_activity_cutoff(netuid) as u64; + let activity_cutoff: u64 = Self::get_activity_cutoff_blocks(netuid); log::trace!("activity_cutoff: {activity_cutoff:?}"); // Last update vector. @@ -205,7 +205,13 @@ impl Pallet { // Recently registered matrix, recently_ij=True if last_tempo was *before* j was last registered. // Mask if: the last tempo block happened *before* the registration block // ==> last_tempo <= registered - let last_tempo: u64 = current_block.saturating_sub(tempo); + // For dynamic tempo - we pick previous-successful-epoch block: `LastMechansimStepBlock + 1` + let lms = LastMechansimStepBlock::::get(netuid); + let last_tempo: u64 = if lms == 0 { + current_block.saturating_sub(tempo) + } else { + lms.saturating_add(1) + }; let recently_registered: Vec = block_at_registration .iter() .map(|registered| last_tempo <= *registered) @@ -595,7 +601,7 @@ impl Pallet { log::trace!("tempo:\n{tempo:?}\n"); // Get activity cutoff. - let activity_cutoff: u64 = Self::get_activity_cutoff(netuid) as u64; + let activity_cutoff: u64 = Self::get_activity_cutoff_blocks(netuid); log::trace!("activity_cutoff: {activity_cutoff:?}"); // Last update vector. @@ -729,24 +735,30 @@ impl Pallet { let uid_of = |acct: &T::AccountId| terms_map.get(acct).map(|t| t.uid); // ---------- v2 ------------------------------------------------------ + // `WeightCommits` tuple: (hash, commit_epoch, commit_block, _). + // Expiry keys off `commit_epoch`; the column mask compares the absolute + // `commit_block` against `block_at_registration` (both block numbers). for (who, q) in WeightCommits::::iter_prefix(netuid_index) { - for (_, cb, _, _) in q.iter() { - if !Self::is_commit_expired(netuid, *cb) { + for (_, commit_epoch, commit_block, _) in q.iter() { + if !Self::is_commit_expired(netuid, *commit_epoch) { if let Some(cell) = uid_of(&who).and_then(|i| commit_blocks.get_mut(i)) { - *cell = (*cell).min(*cb); + *cell = (*cell).min(*commit_block); } break; // earliest active found } } } - // ---------- v3 ------------------------------------------------------ - for (_epoch, q) in TimelockedWeightCommits::::iter_prefix(netuid_index) { - for (who, cb, ..) in q.iter() { - if !Self::is_commit_expired(netuid, *cb) - && let Some(cell) = uid_of(who).and_then(|i| commit_blocks.get_mut(i)) - { - *cell = (*cell).min(*cb); + // ---------- v4 ------------------------------------------------------ + // `TimelockedWeightCommits` is keyed by `commit_epoch`; the value tuple + // carries the absolute `commit_block` in field 1. + for (commit_epoch, q) in TimelockedWeightCommits::::iter_prefix(netuid_index) { + if Self::is_commit_expired(netuid, commit_epoch) { + continue; + } + for (who, commit_block, ..) in q.iter() { + if let Some(cell) = uid_of(who).and_then(|i| commit_blocks.get_mut(i)) { + *cell = (*cell).min(*commit_block); } } } @@ -819,7 +831,13 @@ impl Pallet { // Remove bonds referring to neurons that have registered since last tempo. // Mask if: the last tempo block happened *before* the registration block // ==> last_tempo <= registered - let last_tempo: u64 = current_block.saturating_sub(tempo); + // For dynamic tempo - we pick previous-successful-epoch block: `LastMechansimStepBlock + 1` + let lms = LastMechansimStepBlock::::get(netuid); + let last_tempo: u64 = if lms == 0 { + current_block.saturating_sub(tempo) + } else { + lms.saturating_add(1) + }; bonds = scalar_vec_mask_sparse_matrix( &bonds, last_tempo, @@ -859,7 +877,13 @@ impl Pallet { // Remove bonds referring to neurons that have registered since last tempo. // Mask if: the last tempo block happened *before* the registration block // ==> last_tempo <= registered - let last_tempo: u64 = current_block.saturating_sub(tempo); + // For dynamic tempo - we pick previous-successful-epoch block: `LastMechansimStepBlock + 1` + let lms = LastMechansimStepBlock::::get(netuid); + let last_tempo: u64 = if lms == 0 { + current_block.saturating_sub(tempo) + } else { + lms.saturating_add(1) + }; bonds = scalar_vec_mask_sparse_matrix( &bonds, last_tempo, diff --git a/pallets/subtensor/src/extensions/subtensor.rs b/pallets/subtensor/src/extensions/subtensor.rs index e44c750268..39c6c4e011 100644 --- a/pallets/subtensor/src/extensions/subtensor.rs +++ b/pallets/subtensor/src/extensions/subtensor.rs @@ -188,9 +188,9 @@ where salt, *version_key, ); - match Pallet::::find_commit_block_via_hash(provided_hash) { - Some(commit_block) => { - if Pallet::::is_reveal_block_range(*netuid, commit_block) { + match Pallet::::find_commit_epoch_via_hash(provided_hash) { + Some(commit_epoch) => { + if Pallet::::is_reveal_block_range(*netuid, commit_epoch) { Ok((Default::default(), (), origin)) } else { Err(CustomTransactionError::CommitBlockNotInRevealRange.into()) @@ -218,9 +218,9 @@ where salt, *version_key, ); - match Pallet::::find_commit_block_via_hash(provided_hash) { - Some(commit_block) => { - if Pallet::::is_reveal_block_range(*netuid, commit_block) { + match Pallet::::find_commit_epoch_via_hash(provided_hash) { + Some(commit_epoch) => { + if Pallet::::is_reveal_block_range(*netuid, commit_epoch) { Ok((Default::default(), (), origin)) } else { Err(CustomTransactionError::CommitBlockNotInRevealRange.into()) @@ -258,13 +258,13 @@ where }) .collect::>(); - let batch_reveal_block = provided_hashes + let batch_reveal_epoch = provided_hashes .iter() - .filter_map(|hash| Pallet::::find_commit_block_via_hash(*hash)) + .filter_map(|hash| Pallet::::find_commit_epoch_via_hash(*hash)) .collect::>(); - if provided_hashes.len() == batch_reveal_block.len() { - if Pallet::::is_batch_reveal_block_range(*netuid, batch_reveal_block) { + if provided_hashes.len() == batch_reveal_epoch.len() { + if Pallet::::is_batch_reveal_epoch_range(*netuid, batch_reveal_epoch) { Ok((Default::default(), (), origin)) } else { Err(CustomTransactionError::CommitBlockNotInRevealRange.into()) diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 97ba77a92a..410e3bef16 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -1335,11 +1335,6 @@ pub mod pallet { pub type SubnetTAO = StorageMap<_, Identity, NetUid, TaoBalance, ValueQuery, DefaultZeroTao>; - /// --- MAP ( netuid ) --> tao_in_user_subnet | Returns the amount of TAO in the subnet reserve provided by users as liquidity. - #[pallet::storage] - pub type SubnetTaoProvided = - StorageMap<_, Identity, NetUid, TaoBalance, ValueQuery, DefaultZeroTao>; - /// --- MAP ( netuid ) --> alpha_in_emission | Returns the amount of alph in emission into the pool per block. #[pallet::storage] pub type SubnetAlphaInEmission = @@ -1382,11 +1377,6 @@ pub mod pallet { pub type SubnetAlphaIn = StorageMap<_, Identity, NetUid, AlphaBalance, ValueQuery, DefaultZeroAlpha>; - /// --- MAP ( netuid ) --> alpha_supply_user_in_pool | Returns the amount of alpha in the pool provided by users as liquidity. - #[pallet::storage] - pub type SubnetAlphaInProvided = - StorageMap<_, Identity, NetUid, AlphaBalance, ValueQuery, DefaultZeroAlpha>; - /// --- MAP ( netuid ) --> alpha_supply_in_subnet | Returns the amount of alpha in the subnet. #[pallet::storage] pub type SubnetAlphaOut = @@ -1542,6 +1532,19 @@ pub mod pallet { OptionQuery, >; + /// --- NMAP ( netuid, hotkey, coldkey ) --> () | Reverse index for non-zero locks targeting this hotkey on this subnet. + #[pallet::storage] + pub type LockingColdkeys = StorageNMap< + _, + ( + NMapKey, // subnet + NMapKey, // hotkey + NMapKey, // coldkey + ), + (), + OptionQuery, + >; + /// --- DMAP ( netuid, hotkey ) --> LockState | Total lock per hotkey per subnet. #[pallet::storage] pub type HotkeyLock = StorageDoubleMap< @@ -1793,6 +1796,50 @@ pub mod pallet { #[pallet::storage] pub type Tempo = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultTempo>; + /// Lower bound for owner-set tempo. Also the fixed cooldown for `set_tempo`. + pub const MIN_TEMPO: u16 = 360; + /// Upper bound for owner-set tempo (≈ 7 days at 12 s/block). + pub const MAX_TEMPO: u16 = 50_400; + /// Lower bound for activity-cutoff factor (per-mille). 1_000 = one full tempo. + pub const MIN_ACTIVITY_CUTOFF_FACTOR_MILLI: u32 = 1_000; + /// Upper bound for activity-cutoff factor (per-mille). 50_000 = 50 tempos. + pub const MAX_ACTIVITY_CUTOFF_FACTOR_MILLI: u32 = 50_000; + /// Default activity-cutoff factor (per-mille). 13_889 ≈ legacy 5000-block cutoff + /// at default tempo 360 (`13_889 * 360 / 1000 = 5_000`, exact via ceiling rounding). + pub const INITIAL_ACTIVITY_CUTOFF_FACTOR_MILLI: u32 = 13_889; + + /// Default value for activity-cutoff factor (per-mille). + #[pallet::type_value] + pub fn DefaultActivityCutoffFactorMilli() -> u32 { + INITIAL_ACTIVITY_CUTOFF_FACTOR_MILLI + } + + /// --- MAP ( netuid ) --> last epoch attempt block (consumed slot). + /// Drives normal-cadence scheduling and the admin freeze window. + /// Advances on every `should_run_epoch == true` slot — including consistency-skipped slots — + /// and on a successful `set_tempo` (cycle reset). + #[pallet::storage] + pub type LastEpochBlock = + StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultZeroU64>; + + /// --- MAP ( netuid ) --> block at which a manually triggered epoch should fire. + /// `0` means no trigger pending. Cleared after the triggered epoch runs. + #[pallet::storage] + pub type PendingEpochAt = + StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultZeroU64>; + + /// --- MAP ( netuid ) --> monotonic epoch counter. + /// Incremented by exactly one each time the subnet's epoch slot is consumed in `run_coinbase` + #[pallet::storage] + pub type SubnetEpochIndex = + StorageMap<_, Identity, NetUid, u64, ValueQuery, DefaultZeroU64>; + + /// --- MAP ( netuid ) --> activity-cutoff factor in per-mille epochs (1/1000 granularity). + /// Effective cutoff in blocks = `(factor × tempo) / 1000`, clamped to ≥ 1. + #[pallet::storage] + pub type ActivityCutoffFactorMilli = + StorageMap<_, Identity, NetUid, u32, ValueQuery, DefaultActivityCutoffFactorMilli>; + /// ============================ /// ==== Subnet Parameters ===== /// ============================ @@ -1949,6 +1996,7 @@ pub mod pallet { StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultImmunityPeriod>; /// --- MAP ( netuid ) --> activity_cutoff + // #[deprecated(note = "Replaced by `ActivityCutoffFactorMilli` (per-mille of `Tempo`).")] #[pallet::storage] pub type ActivityCutoff = StorageMap<_, Identity, NetUid, u16, ValueQuery, DefaultActivityCutoff>; @@ -2377,7 +2425,8 @@ pub mod pallet { #[pallet::storage] pub type StakeThreshold = StorageValue<_, u64, ValueQuery, DefaultStakeThreshold>; - /// --- MAP (netuid, who) --> VecDeque<(hash, commit_block, first_reveal_block, last_reveal_block)> | Stores a queue of commits for an account on a given netuid. + /// --- MAP (netuid, who) --> VecDeque<(hash, commit_epoch, commit_block, _unused)> + /// Stores a queue of commit-reveal-v2 commits for an account on a given netuid. #[pallet::storage] pub type WeightCommits = StorageDoubleMap< _, @@ -2459,20 +2508,6 @@ pub mod pallet { OptionQuery, >; - /// DMAP ( hot, cold, netuid ) --> rate limits for staking operations - /// Value contains just a marker: we use this map as a set. - #[pallet::storage] - pub type StakingOperationRateLimiter = StorageNMap< - _, - ( - NMapKey, // hot - NMapKey, // cold - NMapKey, // subnet - ), - bool, - ValueQuery, - >; - #[pallet::storage] // --- MAP(netuid ) --> Root claim threshold pub type RootClaimableThreshold = StorageMap<_, Blake2_128Concat, NetUid, I96F32, ValueQuery, DefaultMinRootClaimAmount>; @@ -2745,7 +2780,7 @@ pub struct TaoBalanceReserve(PhantomData); impl TokenReserve for TaoBalanceReserve { #![deny(clippy::expect_used)] fn reserve(netuid: NetUid) -> TaoBalance { - SubnetTAO::::get(netuid).saturating_add(SubnetTaoProvided::::get(netuid)) + SubnetTAO::::get(netuid) } fn increase_provided(netuid: NetUid, tao: TaoBalance) { @@ -2763,7 +2798,7 @@ pub struct AlphaBalanceReserve(PhantomData); impl TokenReserve for AlphaBalanceReserve { #![deny(clippy::expect_used)] fn reserve(netuid: NetUid) -> AlphaBalance { - SubnetAlphaIn::::get(netuid).saturating_add(SubnetAlphaInProvided::::get(netuid)) + SubnetAlphaIn::::get(netuid) } fn increase_provided(netuid: NetUid, alpha: AlphaBalance) { diff --git a/pallets/subtensor/src/macros/config.rs b/pallets/subtensor/src/macros/config.rs index 7b94866b52..553451ab12 100644 --- a/pallets/subtensor/src/macros/config.rs +++ b/pallets/subtensor/src/macros/config.rs @@ -120,6 +120,18 @@ mod config { /// Max burn lower bound. #[pallet::constant] type MaxBurnLowerBound: Get; + /// Lower bound for owner-set tempo. + #[pallet::constant] + type MinTempo: Get; + /// Upper bound for owner-set tempo. + #[pallet::constant] + type MaxTempo: Get; + /// Lower bound for the activity-cutoff factor (per-mille). + #[pallet::constant] + type MinActivityCutoffFactorMilli: Get; + /// Upper bound for the activity-cutoff factor (per-mille). + #[pallet::constant] + type MaxActivityCutoffFactorMilli: Get; /// Initial adjustment interval. #[pallet::constant] type InitialAdjustmentInterval: Get; @@ -270,5 +282,9 @@ mod config { /// Burn account ID #[pallet::constant] type BurnAccountId: Get; + /// Per-block cap on number of subnet epochs that may execute in a single + /// `block_step`; the rest are deferred 1 block forward via `PendingEpochAt`. + #[pallet::constant] + type MaxEpochsPerBlock: Get; } } diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index b471328aec..2a22915ee9 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -1823,7 +1823,7 @@ mod dispatches { /// May emit a `EvmKeyAssociated` event on success #[pallet::call_index(93)] #[pallet::weight(( - Weight::from_parts(3_000_000, 0).saturating_add(T::DbWeight::get().reads_writes(2, 1)), + ::WeightInfo::associate_evm_key(), DispatchClass::Normal, Pays::No ))] @@ -2593,5 +2593,37 @@ mod dispatches { let coldkey = ensure_signed(origin)?; Self::do_set_perpetual_lock(&coldkey, netuid, enabled) } + + /// Owner-side `set_tempo`. Validates `[MinTempo, MaxTempo]`, applies a fixed + /// `MinTempo`-block cooldown via `TransactionType::TempoUpdate`, respects the admin + /// freeze window, and resets the cycle (`LastEpochBlock = current_block`) on success. + #[pallet::call_index(139)] + #[pallet::weight(::WeightInfo::set_tempo())] + pub fn set_tempo(origin: OriginFor, netuid: NetUid, tempo: u16) -> DispatchResult { + Self::do_set_tempo(origin, netuid, tempo) + } + + /// `set_activity_cutoff_factor`. Per-mille (1/1000) units; `cutoff_blocks + /// = (factor × tempo) / 1000`. Validates `[MinActivityCutoffFactorMilli, + /// MaxActivityCutoffFactorMilli]`. Callable by the subnet owner (rate-limited + /// via `OwnerHyperparamUpdate`, respects the admin freeze window) or by root + /// (bypasses both). + #[pallet::call_index(140)] + #[pallet::weight(::WeightInfo::set_activity_cutoff_factor())] + pub fn set_activity_cutoff_factor( + origin: OriginFor, + netuid: NetUid, + factor_milli: u32, + ) -> DispatchResult { + Self::do_set_activity_cutoff_factor(origin, netuid, factor_milli) + } + + /// Owner-side `trigger_epoch`. Schedules an epoch to fire after `AdminFreezeWindow` + /// blocks. Rate-limited via the existing `OwnerHyperparamUpdate` pattern. + #[pallet::call_index(141)] + #[pallet::weight(::WeightInfo::trigger_epoch())] + pub fn trigger_epoch(origin: OriginFor, netuid: NetUid) -> DispatchResult { + Self::do_trigger_epoch(origin, netuid) + } } } diff --git a/pallets/subtensor/src/macros/errors.rs b/pallets/subtensor/src/macros/errors.rs index cb120b56b5..7f5b119d31 100644 --- a/pallets/subtensor/src/macros/errors.rs +++ b/pallets/subtensor/src/macros/errors.rs @@ -213,14 +213,10 @@ mod errors { SubtokenDisabled, /// Too frequent hotkey swap on subnet HotKeySwapOnSubnetIntervalNotPassed, - /// Zero max stake amount - ZeroMaxStakeAmount, /// Invalid netuid duplication SameNetuid, /// The caller does not have enough balance for the operation. InsufficientBalance, - /// Too frequent staking operations - StakingOperationRateLimitExceeded, /// Invalid lease beneficiary to register the leased network. InvalidLeaseBeneficiary, /// Lease cannot end in the past. @@ -305,5 +301,19 @@ mod errors { CannotUseSystemAccount, /// Trying to unlock more than locked UnlockAmountTooHigh, + /// The supplied tempo is outside the allowed range. + TempoOutOfBounds, + /// The supplied activity-cutoff factor is outside the allowed range. + ActivityCutoffFactorMilliOutOfBounds, + /// An epoch trigger is already pending for this subnet; wait for it to fire + /// before triggering again. + EpochTriggerAlreadyPending, + /// The next automatic epoch is already imminent; a manual trigger would have + /// no effect. + AutoEpochAlreadyImminent, + /// `trigger_epoch` is blocked because commit-reveal is enabled for this subnet: + /// an out-of-band epoch would desync the CRv3 reveal window from the wall-clock + /// Drand schedule and silently drop committed weights. + DynamicTempoBlockedByCommitReveal, } } diff --git a/pallets/subtensor/src/macros/events.rs b/pallets/subtensor/src/macros/events.rs index 918baf1107..b5bea2c186 100644 --- a/pallets/subtensor/src/macros/events.rs +++ b/pallets/subtensor/src/macros/events.rs @@ -612,6 +612,42 @@ mod events { netuid: NetUid, }, + /// Activity-cutoff factor (per-mille) set on a subnet by its owner. + ActivityCutoffFactorMilliSet { + /// The subnet identifier. + netuid: NetUid, + /// Factor (per-mille). + factor_milli: u32, + }, + + /// Owner manually triggered an epoch for their subnet. + EpochTriggered { + /// The subnet identifier. + netuid: NetUid, + /// The account that triggered the epoch. + by: T::AccountId, + /// The earliest block at which the triggered epoch may execute. + fires_at: u64, + }, + + /// An epoch slot was deferred to the next block due to the per-block epoch cap. + EpochDeferred { + /// The subnet identifier. + netuid: NetUid, + /// Block at which the epoch was originally scheduled. + from_block: u64, + /// Block to which the epoch was deferred. + to_block: u64, + }, + + /// Epoch execution skipped by `is_epoch_input_state_consistent` returned false or other errors. + EpochSkipped { + /// The subnet identifier. + netuid: NetUid, + /// The block at which the slot was consumed. + block: u64, + }, + /// Subnet ownership was reassigned by lock conviction. SubnetOwnerChanged { /// The subnet whose owner changed. diff --git a/pallets/subtensor/src/macros/hooks.rs b/pallets/subtensor/src/macros/hooks.rs index a950c6c97a..6371f30e46 100644 --- a/pallets/subtensor/src/macros/hooks.rs +++ b/pallets/subtensor/src/macros/hooks.rs @@ -38,17 +38,6 @@ mod hooks { } } - // ---- Called on the finalization of this pallet. The code weight must be taken into account prior to the execution of this macro. - // - // # Args: - // * 'n': (BlockNumberFor): - // - The number of the block we are finalizing. - fn on_finalize(_block_number: BlockNumberFor) { - for _ in StakingOperationRateLimiter::::drain() { - // Clear all entries each block - } - } - fn on_runtime_upgrade() -> frame_support::weights::Weight { // --- Migrate storage let mut weight = frame_support::weights::Weight::from_parts(0, 0); @@ -181,6 +170,10 @@ mod hooks { .saturating_add(migrations::migrate_remove_deprecated_conviction_maps::migrate_remove_deprecated_conviction_maps::()) // Reset testnet conviction lock storage before deploying the current design. .saturating_add(migrations::migrate_reset_tnet_conviction_locks::migrate_reset_tnet_conviction_locks::()) + // Seed LastEpochBlock for dynamic-tempo / owner-triggered-epochs feature + .saturating_add(migrations::migrate_dynamic_tempo::migrate_dynamic_tempo::()) + // Populate locking reverse map. + .saturating_add(migrations::migrate_populate_locking_coldkeys::migrate_populate_locking_coldkeys::()) // Capture the runtime-upgrade block for TAO-in refund cutover. .saturating_add(migrations::migrate_tao_in_refund_deployment_block::migrate_tao_in_refund_deployment_block::()) // Fix lock state left behind by subnet-scoped hotkey swaps. diff --git a/pallets/subtensor/src/migrations/migrate_cleanup_swap_v3.rs b/pallets/subtensor/src/migrations/migrate_cleanup_swap_v3.rs new file mode 100644 index 0000000000..cebbf373ec --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_cleanup_swap_v3.rs @@ -0,0 +1,70 @@ +use super::*; +use crate::HasMigrationRun; +use frame_support::{storage_alias, traits::Get, weights::Weight}; +use scale_info::prelude::string::String; + +pub mod deprecated_swap_maps { + use super::*; + + /// --- MAP ( netuid ) --> tao_in_user_subnet | Returns the amount of TAO in the subnet reserve provided by users as liquidity. + #[storage_alias] + pub type SubnetTaoProvided = + StorageMap, Identity, NetUid, TaoBalance, ValueQuery>; + + /// --- MAP ( netuid ) --> alpha_supply_user_in_pool | Returns the amount of alpha in the pool provided by users as liquidity. + #[storage_alias] + pub type SubnetAlphaInProvided = + StorageMap, Identity, NetUid, AlphaBalance, ValueQuery>; +} + +pub fn migrate_cleanup_swap_v3() -> Weight { + let migration_name = b"migrate_cleanup_swap_v3".to_vec(); + let mut weight = T::DbWeight::get().reads(1); + + if HasMigrationRun::::get(&migration_name) { + log::info!( + "Migration '{:?}' has already run. Skipping.", + String::from_utf8_lossy(&migration_name) + ); + return weight; + } + + log::info!( + "Running migration '{}'", + String::from_utf8_lossy(&migration_name), + ); + + // ------------------------------ + // Step 1: Move provided to reserves + // ------------------------------ + for (netuid, tao_provided) in deprecated_swap_maps::SubnetTaoProvided::::iter() { + SubnetTAO::::mutate(netuid, |total| { + *total = total.saturating_add(tao_provided); + }); + } + for (netuid, alpha_provided) in deprecated_swap_maps::SubnetAlphaInProvided::::iter() { + SubnetAlphaIn::::mutate(netuid, |total| { + *total = total.saturating_add(alpha_provided); + }); + } + + // ------------------------------ + // Step 2: Remove Map entries + // ------------------------------ + remove_prefix::("SubtensorModule", "SubnetTaoProvided", &mut weight); + remove_prefix::("SubtensorModule", "SubnetAlphaInProvided", &mut weight); + + // ------------------------------ + // Step 3: Mark Migration as Completed + // ------------------------------ + + HasMigrationRun::::insert(&migration_name, true); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + log::info!( + "Migration '{:?}' completed successfully.", + String::from_utf8_lossy(&migration_name) + ); + + weight +} diff --git a/pallets/subtensor/src/migrations/migrate_dynamic_tempo.rs b/pallets/subtensor/src/migrations/migrate_dynamic_tempo.rs new file mode 100644 index 0000000000..c359b96c2f --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_dynamic_tempo.rs @@ -0,0 +1,163 @@ +use super::*; +use frame_support::{traits::Get, weights::Weight}; +use log; +use scale_info::prelude::string::String; +use sp_core::H256; +use sp_std::collections::vec_deque::VecDeque; + +/// One-shot migration for the dynamic-tempo / owner-triggered-epochs feature. +/// +/// 1. Back-fills `LastEpochBlock[netuid]` for every existing subnet so the first +/// post-upgrade epoch lands on the same block as the legacy modulo formula +/// `(block + netuid + 1) % (tempo + 1) == 0`. The new scheduler period is +/// `tempo` (next firing at `LastEpochBlock + tempo`). +/// Existing `Tempo[netuid]` values are preserved as-is regardless of whether +/// they fall inside `[MIN_TEMPO, MAX_TEMPO]`. Owner-side `set_tempo` enforces +/// the bounds for new updates; root-side `sudo_set_tempo` can still write any +/// `u16`. Subnets with `Tempo == 0` are left as-is — the legacy short-circuit +/// keeps them dormant and matches their pre-upgrade behaviour. +/// 2. Converts each subnet's existing `ActivityCutoff[netuid]` (absolute block count) +/// into `ActivityCutoffFactorMilli[netuid]` (per-mille of `tempo`) so that +/// `factor * tempo / 1000 ≈ old_cutoff` post-upgrade. Production defaults +/// (`tempo=360`, `cutoff=5000`) round-trip to 5000 blocks exactly via ceiling +/// division. Out-of-range factors are clamped to +/// `[MIN_ACTIVITY_CUTOFF_FACTOR_MILLI, MAX_ACTIVITY_CUTOFF_FACTOR_MILLI]` — +/// extreme historical cutoffs may shift to the nearest representable factor. +/// 3. Seeds `SubnetEpochIndex[netuid]` (the new stateful epoch counter) with the +/// legacy modulo epoch index `(block + netuid + 1) / (tempo + 1)` so that +/// existing commit-reveal commit keys — `TimelockedWeightCommits` (CR-v4) keyed +/// by epoch, and `WeightCommits` (CR-v2) tagged with `commit_epoch` — stay +/// valid and continuous across the upgrade. +/// 4. Rewrites every CR-v2 `WeightCommits` entry to `(hash, commit_epoch, +/// commit_block, _)`: field 1 (previously the absolute `commit_block`) becomes +/// `commit_epoch` under the legacy modulo formula; field 2 keeps the absolute +/// `commit_block` (used by the epoch's commit-reveal weight column-mask). +pub fn migrate_dynamic_tempo() -> Weight { + let mig_name: Vec = b"dynamic_tempo_v1".to_vec(); + let mig_name_str = String::from_utf8_lossy(&mig_name); + + let mut total_weight = T::DbWeight::get().reads(1); + + if HasMigrationRun::::get(&mig_name) { + log::info!("Migration '{mig_name_str}' already executed - skipping"); + return total_weight; + } + + log::info!("Running migration '{mig_name_str}'"); + + let current_block = Pallet::::get_current_block_as_u64(); + let mut visited: u64 = 0; + let mut last_epoch_seeded: u64 = 0; + let mut epoch_index_seeded: u64 = 0; + let mut activity_factor_seeded: u64 = 0; + let mut activity_factor_clamped: u64 = 0; + let mut crv2_commits_converted: u64 = 0; + let mut reads: u64 = 0; + let mut writes: u64 = 0; + + let netuids: Vec = Tempo::::iter_keys().collect(); + reads = reads.saturating_add(netuids.len() as u64); + + for netuid in netuids.into_iter() { + visited = visited.saturating_add(1); + let tempo = Tempo::::get(netuid); + reads = reads.saturating_add(1); + + if tempo == 0 { + // Legacy `tempo == 0` short-circuit preserved; do not seed `LastEpochBlock`. + continue; + } + + // Compute next-epoch block under the *legacy* modulo formula and back-fill + // `LastEpochBlock` so the *new* scheduler fires its first epoch on the same + // block the legacy chain would have. + // Legacy `blocks_until_next_epoch` (pre-upgrade behaviour, period `tempo + 1`): + // adjusted = current_block + netuid + 1 + // remainder = adjusted % (tempo + 1) + // blocks_until_next = tempo - remainder + // New scheduler period is `tempo`, next firing at `LastEpochBlock + tempo`. + // Solve for `LastEpochBlock`: + // LastEpochBlock = current_block + blocks_until_next - tempo + // = current_block - (tempo - blocks_until_next) + let netuid_plus_one = (u16::from(netuid) as u64).saturating_add(1); + let tempo_plus_one = (tempo as u64).saturating_add(1); + let adjusted = current_block.wrapping_add(netuid_plus_one); + let remainder = adjusted.checked_rem(tempo_plus_one).unwrap_or(0); + let blocks_until_next = (tempo as u64).saturating_sub(remainder); + let offset = (tempo as u64).saturating_sub(blocks_until_next); + let last_epoch = current_block.saturating_sub(offset); + + LastEpochBlock::::insert(netuid, last_epoch); + last_epoch_seeded = last_epoch_seeded.saturating_add(1); + writes = writes.saturating_add(1); + + // Seed the stateful epoch counter with the legacy modulo epoch index + // `(current_block + netuid + 1) / (tempo + 1)` so CR commit keys + // (TimelockedWeightCommits epoch keys, WeightCommits commit_epoch) stay + // continuous across the upgrade. + let legacy_epoch = adjusted.checked_div(tempo_plus_one).unwrap_or(0); + SubnetEpochIndex::::insert(netuid, legacy_epoch); + epoch_index_seeded = epoch_index_seeded.saturating_add(1); + writes = writes.saturating_add(1); + + // Convert legacy absolute `ActivityCutoff` into per-mille `ActivityCutoffFactorMilli` + let old_cutoff = ActivityCutoff::::get(netuid) as u64; + reads = reads.saturating_add(1); + let tempo_u64 = tempo as u64; + let raw_factor = old_cutoff + .saturating_mul(1_000) + .saturating_add(tempo_u64.saturating_sub(1)) + .checked_div(tempo_u64) + .unwrap_or(INITIAL_ACTIVITY_CUTOFF_FACTOR_MILLI as u64); + let clamped = raw_factor + .max(MIN_ACTIVITY_CUTOFF_FACTOR_MILLI as u64) + .min(MAX_ACTIVITY_CUTOFF_FACTOR_MILLI as u64) as u32; + if clamped as u64 != raw_factor { + activity_factor_clamped = activity_factor_clamped.saturating_add(1); + } + ActivityCutoffFactorMilli::::insert(netuid, clamped); + activity_factor_seeded = activity_factor_seeded.saturating_add(1); + writes = writes.saturating_add(1); + } + + // --- CR-v2: rewrite every `WeightCommits` entry to the + // `(hash, commit_epoch, commit_block, _)` layout. Field 1 was the absolute + // `commit_block`; it becomes `commit_epoch` (legacy modulo epoch). Field 2 + // keeps the absolute `commit_block` (used by the epoch column-mask). + let crv2_entries: Vec<_> = WeightCommits::::iter().collect(); + reads = reads.saturating_add(crv2_entries.len() as u64); + for (netuid_index, account, commits) in crv2_entries.into_iter() { + let (netuid, _) = Pallet::::get_netuid_and_subid(netuid_index).unwrap_or_default(); + let tempo = Tempo::::get(netuid); + reads = reads.saturating_add(1); + let tempo_plus_one = (tempo as u64).saturating_add(1); + let netuid_plus_one = (u16::from(netuid) as u64).saturating_add(1); + + let converted: VecDeque<(H256, u64, u64, u64)> = commits + .into_iter() + .map(|(hash, commit_block, _, _)| { + let commit_epoch = commit_block + .saturating_add(netuid_plus_one) + .checked_div(tempo_plus_one) + .unwrap_or(0); + (hash, commit_epoch, commit_block, 0u64) + }) + .collect(); + WeightCommits::::insert(netuid_index, account, converted); + crv2_commits_converted = crv2_commits_converted.saturating_add(1); + writes = writes.saturating_add(1); + } + + total_weight = total_weight.saturating_add(T::DbWeight::get().reads_writes(reads, writes)); + + log::info!( + "Dynamic tempo migration: visited={visited}, last_epoch_seeded={last_epoch_seeded}, epoch_index_seeded={epoch_index_seeded}, activity_factor_seeded={activity_factor_seeded}, activity_factor_clamped={activity_factor_clamped}, crv2_commits_converted={crv2_commits_converted}" + ); + + HasMigrationRun::::insert(&mig_name, true); + total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1)); + + log::info!("Migration '{mig_name_str}' completed"); + + total_weight +} diff --git a/pallets/subtensor/src/migrations/migrate_populate_locking_coldkeys.rs b/pallets/subtensor/src/migrations/migrate_populate_locking_coldkeys.rs new file mode 100644 index 0000000000..c1220c2077 --- /dev/null +++ b/pallets/subtensor/src/migrations/migrate_populate_locking_coldkeys.rs @@ -0,0 +1,71 @@ +use alloc::string::String; +use frame_support::{traits::Get, weights::Weight}; + +use crate::{Config, HasMigrationRun, Lock, Pallet as Subtensor}; + +const MIGRATION_NAME: &[u8] = b"migrate_populate_locking_coldkeys"; + +pub fn migrate_populate_locking_coldkeys() -> Weight { + let mut weight = T::DbWeight::get().reads(1); + + if HasMigrationRun::::get(MIGRATION_NAME) { + log::info!( + "Migration '{}' already executed - skipping", + String::from_utf8_lossy(MIGRATION_NAME) + ); + return weight; + } + + log::info!( + "Running migration '{}'", + String::from_utf8_lossy(MIGRATION_NAME) + ); + + let now = Subtensor::::get_current_block_as_u64(); + let unlock_rate = crate::UnlockRate::::get(); + let maturity_rate = crate::MaturityRate::::get(); + let mut scanned_count = 0u64; + let mut indexed_count = 0u64; + let mut removed_count = 0u64; + let lock_keys = Lock::::iter_keys().collect::>(); + + // Note for AI reviewer: Lock has very few records at the time of this migration, + // so iterating it once here is safe and keeps the migration simple. + for (coldkey, netuid, hotkey) in lock_keys { + scanned_count = scanned_count.saturating_add(1); + + let mut model = + Subtensor::::read_conviction_model_for_hotkey(&coldkey, netuid, &hotkey, now); + model.roll_forward(now, unlock_rate, maturity_rate); + + if model.individual_lock().is_zero() { + removed_count = removed_count.saturating_add(1); + } else { + indexed_count = indexed_count.saturating_add(1); + } + + Subtensor::::save_conviction_model(&coldkey, netuid, &hotkey, model); + } + + weight = weight.saturating_add(T::DbWeight::get().reads(scanned_count)); + weight = weight.saturating_add( + T::DbWeight::get().writes( + indexed_count + .saturating_mul(2) + .saturating_add(removed_count.saturating_mul(3)), + ), + ); + + HasMigrationRun::::insert(MIGRATION_NAME, true); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + + log::info!( + "Migration '{}' completed. scanned_entries={}, indexed_entries={}, removed_zero_entries={}", + String::from_utf8_lossy(MIGRATION_NAME), + scanned_count, + indexed_count, + removed_count + ); + + weight +} diff --git a/pallets/subtensor/src/migrations/mod.rs b/pallets/subtensor/src/migrations/mod.rs index b775cfaa49..e846d325dc 100644 --- a/pallets/subtensor/src/migrations/mod.rs +++ b/pallets/subtensor/src/migrations/mod.rs @@ -5,6 +5,7 @@ use sp_io::KillStorageResult; use sp_io::hashing::twox_128; use sp_io::storage::clear_prefix; pub mod migrate_auto_stake_destination; +pub mod migrate_cleanup_swap_v3; pub mod migrate_clear_deprecated_registration_maps; pub mod migrate_coldkey_swap_scheduled; pub mod migrate_coldkey_swap_scheduled_to_announcements; @@ -16,6 +17,7 @@ pub mod migrate_crv3_v2_to_timelocked; pub mod migrate_delete_subnet_21; pub mod migrate_delete_subnet_3; pub mod migrate_disable_commit_reveal; +pub mod migrate_dynamic_tempo; pub mod migrate_fix_bad_hk_swap; pub mod migrate_fix_childkeys; pub mod migrate_fix_is_network_member; @@ -33,6 +35,7 @@ pub mod migrate_network_lock_cost_2500; pub mod migrate_network_lock_reduction_interval; pub mod migrate_orphaned_storage_items; pub mod migrate_pending_emissions; +pub mod migrate_populate_locking_coldkeys; pub mod migrate_populate_owned_hotkeys; pub mod migrate_rao; pub mod migrate_rate_limit_keys; diff --git a/pallets/subtensor/src/rpc_info/metagraph.rs b/pallets/subtensor/src/rpc_info/metagraph.rs index ec61f2e596..2dbaa883d9 100644 --- a/pallets/subtensor/src/rpc_info/metagraph.rs +++ b/pallets/subtensor/src/rpc_info/metagraph.rs @@ -10,7 +10,7 @@ use substrate_fixed::types::I96F32; use subtensor_macros::freeze_struct; use subtensor_runtime_common::{AlphaBalance, MechId, NetUid, NetUidStorageIndex, TaoBalance}; -#[freeze_struct("fbab6d1e7f3c69ae")] +#[freeze_struct("54520f5534d7e59e")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct Metagraph { // Subnet index @@ -54,7 +54,7 @@ pub struct Metagraph { max_weights_limit: Compact, // max allowed weights per val weights_version: Compact, // allowed weights version weights_rate_limit: Compact, // rate limit on weights - activity_cutoff: Compact, // validator weights cut off period in blocks + activity_cutoff: Compact, // validator weights cut off period in blocks max_validators: Compact, // max allowed validators // Registration @@ -110,7 +110,7 @@ pub struct Metagraph { alpha_dividends_per_hotkey: Vec<(AccountId, Compact)>, // List of dividend payout in alpha via subnet. } -#[freeze_struct("3ff2befdb7b393ea")] +#[freeze_struct("5f9c8beab622882c")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct SelectiveMetagraph { // Subnet index @@ -154,8 +154,8 @@ pub struct SelectiveMetagraph { max_weights_limit: Option>, // max allowed weights per val weights_version: Option>, // allowed weights version weights_rate_limit: Option>, // rate limit on weights - activity_cutoff: Option>, // validator weights cut off period in blocks - max_validators: Option>, // max allowed validators + activity_cutoff: Option>, // validator weights cut off period in blocks (effective = factor × tempo / 1000) + max_validators: Option>, // max allowed validators // Registration num_uids: Option>, @@ -710,7 +710,7 @@ impl Pallet { max_weights_limit: Self::get_max_weight_limit(netuid).into(), // max allowed weight weights_version: Self::get_weights_version_key(netuid).into(), // allowed weights version weights_rate_limit: Self::get_weights_set_rate_limit(netuid).into(), // rate limit on weights. - activity_cutoff: Self::get_activity_cutoff(netuid).into(), // validator weights cut off period in blocks + activity_cutoff: Self::get_activity_cutoff_blocks(netuid).into(), // validator weights cut off period in blocks max_validators: Self::get_max_allowed_validators(netuid).into(), // max allowed validators. // Registration @@ -1051,7 +1051,7 @@ impl Pallet { }, Some(SelectiveMetagraphIndex::ActivityCutoff) => SelectiveMetagraph { netuid: netuid.into(), - activity_cutoff: Some(Self::get_activity_cutoff(netuid).into()), + activity_cutoff: Some(Self::get_activity_cutoff_blocks(netuid).into()), ..Default::default() }, Some(SelectiveMetagraphIndex::MaxValidators) => SelectiveMetagraph { diff --git a/pallets/subtensor/src/rpc_info/stake_info.rs b/pallets/subtensor/src/rpc_info/stake_info.rs index 545edf6db6..2d3316f34d 100644 --- a/pallets/subtensor/src/rpc_info/stake_info.rs +++ b/pallets/subtensor/src/rpc_info/stake_info.rs @@ -2,6 +2,7 @@ extern crate alloc; use codec::Compact; use frame_support::pallet_prelude::{Decode, Encode}; +use sp_std::collections::btree_map::BTreeMap; use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; use subtensor_swap_interface::SwapHandler; @@ -21,6 +22,29 @@ pub struct StakeInfo { is_registered: bool, } +#[freeze_struct("2d52e2de04425fb6")] +#[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] +pub struct StakeAvailability { + total: Compact, + locked: Compact, + available: Compact, +} + +// Per-subnet stake breakdown: total alpha, locked mass, and what is free to unstake. +impl StakeAvailability { + pub fn total(&self) -> AlphaBalance { + self.total.into() + } + + pub fn locked(&self) -> AlphaBalance { + self.locked.into() + } + + pub fn available(&self) -> AlphaBalance { + self.available.into() + } +} + impl Pallet { fn _get_stake_info_for_coldkeys( coldkeys: Vec, @@ -119,6 +143,69 @@ impl Pallet { }) } + /// Batch query of unstakable stake per coldkey and subnet. + /// + /// `netuids: None` scans every subnet; `Some(vec)` limits the scan. + /// Subnets with zero stake and zero lock are left out of the response. + /// + /// Invalid `Some(vec)` requests (empty or longer than the number of subnets on chain) + /// return each coldkey with an empty inner map. Non-existent netuids are omitted. + pub fn get_stake_availability_for_coldkeys( + coldkey_accounts: Vec, + netuids: Option>, + ) -> BTreeMap> { + if coldkey_accounts.is_empty() { + return BTreeMap::new(); + } + + let existing_netuids = Self::get_all_subnet_netuids(); + + let netuids = match netuids { + None => existing_netuids, + Some(mut requested) => { + // Same netuid may appear more than once in the request — keep one row per subnet. + requested.sort(); + requested.dedup(); + if requested.is_empty() || requested.len() > existing_netuids.len() { + return coldkey_accounts + .into_iter() + .map(|coldkey| (coldkey, BTreeMap::new())) + .collect(); + } + requested.retain(|n| Self::if_subnet_exist(*n)); + requested + } + }; + + coldkey_accounts + .into_iter() + .map(|coldkey| { + let availability: BTreeMap = netuids + .iter() + .filter_map(|netuid| { + let (total, locked, available) = + Self::stake_availability(&coldkey, *netuid); + // Nothing staked and no active lock — skip this subnet. + if total.is_zero() && locked.is_zero() { + None + } else { + Some(( + *netuid, + StakeAvailability { + total: total.into(), + locked: locked.into(), + available: available.into(), + }, + )) + } + }) + .collect(); + + (coldkey, availability) + }) + .collect() + } + pub fn get_stake_fee( origin: Option<(T::AccountId, NetUid)>, _origin_coldkey_account: T::AccountId, diff --git a/pallets/subtensor/src/rpc_info/subnet_info.rs b/pallets/subtensor/src/rpc_info/subnet_info.rs index 7446e4f6fc..aab27c19c6 100644 --- a/pallets/subtensor/src/rpc_info/subnet_info.rs +++ b/pallets/subtensor/src/rpc_info/subnet_info.rs @@ -53,7 +53,7 @@ pub struct SubnetInfov2 { identity: Option, } -#[freeze_struct("fd2db338b156d251")] +#[freeze_struct("5a0830a4518a7325")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct SubnetHyperparams { rho: Compact, @@ -67,7 +67,7 @@ pub struct SubnetHyperparams { weights_version: Compact, weights_rate_limit: Compact, adjustment_interval: Compact, - activity_cutoff: Compact, + activity_cutoff: Compact, pub registration_allowed: bool, target_regs_per_interval: Compact, min_burn: Compact, @@ -85,7 +85,7 @@ pub struct SubnetHyperparams { liquid_alpha_enabled: bool, } -#[freeze_struct("bb4666554020e789")] +#[freeze_struct("336a6658e70b5554")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct SubnetHyperparamsV2 { rho: Compact, @@ -99,7 +99,7 @@ pub struct SubnetHyperparamsV2 { weights_version: Compact, weights_rate_limit: Compact, adjustment_interval: Compact, - activity_cutoff: Compact, + activity_cutoff: Compact, pub registration_allowed: bool, target_regs_per_interval: Compact, min_burn: Compact, @@ -328,7 +328,7 @@ impl Pallet { let weights_version = Self::get_weights_version_key(netuid); let weights_rate_limit = Self::get_weights_set_rate_limit(netuid); let adjustment_interval = Self::get_adjustment_interval(netuid); - let activity_cutoff = Self::get_activity_cutoff(netuid); + let activity_cutoff = Self::get_activity_cutoff_blocks(netuid); let registration_allowed = Self::get_network_registration_allowed(netuid); let target_regs_per_interval = Self::get_target_registrations_per_interval(netuid); let min_burn = Self::get_min_burn(netuid); @@ -391,7 +391,7 @@ impl Pallet { let weights_version = Self::get_weights_version_key(netuid); let weights_rate_limit = Self::get_weights_set_rate_limit(netuid); let adjustment_interval = Self::get_adjustment_interval(netuid); - let activity_cutoff = Self::get_activity_cutoff(netuid); + let activity_cutoff = Self::get_activity_cutoff_blocks(netuid); let registration_allowed = Self::get_network_registration_allowed(netuid); let target_regs_per_interval = Self::get_target_registrations_per_interval(netuid); let min_burn = Self::get_min_burn(netuid); @@ -414,7 +414,6 @@ impl Pallet { let subnet_token_enabled = Self::get_subtoken_enabled(netuid); let transfers_enabled = Self::get_transfer_toggle(netuid); let bonds_reset = Self::get_bonds_reset(netuid); - let user_liquidity_enabled: bool = Self::is_user_liquidity_enabled(netuid); Some(SubnetHyperparamsV2 { rho: rho.into(), @@ -449,7 +448,7 @@ impl Pallet { subnet_is_active: subnet_token_enabled, transfers_enabled, bonds_reset_enabled: bonds_reset, - user_liquidity_enabled, + user_liquidity_enabled: false, }) } @@ -516,7 +515,12 @@ impl Pallet { .into(), ( "activity_cutoff", - HyperparamValue::U16(Self::get_activity_cutoff(netuid).into()), + HyperparamValue::U64(Self::get_activity_cutoff_blocks(netuid).into()), + ) + .into(), + ( + "activity_cutoff_factor", + HyperparamValue::U32(Self::get_activity_cutoff_factor_milli(netuid).into()), ) .into(), ( @@ -607,11 +611,7 @@ impl Pallet { HyperparamValue::Bool(Self::get_bonds_reset(netuid)), ) .into(), - ( - "user_liquidity_enabled", - HyperparamValue::Bool(Self::is_user_liquidity_enabled(netuid)), - ) - .into(), + ("user_liquidity_enabled", HyperparamValue::Bool(false),).into(), ( "owner_cut_enabled", HyperparamValue::Bool(Self::get_owner_cut_enabled(netuid)), @@ -622,6 +622,11 @@ impl Pallet { HyperparamValue::Bool(Self::get_owner_cut_auto_lock_enabled(netuid)), ) .into(), + ( + "min_childkey_take", + HyperparamValue::U16(Self::get_effective_min_childkey_take(netuid).into()), + ) + .into(), ]) } } diff --git a/pallets/subtensor/src/staking/add_stake.rs b/pallets/subtensor/src/staking/add_stake.rs index b88e75cd31..33cadf241b 100644 --- a/pallets/subtensor/src/staking/add_stake.rs +++ b/pallets/subtensor/src/staking/add_stake.rs @@ -66,7 +66,6 @@ impl Pallet { netuid, stake_to_be_added, T::SwapInterface::max_price(), - true, false, ) } @@ -155,7 +154,6 @@ impl Pallet { netuid, possible_stake, limit_price, - true, false, ) } @@ -172,7 +170,8 @@ impl Pallet { if limit_price >= 1_000_000_000.into() { return Ok(u64::MAX); } else { - return Err(Error::::ZeroMaxStakeAmount.into()); + // Price will never move down, so maximum amount that can be staked is zero + return Ok(0_u64); } } @@ -181,10 +180,6 @@ impl Pallet { let result = T::SwapInterface::swap(netuid.into(), order, limit_price, false, true) .map(|r| r.amount_paid_in.saturating_add(r.fee_paid))?; - if !result.is_zero() { - Ok(result.into()) - } else { - Err(Error::::ZeroMaxStakeAmount.into()) - } + Ok(result.into()) } } diff --git a/pallets/subtensor/src/staking/helpers.rs b/pallets/subtensor/src/staking/helpers.rs index 70e7f2ae57..f11012f0e2 100644 --- a/pallets/subtensor/src/staking/helpers.rs +++ b/pallets/subtensor/src/staking/helpers.rs @@ -1,7 +1,7 @@ use alloc::collections::BTreeMap; use safe_math::*; use share_pool::SafeFloat; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::{U64F64, U96F32}; use subtensor_runtime_common::{NetUid, TaoBalance}; use subtensor_swap_interface::{Order, SwapHandler}; @@ -43,15 +43,13 @@ impl Pallet { Self::get_all_subnet_netuids() .into_iter() .map(|netuid| { - let alpha = U96F32::saturating_from_num(Self::get_stake_for_hotkey_on_subnet( + let alpha = U64F64::saturating_from_num(Self::get_stake_for_hotkey_on_subnet( hotkey, netuid, )); - let alpha_price = U96F32::saturating_from_num( - T::SwapInterface::current_alpha_price(netuid.into()), - ); + let alpha_price = T::SwapInterface::current_alpha_price(netuid.into()); alpha.saturating_mul(alpha_price) }) - .sum::() + .sum::() .saturating_to_num::() .into() } @@ -71,7 +69,7 @@ impl Pallet { let order = GetTaoForAlpha::::with_amount(alpha_stake); T::SwapInterface::sim_swap(netuid.into(), order) .map(|r| { - let fee: u64 = U96F32::saturating_from_num(r.fee_paid) + let fee: u64 = U64F64::saturating_from_num(r.fee_paid) .saturating_mul(T::SwapInterface::current_alpha_price( netuid.into(), )) @@ -105,7 +103,7 @@ impl Pallet { let order = GetTaoForAlpha::::with_amount(alpha_stake); T::SwapInterface::sim_swap(netuid.into(), order) .map(|r| { - let fee: u64 = U96F32::saturating_from_num(r.fee_paid) + let fee: u64 = U64F64::saturating_from_num(r.fee_paid) .saturating_mul(T::SwapInterface::current_alpha_price( netuid.into(), )) @@ -238,7 +236,7 @@ impl Pallet { let alpha_stake = Self::get_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid); let min_alpha_stake = - U96F32::saturating_from_num(Self::get_nominator_min_required_stake()) + U64F64::saturating_from_num(Self::get_nominator_min_required_stake()) .safe_div(T::SwapInterface::current_alpha_price(netuid)) .saturating_to_num::(); if alpha_stake > 0.into() && alpha_stake < min_alpha_stake.into() { @@ -283,10 +281,6 @@ impl Pallet { } } - pub fn is_user_liquidity_enabled(netuid: NetUid) -> bool { - T::SwapInterface::is_user_liquidity_enabled(netuid) - } - /// The function clears Alpha map in batches. Each run will check ALPHA_MAP_BATCH_SIZE /// alphas. It keeps the alpha value stored when it's >= than MIN_ALPHA. /// The function uses AlphaMapLastKey as a storage for key iterator between runs. diff --git a/pallets/subtensor/src/staking/lock.rs b/pallets/subtensor/src/staking/lock.rs index aa4a6508ab..e27f3e8d1e 100644 --- a/pallets/subtensor/src/staking/lock.rs +++ b/pallets/subtensor/src/staking/lock.rs @@ -9,6 +9,7 @@ use substrate_fixed::types::{I64F64, U64F64}; use subtensor_runtime_common::NetUid; pub const ONE_YEAR: u64 = 7200 * 365 + 1800; +pub const LOCK_STATE_ZERO_THRESHOLD: u64 = 100; /// Exponential lock state for a coldkey on a subnet. #[crate::freeze_struct("1f6be20a66128b8d")] @@ -22,6 +23,33 @@ pub struct LockState { pub last_update: u64, } +impl LockState { + pub fn is_zero(&self) -> bool { + self.locked_mass < AlphaBalance::from(LOCK_STATE_ZERO_THRESHOLD) + && self.conviction < U64F64::saturating_from_num(LOCK_STATE_ZERO_THRESHOLD) + } +} + +/// Unsigned decrease produced by rolling a lock forward. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct RollDelta { + pub locked_mass_delta: AlphaBalance, + pub conviction_delta: U64F64, +} + +impl RollDelta { + pub fn zero() -> Self { + Self { + locked_mass_delta: AlphaBalance::ZERO, + conviction_delta: U64F64::saturating_from_num(0), + } + } + + pub fn is_zero(&self) -> bool { + self.locked_mass_delta.is_zero() && self.conviction_delta == U64F64::saturating_from_num(0) + } +} + /// A struct that incapsulates Lock primitives such as adding, removing, /// rolling, and updating aggregates. /// @@ -75,54 +103,6 @@ impl ConvictionModel { } } - pub fn roll_forward(&mut self, now: u64, unlock_rate: u64, maturity_rate: u64) { - self.individual_lock = Self::roll_forward_lock( - self.individual_lock.clone(), - now, - unlock_rate, - maturity_rate, - self.owner_lock, - self.perpetual_lock, - ); - self.individual_lock_dirty = true; - self.agg_perpetual_general = Self::roll_forward_lock( - self.agg_perpetual_general.clone(), - now, - unlock_rate, - maturity_rate, - false, - true, - ); - self.agg_perpetual_general_dirty = true; - self.agg_decaying_general = Self::roll_forward_lock( - self.agg_decaying_general.clone(), - now, - unlock_rate, - maturity_rate, - false, - false, - ); - self.agg_decaying_general_dirty = true; - self.agg_perpetual_owner = Self::roll_forward_lock( - self.agg_perpetual_owner.clone(), - now, - unlock_rate, - maturity_rate, - true, - true, - ); - self.agg_perpetual_owner_dirty = true; - self.agg_decaying_owner = Self::roll_forward_lock( - self.agg_decaying_owner.clone(), - now, - unlock_rate, - maturity_rate, - true, - false, - ); - self.agg_decaying_owner_dirty = true; - } - pub fn individual_lock(&self) -> &LockState { &self.individual_lock } @@ -211,12 +191,13 @@ impl ConvictionModel { maturity_rate, self.owner_lock, self.perpetual_lock, - ); + ) + .0; self.individual_lock_dirty = true; } - pub fn roll_forward_individual(&mut self, now: u64, unlock_rate: u64, maturity_rate: u64) { - self.individual_lock = Self::roll_forward_lock( + pub fn roll_forward(&mut self, now: u64, unlock_rate: u64, maturity_rate: u64) { + let (rolled_individual_lock, roll_delta) = Self::roll_forward_lock( self.individual_lock.clone(), now, unlock_rate, @@ -224,7 +205,13 @@ impl ConvictionModel { self.owner_lock, self.perpetual_lock, ); + self.individual_lock = rolled_individual_lock; self.individual_lock_dirty = true; + if !roll_delta.is_zero() { + self.apply_roll_delta_to_aggregate(roll_delta, now); + } else { + self.roll_forward_aggregate(now, unlock_rate, maturity_rate); + } } pub fn roll_forward_aggregate(&mut self, now: u64, unlock_rate: u64, maturity_rate: u64) { @@ -238,7 +225,8 @@ impl ConvictionModel { maturity_rate, owner_lock, perpetual_lock, - ); + ) + .0; *aggregate_dirty = true; } @@ -254,6 +242,17 @@ impl ConvictionModel { *aggregate_dirty = true; } + fn apply_roll_delta_to_aggregate(&mut self, roll_delta: RollDelta, now: u64) { + let (aggregate, aggregate_dirty) = self.aggregate_mut(); + *aggregate = Self::reduce_lock( + aggregate, + roll_delta.locked_mass_delta, + roll_delta.conviction_delta, + ); + aggregate.last_update = now; + *aggregate_dirty = true; + } + pub fn reduce(&mut self, locked_mass: AlphaBalance, conviction: U64F64) { self.individual_lock = Self::reduce_lock(&self.individual_lock, locked_mass, conviction); self.individual_lock_dirty = true; @@ -414,7 +413,9 @@ impl ConvictionModel { maturity_rate: u64, owner_lock: bool, perpetual_lock: bool, - ) -> LockState { + ) -> (LockState, RollDelta) { + let previous_locked_mass = lock.locked_mass; + let previous_conviction = lock.conviction; let mut rolled = if now > lock.last_update { let dt = now.saturating_sub(lock.last_update); let (new_locked_mass, new_conviction) = Self::calculate_decayed_mass_and_conviction( @@ -439,24 +440,46 @@ impl ConvictionModel { rolled.conviction = U64F64::saturating_from_num(u64::from(rolled.locked_mass)); } - rolled + if rolled.is_zero() { + rolled.locked_mass = AlphaBalance::ZERO; + rolled.conviction = U64F64::saturating_from_num(0); + } + + let roll_delta = RollDelta { + locked_mass_delta: previous_locked_mass.saturating_sub(rolled.locked_mass), + conviction_delta: previous_conviction.saturating_sub(rolled.conviction), + }; + + (rolled, roll_delta) } } impl Pallet { + pub fn add_locking_coldkey(hotkey: &T::AccountId, netuid: NetUid, coldkey: &T::AccountId) { + LockingColdkeys::::insert((netuid, hotkey, coldkey), ()); + } + + pub fn maybe_remove_locking_coldkey( + hotkey: &T::AccountId, + netuid: NetUid, + coldkey: &T::AccountId, + ) { + LockingColdkeys::::remove((netuid, hotkey, coldkey)); + } + pub fn insert_lock_state( coldkey: &T::AccountId, netuid: NetUid, hotkey: &T::AccountId, lock_state: LockState, ) { - if !lock_state.locked_mass.is_zero() - || lock_state.conviction > U64F64::saturating_from_num(0) - { - Lock::::insert((coldkey, netuid, hotkey), lock_state); - } else { + if lock_state.is_zero() { + Self::maybe_remove_locking_coldkey(hotkey, netuid, coldkey); // If there is no record previously, this is a no-op Lock::::remove((coldkey, netuid, hotkey)); + } else { + Self::add_locking_coldkey(hotkey, netuid, coldkey); + Lock::::insert((coldkey, netuid, hotkey), lock_state); } } @@ -504,11 +527,11 @@ impl Pallet { } } - fn is_subnet_owner_hotkey(netuid: NetUid, hotkey: &T::AccountId) -> bool { + pub(crate) fn is_subnet_owner_hotkey(netuid: NetUid, hotkey: &T::AccountId) -> bool { hotkey == &SubnetOwnerHotkey::::get(netuid) } - fn is_perpetual_lock(coldkey: &T::AccountId, netuid: NetUid) -> bool { + pub(crate) fn is_perpetual_lock(coldkey: &T::AccountId, netuid: NetUid) -> bool { DecayingLock::::get(coldkey, netuid) == Some(false) } @@ -520,7 +543,7 @@ impl Pallet { } } - fn read_conviction_model_for_hotkey( + pub(crate) fn read_conviction_model_for_hotkey( coldkey: &T::AccountId, netuid: NetUid, hotkey: &T::AccountId, @@ -550,7 +573,7 @@ impl Pallet { }) } - fn save_conviction_model( + pub(crate) fn save_conviction_model( coldkey: &T::AccountId, netuid: NetUid, hotkey: &T::AccountId, @@ -586,7 +609,7 @@ impl Pallet { let current_enabled = Self::is_perpetual_lock(coldkey, netuid); if let Some((hotkey, mut model)) = Self::read_conviction_model(coldkey, netuid, now) { - model.roll_forward_individual(now, UnlockRate::::get(), MaturityRate::::get()); + model.roll_forward(now, UnlockRate::::get(), MaturityRate::::get()); let rolled = model.individual_lock().clone(); Self::save_conviction_model(coldkey, netuid, &hotkey, model); @@ -635,11 +658,7 @@ impl Pallet { let now = Self::get_current_block_as_u64(); Self::read_conviction_model(coldkey, netuid, now) .map(|(_hotkey, mut model)| { - model.roll_forward_individual( - now, - UnlockRate::::get(), - MaturityRate::::get(), - ); + model.roll_forward(now, UnlockRate::::get(), MaturityRate::::get()); model.individual_lock().locked_mass }) .unwrap_or(AlphaBalance::ZERO) @@ -650,11 +669,7 @@ impl Pallet { let now = Self::get_current_block_as_u64(); Self::read_conviction_model(coldkey, netuid, now) .map(|(_hotkey, mut model)| { - model.roll_forward_individual( - now, - UnlockRate::::get(), - MaturityRate::::get(), - ); + model.roll_forward(now, UnlockRate::::get(), MaturityRate::::get()); model.individual_lock().conviction }) .unwrap_or_else(|| U64F64::saturating_from_num(0)) @@ -664,20 +679,29 @@ impl Pallet { pub fn get_coldkey_lock(coldkey: &T::AccountId, netuid: NetUid) -> Option { let now = Self::get_current_block_as_u64(); Self::read_conviction_model(coldkey, netuid, now).map(|(_hotkey, mut model)| { - model.roll_forward_individual(now, UnlockRate::::get(), MaturityRate::::get()); + model.roll_forward(now, UnlockRate::::get(), MaturityRate::::get()); model.individual_lock().clone() }) } - /// Returns the alpha amount available to unstake for a coldkey on a subnet. - pub fn available_to_unstake(coldkey: &T::AccountId, netuid: NetUid) -> AlphaBalance { + /// (total_stake, locked_mass, available_to_unstake) for a coldkey on one subnet. + /// + /// The lock is subnet-wide: it blocks unstaking from any hotkey on that subnet, + /// not from a single hotkey position. + pub(crate) fn stake_availability( + coldkey: &T::AccountId, + netuid: NetUid, + ) -> (AlphaBalance, AlphaBalance, AlphaBalance) { let total = Self::total_coldkey_alpha_on_subnet(coldkey, netuid); let locked = Self::get_current_locked(coldkey, netuid); - if total > locked { - total.saturating_sub(locked) - } else { - AlphaBalance::ZERO - } + let available = total.saturating_sub(locked); + (total, locked, available) + } + + /// Alpha the coldkey can still unstake on this subnet right now. + pub fn available_to_unstake(coldkey: &T::AccountId, netuid: NetUid) -> AlphaBalance { + let (_, _, available) = Self::stake_availability(coldkey, netuid); + available } /// Ensures that the amount can be unstaked @@ -716,7 +740,7 @@ impl Pallet { } None => Self::read_conviction_model_for_hotkey(coldkey, netuid, hotkey, now), }; - model.roll_forward_individual(now, UnlockRate::::get(), MaturityRate::::get()); + model.roll_forward(now, UnlockRate::::get(), MaturityRate::::get()); if model.individual_lock().locked_mass.is_zero() && model.individual_lock().conviction == U64F64::saturating_from_num(0) @@ -772,7 +796,7 @@ impl Pallet { pub fn force_reduce_lock(coldkey: &T::AccountId, netuid: NetUid, amount: AlphaBalance) { let now = Self::get_current_block_as_u64(); if let Some((hotkey, mut model)) = Self::read_conviction_model(coldkey, netuid, now) { - model.roll_forward_individual(now, UnlockRate::::get(), MaturityRate::::get()); + model.roll_forward(now, UnlockRate::::get(), MaturityRate::::get()); model.roll_forward_aggregate(now, UnlockRate::::get(), MaturityRate::::get()); model.force_reduce_individual(amount, now); Self::save_conviction_model(coldkey, netuid, &hotkey, model); @@ -786,18 +810,8 @@ impl Pallet { // Cleanup locks for the specific coldkey and hotkey if let Some((hotkey, mut model)) = Self::read_conviction_model(coldkey, netuid, now) { - model.roll_forward_individual(now, UnlockRate::::get(), MaturityRate::::get()); - let rolled = model.individual_lock().clone(); - if rolled.locked_mass.is_zero() { - model.set_individual_lock(LockState { - locked_mass: AlphaBalance::ZERO, - conviction: U64F64::saturating_from_num(0), - last_update: now, - }); - model.roll_forward_aggregate(now, UnlockRate::::get(), MaturityRate::::get()); - model.reduce_aggregate(rolled.locked_mass, rolled.conviction); - Self::save_conviction_model(coldkey, netuid, &hotkey, model); - } + model.roll_forward(now, UnlockRate::::get(), MaturityRate::::get()); + Self::save_conviction_model(coldkey, netuid, &hotkey, model); } } @@ -879,6 +893,7 @@ impl Pallet { false, true, ) + .0 .conviction }) .unwrap_or_else(|| U64F64::saturating_from_num(0)); @@ -892,6 +907,7 @@ impl Pallet { false, false, ) + .0 .conviction }) .unwrap_or_else(|| U64F64::saturating_from_num(0)); @@ -907,6 +923,7 @@ impl Pallet { true, true, ) + .0 .conviction }) .unwrap_or_else(|| U64F64::saturating_from_num(0)); @@ -920,6 +937,7 @@ impl Pallet { true, false, ) + .0 .conviction }) .unwrap_or_else(|| U64F64::saturating_from_num(0)); @@ -946,6 +964,7 @@ impl Pallet { false, true, ) + .0 .conviction }) .fold(U64F64::saturating_from_num(0), |acc, conviction| { @@ -961,6 +980,7 @@ impl Pallet { false, false, ) + .0 .conviction }) .fold(U64F64::saturating_from_num(0), |acc, conviction| { @@ -976,6 +996,7 @@ impl Pallet { true, true, ) + .0 .conviction }) .unwrap_or_else(|| U64F64::saturating_from_num(0)); @@ -989,6 +1010,7 @@ impl Pallet { true, false, ) + .0 .conviction }) .unwrap_or_else(|| U64F64::saturating_from_num(0)); @@ -1018,7 +1040,7 @@ impl Pallet { let entry = scores .entry(hotkey) .or_insert_with(|| U64F64::saturating_from_num(0)); - *entry = entry.saturating_add(rolled.conviction); + *entry = entry.saturating_add(rolled.0.conviction); }); DecayingHotkeyLock::::iter_prefix(netuid).for_each(|(hotkey, lock)| { let rolled = ConvictionModel::roll_forward_lock( @@ -1032,7 +1054,7 @@ impl Pallet { let entry = scores .entry(hotkey) .or_insert_with(|| U64F64::saturating_from_num(0)); - *entry = entry.saturating_add(rolled.conviction); + *entry = entry.saturating_add(rolled.0.conviction); }); if let Some(lock) = OwnerLock::::get(netuid) { let owner_hotkey = SubnetOwnerHotkey::::get(netuid); @@ -1047,7 +1069,7 @@ impl Pallet { let entry = scores .entry(owner_hotkey) .or_insert_with(|| U64F64::saturating_from_num(0)); - *entry = entry.saturating_add(rolled.conviction); + *entry = entry.saturating_add(rolled.0.conviction); } if let Some(lock) = DecayingOwnerLock::::get(netuid) { let owner_hotkey = SubnetOwnerHotkey::::get(netuid); @@ -1062,7 +1084,7 @@ impl Pallet { let entry = scores .entry(owner_hotkey) .or_insert_with(|| U64F64::saturating_from_num(0)); - *entry = entry.saturating_add(rolled.conviction); + *entry = entry.saturating_add(rolled.0.conviction); } scores @@ -1149,6 +1171,7 @@ impl Pallet { false, true, ) + .0 }) .unwrap_or_else(|| Self::empty_lock(now)); Self::insert_hotkey_lock_state( @@ -1157,10 +1180,10 @@ impl Pallet { LockState { locked_mass: current .locked_mass - .saturating_add(moved_owner_lock.locked_mass), + .saturating_add(moved_owner_lock.0.locked_mass), conviction: current .conviction - .saturating_add(moved_owner_lock.conviction), + .saturating_add(moved_owner_lock.0.conviction), last_update: now, }, ); @@ -1184,6 +1207,7 @@ impl Pallet { false, false, ) + .0 }) .unwrap_or_else(|| Self::empty_lock(now)); Self::insert_decaying_hotkey_lock_state( @@ -1192,10 +1216,10 @@ impl Pallet { LockState { locked_mass: current .locked_mass - .saturating_add(moved_owner_lock.locked_mass), + .saturating_add(moved_owner_lock.0.locked_mass), conviction: current .conviction - .saturating_add(moved_owner_lock.conviction), + .saturating_add(moved_owner_lock.0.conviction), last_update: now, }, ); @@ -1219,6 +1243,7 @@ impl Pallet { true, true, ) + .0 }) .unwrap_or_else(|| Self::empty_lock(now)); Self::insert_owner_lock_state( @@ -1227,10 +1252,10 @@ impl Pallet { LockState { locked_mass: current .locked_mass - .saturating_add(moved_king_lock.locked_mass), + .saturating_add(moved_king_lock.0.locked_mass), conviction: current .conviction - .saturating_add(moved_king_lock.conviction), + .saturating_add(moved_king_lock.0.conviction), last_update: now, }, now, @@ -1238,7 +1263,8 @@ impl Pallet { maturity_rate, true, true, - ), + ) + .0, ); } if let Some(king_lock) = DecayingHotkeyLock::::take(netuid, &king_hotkey) { @@ -1260,6 +1286,7 @@ impl Pallet { true, false, ) + .0 }) .unwrap_or_else(|| Self::empty_lock(now)); Self::insert_decaying_owner_lock_state( @@ -1268,10 +1295,10 @@ impl Pallet { LockState { locked_mass: current .locked_mass - .saturating_add(moved_king_lock.locked_mass), + .saturating_add(moved_king_lock.0.locked_mass), conviction: current .conviction - .saturating_add(moved_king_lock.conviction), + .saturating_add(moved_king_lock.0.conviction), last_update: now, }, now, @@ -1279,7 +1306,8 @@ impl Pallet { maturity_rate, true, false, - ), + ) + .0, ); } @@ -1308,7 +1336,7 @@ impl Pallet { Self::is_subnet_owner_hotkey(netuid, &hotkey), Self::is_perpetual_lock(coldkey, netuid), ); - if rolled.locked_mass > AlphaBalance::ZERO { + if rolled.0.locked_mass > AlphaBalance::ZERO { return Err(Error::::ActiveLockExists); } } @@ -1331,45 +1359,60 @@ impl Pallet { Self::ensure_no_active_locks(new_coldkey)?; let mut locks_to_transfer: Vec<(NetUid, T::AccountId, LockState)> = Vec::new(); + let decaying_locks_to_transfer: Vec<(NetUid, bool)> = + DecayingLock::::iter_prefix(old_coldkey).collect(); // Gather locks for old coldkey for ((netuid, hotkey), lock) in Lock::::iter_prefix((old_coldkey,)) { locks_to_transfer.push((netuid, hotkey, lock)); } + for (netuid, decaying) in decaying_locks_to_transfer.iter() { + DecayingLock::::insert(new_coldkey, *netuid, *decaying); + } + // Remove locks for old coldkey and insert for new for (netuid, hotkey, lock) in locks_to_transfer { let now = Self::get_current_block_as_u64(); let unlock_rate = UnlockRate::::get(); let maturity_rate = MaturityRate::::get(); + let perpetual_lock = decaying_locks_to_transfer + .iter() + .any(|(decaying_netuid, decaying)| *decaying_netuid == netuid && !*decaying); let old_lock = ConvictionModel::roll_forward_lock( lock, now, unlock_rate, maturity_rate, Self::is_subnet_owner_hotkey(netuid, &hotkey), - Self::is_perpetual_lock(old_coldkey, netuid), + perpetual_lock, ); let new_lock = ConvictionModel::roll_forward_lock( - old_lock.clone(), + old_lock.0.clone(), now, unlock_rate, maturity_rate, Self::is_subnet_owner_hotkey(netuid, &hotkey), - Self::is_perpetual_lock(new_coldkey, netuid), - ); + perpetual_lock, + ) + .0; Lock::::remove((old_coldkey.clone(), netuid, hotkey.clone())); + Self::maybe_remove_locking_coldkey(&hotkey, netuid, old_coldkey); Self::reduce_aggregate_lock( old_coldkey, &hotkey, netuid, - old_lock.locked_mass, - old_lock.conviction, + old_lock.0.locked_mass, + old_lock.0.conviction, ); Self::insert_lock_state(new_coldkey, netuid, &hotkey, new_lock.clone()); Self::add_aggregate_lock(new_coldkey, &hotkey, netuid, new_lock); } + for (netuid, _) in decaying_locks_to_transfer { + DecayingLock::::remove(old_coldkey, netuid); + } + Ok(()) } @@ -1432,14 +1475,16 @@ impl Pallet { reads = reads.saturating_add(5); } - if !netuids_to_transfer.is_empty() { - for ((coldkey, netuid, hotkey), lock) in Lock::::iter() { - if hotkey == *old_hotkey - && netuids_to_transfer - .iter() - .any(|(rebuild_netuid, _, _)| *rebuild_netuid == netuid) - { - locks_to_transfer.push((coldkey, netuid, lock)); + // Build a concrete transfer list from the hotkey-to-coldkey index. + // The index can contain stale coldkeys, so only locks that still exist + // are carried forward; missing locks are pruned from the index. + for (netuid, _, _) in &netuids_to_transfer { + for (coldkey, _) in LockingColdkeys::::iter_prefix((*netuid, old_hotkey)) { + if let Some(lock) = Lock::::get((coldkey.clone(), *netuid, old_hotkey.clone())) { + locks_to_transfer.push((coldkey, *netuid, lock)); + } else { + Self::maybe_remove_locking_coldkey(old_hotkey, *netuid, &coldkey); + writes = writes.saturating_add(1); } reads = reads.saturating_add(1); } @@ -1463,7 +1508,8 @@ impl Pallet { maturity_rate, old_owner_lock, perpetual_lock, - ); + ) + .0; let moved = ConvictionModel::roll_forward_lock( rolled, now, @@ -1471,8 +1517,10 @@ impl Pallet { maturity_rate, new_owner_lock, perpetual_lock, - ); + ) + .0; Lock::::remove((coldkey.clone(), netuid, old_hotkey.clone())); + Self::maybe_remove_locking_coldkey(old_hotkey, netuid, &coldkey); Self::insert_lock_state(&coldkey, netuid, new_hotkey, moved); writes = writes.saturating_add(2); } @@ -1491,6 +1539,7 @@ impl Pallet { true, true, ) + .0 }) } else { HotkeyLock::::take(netuid, old_hotkey).map(|lock| { @@ -1502,6 +1551,7 @@ impl Pallet { false, true, ) + .0 }) }; let moved_decaying_lock = if old_was_owner { @@ -1514,6 +1564,7 @@ impl Pallet { true, false, ) + .0 }) } else { DecayingHotkeyLock::::take(netuid, old_hotkey).map(|lock| { @@ -1525,6 +1576,7 @@ impl Pallet { false, false, ) + .0 }) }; @@ -1539,7 +1591,8 @@ impl Pallet { maturity_rate, true, true, - ), + ) + .0, ); } else { Self::insert_hotkey_lock_state( @@ -1552,7 +1605,8 @@ impl Pallet { maturity_rate, false, true, - ), + ) + .0, ); } } @@ -1567,7 +1621,8 @@ impl Pallet { maturity_rate, true, false, - ), + ) + .0, ); } else { Self::insert_decaying_hotkey_lock_state( @@ -1580,7 +1635,8 @@ impl Pallet { maturity_rate, false, false, - ), + ) + .0, ); } } @@ -1612,7 +1668,7 @@ impl Pallet { Some((origin_hotkey, mut model)) => { let unlock_rate = UnlockRate::::get(); let maturity_rate = MaturityRate::::get(); - model.roll_forward_individual(now, unlock_rate, maturity_rate); + model.roll_forward(now, unlock_rate, maturity_rate); let mut lock = model.individual_lock().clone(); let removed = lock.clone(); @@ -1628,9 +1684,11 @@ impl Pallet { maturity_rate, Self::is_subnet_owner_hotkey(netuid, destination_hotkey), Self::is_perpetual_lock(coldkey, netuid), - ); + ) + .0; Lock::::remove((coldkey.clone(), netuid, origin_hotkey.clone())); + Self::maybe_remove_locking_coldkey(&origin_hotkey, netuid, coldkey); Self::insert_lock_state(coldkey, netuid, destination_hotkey, lock.clone()); Self::reduce_aggregate_lock( coldkey, @@ -1713,11 +1771,11 @@ impl Pallet { let unlock_rate = UnlockRate::::get(); let maturity_rate = MaturityRate::::get(); - source_model.roll_forward_individual(now, unlock_rate, maturity_rate); + source_model.roll_forward(now, unlock_rate, maturity_rate); let mut source_lock = source_model.individual_lock().clone(); let maybe_destination_lock = Self::read_conviction_model(destination_coldkey, netuid, now) .map(|(hotkey, mut model)| { - model.roll_forward_individual(now, unlock_rate, maturity_rate); + model.roll_forward(now, unlock_rate, maturity_rate); (hotkey, model.individual_lock().clone()) }); @@ -1788,7 +1846,8 @@ impl Pallet { maturity_rate, Self::is_subnet_owner_hotkey(netuid, &source_hotkey), Self::is_perpetual_lock(origin_coldkey, netuid), - ); + ) + .0; destination_lock = ConvictionModel::roll_forward_lock( destination_lock, now, @@ -1796,7 +1855,8 @@ impl Pallet { maturity_rate, Self::is_subnet_owner_hotkey(netuid, &destination_hotkey), Self::is_perpetual_lock(destination_coldkey, netuid), - ); + ) + .0; // Upsert updated locks (only once per this fn) even if there were no updates because // of roll-forward @@ -1832,43 +1892,23 @@ impl Pallet { /// Destroys all lock maps for network dissolution pub fn destroy_lock_maps(netuid: NetUid) { + // LockingColdkeys: (netuid, hotkey, coldkey) // Lock: (coldkey, netuid, hotkey) { - let to_rm: sp_std::vec::Vec<(T::AccountId, T::AccountId)> = Lock::::iter() - .filter_map( - |((cold, n, hot), _)| { - if n == netuid { Some((cold, hot)) } else { None } - }, - ) - .collect(); + let to_rm: sp_std::vec::Vec<((T::AccountId, T::AccountId), ())> = + LockingColdkeys::::iter_prefix((netuid,)).collect(); - for (cold, hot) in to_rm { + for ((hot, cold), _) in to_rm { Lock::::remove((cold, netuid, hot)); } + let _ = LockingColdkeys::::clear_prefix((netuid,), u32::MAX, None); } // HotkeyLock: (netuid, hotkey) → LockState - { - let to_rm: sp_std::vec::Vec = HotkeyLock::::iter_prefix(netuid) - .map(|(hot, _)| hot) - .collect(); - - for hot in to_rm { - HotkeyLock::::remove(netuid, hot); - } - } + let _ = HotkeyLock::::clear_prefix(netuid, u32::MAX, None); // DecayingHotkeyLock: (netuid, hotkey) - { - let to_rm: sp_std::vec::Vec = - DecayingHotkeyLock::::iter_prefix(netuid) - .map(|(hot, _)| hot) - .collect(); - - for hot in to_rm { - DecayingHotkeyLock::::remove(netuid, hot); - } - } + let _ = DecayingHotkeyLock::::clear_prefix(netuid, u32::MAX, None); // OwnerLock / DecayingOwnerLock: (netuid) OwnerLock::::remove(netuid); diff --git a/pallets/subtensor/src/staking/mod.rs b/pallets/subtensor/src/staking/mod.rs index a10908eca3..cf8ac006ac 100644 --- a/pallets/subtensor/src/staking/mod.rs +++ b/pallets/subtensor/src/staking/mod.rs @@ -7,6 +7,7 @@ pub mod helpers; pub mod increase_take; pub mod lock; pub mod move_stake; +pub mod order_swap; pub mod recycle_alpha; pub mod remove_stake; pub mod set_children; diff --git a/pallets/subtensor/src/staking/move_stake.rs b/pallets/subtensor/src/staking/move_stake.rs index aafefa28ed..b189c90968 100644 --- a/pallets/subtensor/src/staking/move_stake.rs +++ b/pallets/subtensor/src/staking/move_stake.rs @@ -50,7 +50,6 @@ impl Pallet { None, None, false, - true, )?; // Log the event. @@ -141,7 +140,6 @@ impl Pallet { None, None, true, - false, )?; // 9. Emit an event for logging/monitoring. @@ -206,7 +204,6 @@ impl Pallet { None, None, false, - true, )?; // Emit an event for logging. @@ -274,7 +271,6 @@ impl Pallet { Some(limit_price), Some(allow_partial), false, - true, )?; // Emit an event for logging. @@ -306,7 +302,6 @@ impl Pallet { maybe_limit_price: Option, maybe_allow_partial: Option, check_transfer_toggle: bool, - set_limit: bool, ) -> Result { // Cap the alpha_amount at available Alpha because user might be paying transaxtion fees // in Alpha and their total is already reduced by now. @@ -385,7 +380,6 @@ impl Pallet { destination_netuid, tao_unstaked, T::SwapInterface::max_price(), - set_limit, drop_fee_destination, )?; } @@ -424,8 +418,9 @@ impl Pallet { /// /// In the corner case when SubnetTAO(2) == SubnetTAO(1), no slippage is going to occur. /// - /// TODO: This formula only works for a single swap step, so it is not 100% correct for swap v3. We need an updated one. - /// + /// TODO: This formula only works for a single swap step, so it is not 100% correct for swap v3 or + /// highly assymetric balancers. + /// We need an updated one. pub fn get_max_amount_move( origin_netuid: NetUid, destination_netuid: NetUid, @@ -440,7 +435,7 @@ impl Pallet { && (destination_netuid.is_root() || SubnetMechanism::::get(destination_netuid) == 0) { if limit_price > tao.saturating_to_num::().into() { - return Err(Error::::ZeroMaxStakeAmount.into()); + return Ok(AlphaBalance::ZERO); } else { return Ok(AlphaBalance::MAX); } @@ -477,23 +472,19 @@ impl Pallet { } // Corner case: SubnetTAO for any of two subnets is zero - let subnet_tao_1 = Self::get_subnet_tao(origin_netuid) - .saturating_add(SubnetTaoProvided::::get(origin_netuid)); - let subnet_tao_2 = Self::get_subnet_tao(destination_netuid) - .saturating_add(SubnetTaoProvided::::get(destination_netuid)); + let subnet_tao_1 = SubnetTAO::::get(origin_netuid); + let subnet_tao_2 = SubnetTAO::::get(destination_netuid); if subnet_tao_1.is_zero() || subnet_tao_2.is_zero() { - return Err(Error::::ZeroMaxStakeAmount.into()); + return Ok(AlphaBalance::ZERO); } let subnet_tao_1_float: U64F64 = U64F64::saturating_from_num(subnet_tao_1); let subnet_tao_2_float: U64F64 = U64F64::saturating_from_num(subnet_tao_2); // Corner case: SubnetAlphaIn for any of two subnets is zero - let alpha_in_1 = SubnetAlphaIn::::get(origin_netuid) - .saturating_add(SubnetAlphaInProvided::::get(origin_netuid)); - let alpha_in_2 = SubnetAlphaIn::::get(destination_netuid) - .saturating_add(SubnetAlphaInProvided::::get(destination_netuid)); + let alpha_in_1 = SubnetAlphaIn::::get(origin_netuid); + let alpha_in_2 = SubnetAlphaIn::::get(destination_netuid); if alpha_in_1.is_zero() || alpha_in_2.is_zero() { - return Err(Error::::ZeroMaxStakeAmount.into()); + return Ok(AlphaBalance::ZERO); } let alpha_in_1_float: U64F64 = U64F64::saturating_from_num(alpha_in_1); let alpha_in_2_float: U64F64 = U64F64::saturating_from_num(alpha_in_2); @@ -509,7 +500,7 @@ impl Pallet { T::SwapInterface::current_alpha_price(destination_netuid.into()), ); if limit_price_float > current_price { - return Err(Error::::ZeroMaxStakeAmount.into()); + return Ok(AlphaBalance::ZERO); } // Corner case: limit_price is zero @@ -532,10 +523,6 @@ impl Pallet { .saturating_sub(alpha_in_1_float.saturating_mul(t2_over_sum)) .saturating_to_num::(); - if final_result != 0 { - Ok(final_result.into()) - } else { - Err(Error::::ZeroMaxStakeAmount.into()) - } + Ok(final_result.into()) } } diff --git a/pallets/subtensor/src/staking/order_swap.rs b/pallets/subtensor/src/staking/order_swap.rs new file mode 100644 index 0000000000..fd0ae3461c --- /dev/null +++ b/pallets/subtensor/src/staking/order_swap.rs @@ -0,0 +1,191 @@ +use super::*; +use frame_support::traits::fungible::Mutate; +use frame_support::traits::tokens::Preservation; +use frame_support::transactional; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance}; +use subtensor_swap_interface::{Order, OrderSwapInterface, SwapHandler, SwapResult}; + +impl OrderSwapInterface for Pallet { + #[transactional] + fn buy_alpha( + coldkey: &T::AccountId, + hotkey: &T::AccountId, + netuid: NetUid, + tao_amount: TaoBalance, + limit_price: TaoBalance, + validate: bool, + ) -> Result { + ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + Self::ensure_subtoken_enabled(netuid)?; + if validate { + ensure!( + Self::hotkey_account_exists(hotkey), + Error::::HotKeyAccountNotExists + ); + ensure!( + tao_amount >= DefaultMinStake::::get(), + Error::::AmountTooLow + ); + ensure!( + Self::can_remove_balance_from_coldkey_account(coldkey, tao_amount), + Error::::NotEnoughBalanceToStake + ); + } + // `limit_price` is already in ×10⁹ scale (same as the `current_alpha_price` RPC + // endpoint), which is also the scale the AMM uses for its price_limit argument. + // Pass it directly without any scaling. u64::MAX means "no ceiling". + let amm_limit = limit_price; + // Stable subnets (mechanism_id != 1) are always 1:1 and never stop early. + if SubnetMechanism::::get(netuid) == 1 { + let sim_order = GetAlphaForTao::::with_amount(tao_amount); + let sim: SwapResult = + T::SwapInterface::swap(netuid.into(), sim_order, amm_limit, false, true)?; + ensure!( + sim.amount_paid_in.saturating_add(sim.fee_paid) >= tao_amount, + Error::::SlippageTooHigh + ); + } + let alpha_out = + Self::stake_into_subnet(hotkey, coldkey, netuid, tao_amount, amm_limit, false)?; + Ok(alpha_out) + } + + #[transactional] + fn sell_alpha( + coldkey: &T::AccountId, + hotkey: &T::AccountId, + netuid: NetUid, + alpha_amount: AlphaBalance, + limit_price: TaoBalance, + validate: bool, + ) -> Result { + ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + Self::ensure_subtoken_enabled(netuid)?; + if validate { + Self::validate_remove_stake( + coldkey, + hotkey, + netuid, + alpha_amount, + alpha_amount, + false, + )?; + } + // `limit_price` is already in ×10⁹ scale (same as the `current_alpha_price` RPC + // endpoint), which is also the scale the AMM uses for its price_limit argument. + // Pass it directly without any scaling. 0 means "no floor". + let amm_limit = limit_price; + // Stable subnets (mechanism_id != 1) are always 1:1 and never stop early. + if SubnetMechanism::::get(netuid) == 1 { + let sim_order = GetTaoForAlpha::::with_amount(alpha_amount); + let sim: SwapResult = + T::SwapInterface::swap(netuid.into(), sim_order, amm_limit, false, true)?; + ensure!( + sim.amount_paid_in.saturating_add(sim.fee_paid) >= alpha_amount, + Error::::SlippageTooHigh + ); + } + let tao_out = Self::unstake_from_subnet( + hotkey, + coldkey, + coldkey, + netuid, + alpha_amount, + amm_limit, + false, + )?; + Ok(tao_out) + } + + fn current_alpha_price(netuid: NetUid) -> U64F64 { + T::SwapInterface::current_alpha_price(netuid) + } + + fn transfer_tao(from: &T::AccountId, to: &T::AccountId, amount: TaoBalance) -> DispatchResult { + ::Currency::transfer(from, to, amount, Preservation::Expendable)?; + Ok(()) + } + + #[transactional] + fn transfer_staked_alpha( + from_coldkey: &T::AccountId, + from_hotkey: &T::AccountId, + to_coldkey: &T::AccountId, + to_hotkey: &T::AccountId, + netuid: NetUid, + amount: AlphaBalance, + validate_sender: bool, + validate_receiver: bool, + ) -> DispatchResult { + ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); + Self::ensure_subtoken_enabled(netuid)?; + if validate_sender { + ensure!( + Self::hotkey_account_exists(from_hotkey), + Error::::HotKeyAccountNotExists + ); + ensure!(!amount.is_zero(), Error::::AmountTooLow); + let tao_equiv = T::SwapInterface::current_alpha_price(netuid) + .saturating_mul(U64F64::saturating_from_num(amount.to_u64())) + .saturating_to_num::(); + ensure!( + TaoBalance::from(tao_equiv) >= DefaultMinStake::::get(), + Error::::AmountTooLow + ); + Self::ensure_available_to_unstake(from_coldkey, netuid, amount)?; + } + + if validate_receiver { + ensure!( + Self::hotkey_account_exists(to_hotkey), + Error::::HotKeyAccountNotExists + ); + } + + let available = + Self::get_stake_for_hotkey_and_coldkey_on_subnet(from_hotkey, from_coldkey, netuid); + ensure!(available >= amount, Error::::NotEnoughStakeToWithdraw); + Self::decrease_stake_for_hotkey_and_coldkey_on_subnet( + from_hotkey, + from_coldkey, + netuid, + amount, + ); + Self::increase_stake_for_hotkey_and_coldkey_on_subnet( + to_hotkey, to_coldkey, netuid, amount, + ); + LastColdkeyHotkeyStakeBlock::::insert( + to_coldkey, + to_hotkey, + Self::get_current_block_as_u64(), + ); + Ok(()) + } + + fn register_pallet_hotkey(coldkey: &T::AccountId, hotkey: &T::AccountId) -> DispatchResult { + Self::create_account_if_non_existent(coldkey, hotkey) + } + + fn pallet_hotkey_registered(coldkey: &T::AccountId, hotkey: &T::AccountId) -> bool { + Self::coldkey_owns_hotkey(coldkey, hotkey) + } + + #[cfg(feature = "runtime-benchmarks")] + fn set_up_netuid_for_benchmark(netuid: NetUid) { + if !Self::if_subnet_exist(netuid) { + Self::init_new_network(netuid, 100); + } + SubtokenEnabled::::insert(netuid, true); + // Seed pool reserves so the AMM price is well-defined and swaps return non-zero. + SubnetTAO::::insert(netuid, TaoBalance::from(1_000_000_000_000_u64)); + SubnetAlphaIn::::insert(netuid, AlphaBalance::from(1_000_000_000_000_u64)); + } + + #[cfg(feature = "runtime-benchmarks")] + fn set_up_acc_for_benchmark(hotkey: &T::AccountId, coldkey: &T::AccountId) { + let _ = Self::create_account_if_non_existent(coldkey, hotkey); + let credit = Self::mint_tao(TaoBalance::from(1_000_000_000_000_u64)); + let _ = Self::spend_tao(coldkey, credit, TaoBalance::from(1_000_000_000_000_u64)); + } +} diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index 03b962202c..cf640dc661 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -274,7 +274,6 @@ impl Pallet { NetUid::ROOT, total_tao_unstaked, T::SwapInterface::max_price(), - false, // no limit for Root subnet false, )?; @@ -391,7 +390,7 @@ impl Pallet { if limit_price <= 1_000_000_000.into() { return Ok(AlphaBalance::MAX); } else { - return Err(Error::::ZeroMaxStakeAmount.into()); + return Ok(AlphaBalance::ZERO); } } @@ -400,11 +399,7 @@ impl Pallet { let result = T::SwapInterface::swap(netuid.into(), order, limit_price.into(), false, true) .map(|r| r.amount_paid_in.saturating_add(r.fee_paid))?; - if !result.is_zero() { - Ok(result) - } else { - Err(Error::::ZeroMaxStakeAmount.into()) - } + Ok(result) } pub fn do_remove_stake_full_limit( @@ -455,7 +450,9 @@ impl Pallet { .saturating_to_num::(); owner_emission_tao = if owner_alpha_u64 > 0 { - let cur_price: U96F32 = T::SwapInterface::current_alpha_price(netuid.into()); + let cur_price: U96F32 = U96F32::saturating_from_num( + T::SwapInterface::current_alpha_price(netuid.into()), + ); let val_u64 = U96F32::from_num(owner_alpha_u64) .saturating_mul(cur_price) .floor() @@ -610,7 +607,6 @@ impl Pallet { } // 7.c) Remove α‑in/α‑out counters (fully destroyed). SubnetAlphaIn::::remove(netuid); - SubnetAlphaInProvided::::remove(netuid); SubnetAlphaOut::::remove(netuid); SubnetProtocolAlpha::::remove(netuid); @@ -646,21 +642,8 @@ impl Pallet { } } - // 9) Cleanup all subnet stake locks if any. - let lock_keys: Vec<(T::AccountId, NetUid, T::AccountId)> = Lock::::iter_keys() - .filter(|(_, this_netuid, _)| *this_netuid == netuid) - .collect(); - for (coldkey, netuid, hotkey) in lock_keys { - Lock::::remove((coldkey, netuid, hotkey)); - } - - // 10) Cleanup all subnet hotkey locks if any. - let hotkey_lock_keys: Vec<(NetUid, T::AccountId)> = HotkeyLock::::iter_keys() - .filter(|(this_netuid, _)| *this_netuid == netuid) - .collect(); - for (netuid, hotkey) in hotkey_lock_keys { - HotkeyLock::::remove(netuid, hotkey); - } + // 10) Cleanup all subnet stake locks and lock aggregates if any. + Self::destroy_lock_maps(netuid); Ok(()) } diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index fb2de35e95..2826590c50 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -2,7 +2,7 @@ use super::*; use safe_math::*; use share_pool::{SafeFloat, SharePool, SharePoolDataOperations}; use sp_std::{collections::btree_map::BTreeMap, ops::Neg}; -use substrate_fixed::types::{I64F64, I96F32, U96F32}; +use substrate_fixed::types::{I64F64, I96F32, U64F64, U96F32}; use subtensor_runtime_common::{AlphaBalance, AuthorshipInfo, NetUid, TaoBalance, Token}; use subtensor_swap_interface::{Order, SwapHandler, SwapResult}; @@ -21,8 +21,8 @@ impl Pallet { SubnetAlphaIn::::get(netuid).saturating_add(SubnetAlphaOut::::get(netuid)) } - pub fn get_moving_alpha_price(netuid: NetUid) -> U96F32 { - let one = U96F32::saturating_from_num(1.0); + pub fn get_moving_alpha_price(netuid: NetUid) -> U64F64 { + let one = U64F64::saturating_from_num(1.0); if netuid.is_root() { // Root. one @@ -30,12 +30,12 @@ impl Pallet { // Stable one } else { - U96F32::saturating_from_num(SubnetMovingPrice::::get(netuid)) + U64F64::saturating_from_num(SubnetMovingPrice::::get(netuid)) } } pub fn update_moving_price(netuid: NetUid) { - let blocks_since_start_call = U96F32::saturating_from_num({ + let blocks_since_start_call = U64F64::saturating_from_num({ // We expect FirstEmissionBlockNumber to be set earlier, and we take the block when // `start_call` was called (first block before FirstEmissionBlockNumber). let start_call_block = FirstEmissionBlockNumber::::get(netuid) @@ -50,19 +50,20 @@ impl Pallet { // will take in order for the distance between current EMA of price and current price to shorten // by half. let halving_time = EMAPriceHalvingBlocks::::get(netuid); - let current_ma_unsigned = U96F32::saturating_from_num(SubnetMovingAlpha::::get()); - let alpha: U96F32 = current_ma_unsigned.saturating_mul(blocks_since_start_call.safe_div( - blocks_since_start_call.saturating_add(U96F32::saturating_from_num(halving_time)), + let current_ma_unsigned = U64F64::saturating_from_num(SubnetMovingAlpha::::get()); + let alpha: U64F64 = current_ma_unsigned.saturating_mul(blocks_since_start_call.safe_div( + blocks_since_start_call.saturating_add(U64F64::saturating_from_num(halving_time)), )); // Because alpha = b / (b + h), where b and h > 0, alpha < 1, so 1 - alpha > 0. // We can use unsigned type here: U96F32 - let one_minus_alpha: U96F32 = U96F32::saturating_from_num(1.0).saturating_sub(alpha); - let current_price: U96F32 = alpha.saturating_mul( + let one_minus_alpha: U64F64 = U64F64::saturating_from_num(1.0).saturating_sub(alpha); + let current_price: U64F64 = alpha.saturating_mul(U64F64::saturating_from_num( T::SwapInterface::current_alpha_price(netuid.into()) - .min(U96F32::saturating_from_num(1.0)), - ); - let current_moving: U96F32 = - one_minus_alpha.saturating_mul(Self::get_moving_alpha_price(netuid)); + .min(U64F64::saturating_from_num(1.0)), + )); + let current_moving: U64F64 = one_minus_alpha.saturating_mul(U64F64::saturating_from_num( + Self::get_moving_alpha_price(netuid), + )); // Convert batch to signed I96F32 to avoid migration of SubnetMovingPrice for now`` let new_moving: I96F32 = I96F32::saturating_from_num(current_price.saturating_add(current_moving)); @@ -70,12 +71,12 @@ impl Pallet { } /// Gets the Median Subnet Alpha Price - pub fn get_median_subnet_alpha_price() -> U96F32 { - let default_price = U96F32::saturating_from_num(1_u64); - let zero_price = U96F32::saturating_from_num(0_u64); - let two = U96F32::saturating_from_num(2_u64); + pub fn get_median_subnet_alpha_price() -> U64F64 { + let default_price = U64F64::saturating_from_num(1_u64); + let zero_price = U64F64::saturating_from_num(0_u64); + let two = U64F64::saturating_from_num(2_u64); - let mut price_counts: BTreeMap = BTreeMap::new(); + let mut price_counts: BTreeMap = BTreeMap::new(); let mut total_prices: usize = 0; for (netuid, added) in NetworksAdded::::iter() { @@ -112,8 +113,8 @@ impl Pallet { }; let mut cumulative: usize = 0; - let mut lower_price: Option = None; - let mut upper_price: Option = None; + let mut lower_price: Option = None; + let mut upper_price: Option = None; for (price, count) in price_counts.into_iter() { let next_cumulative = cumulative.saturating_add(count); @@ -853,7 +854,6 @@ impl Pallet { netuid: NetUid, tao: TaoBalance, price_limit: TaoBalance, - set_limit: bool, drop_fees: bool, ) -> Result { // Transfer TAO from coldkey to the subnet account. @@ -919,10 +919,6 @@ impl Pallet { LastColdkeyHotkeyStakeBlock::::insert(coldkey, hotkey, Self::get_current_block_as_u64()); - if set_limit { - Self::set_stake_operation_limit(hotkey, coldkey, netuid.into()); - } - // If this is a root-stake if netuid == NetUid::ROOT { // Adjust root claimed for this hotkey and coldkey. @@ -1004,7 +1000,7 @@ impl Pallet { let current_price = ::SwapInterface::current_alpha_price(netuid.into()); let tao_equivalent: TaoBalance = current_price - .saturating_mul(U96F32::saturating_from_num(alpha)) + .saturating_mul(U64F64::saturating_from_num(alpha)) .saturating_to_num::() .into(); @@ -1147,8 +1143,6 @@ impl Pallet { // Ensure that the subnet exists. ensure!(Self::if_subnet_exist(netuid), Error::::SubnetNotExists); - Self::ensure_stake_operation_limit_not_exceeded(hotkey, coldkey, netuid.into())?; - // Ensure that the subnet is enabled. // Self::ensure_subtoken_enabled(netuid)?; @@ -1250,12 +1244,6 @@ impl Pallet { ensure!(origin_netuid != destination_netuid, Error::::SameNetuid); } - Self::ensure_stake_operation_limit_not_exceeded( - origin_hotkey, - origin_coldkey, - origin_netuid.into(), - )?; - // Ensure that both subnets exist. ensure!( Self::if_subnet_exist(origin_netuid), @@ -1376,27 +1364,6 @@ impl Pallet { }); } } - - pub fn set_stake_operation_limit( - hotkey: &T::AccountId, - coldkey: &T::AccountId, - netuid: NetUid, - ) { - StakingOperationRateLimiter::::insert((hotkey, coldkey, netuid), true); - } - - pub fn ensure_stake_operation_limit_not_exceeded( - hotkey: &T::AccountId, - coldkey: &T::AccountId, - netuid: NetUid, - ) -> Result<(), Error> { - ensure!( - !StakingOperationRateLimiter::::contains_key((hotkey, coldkey, netuid)), - Error::::StakingOperationRateLimitExceeded - ); - - Ok(()) - } } /////////////////////////////////////////// diff --git a/pallets/subtensor/src/subnets/subnet.rs b/pallets/subtensor/src/subnets/subnet.rs index 7121874400..5f5fcafd61 100644 --- a/pallets/subtensor/src/subnets/subnet.rs +++ b/pallets/subtensor/src/subnets/subnet.rs @@ -3,7 +3,7 @@ use frame_support::PalletId; use safe_math::FixedExt; use sp_core::Get; use sp_runtime::traits::AccountIdConversion; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::U64F64; use subtensor_runtime_common::{NetUid, TaoBalance}; impl Pallet { /// Returns true if the subnetwork exists. @@ -227,14 +227,12 @@ impl Pallet { pool_initial_tao }; - let total_pool_alpha: AlphaBalance = U96F32::saturating_from_num(total_pool_tao.to_u64()) + let total_pool_alpha: AlphaBalance = U64F64::saturating_from_num(total_pool_tao.to_u64()) .safe_div(median_subnet_alpha_price) .saturating_floor() .saturating_to_num::() .into(); - let owner_alpha_stake = AlphaBalance::ZERO; - // With the full lock retained in the reserve, this will normally be zero. let tao_recycled_for_registration = actual_tao_lock_amount.saturating_sub(total_pool_tao); @@ -244,9 +242,7 @@ impl Pallet { SubnetOwner::::insert(netuid_to_register, coldkey.clone()); Self::set_subnet_owner_hotkey(netuid_to_register, hotkey)?; SubnetLocked::::insert(netuid_to_register, actual_tao_lock_amount); - SubnetTaoProvided::::insert(netuid_to_register, TaoBalance::ZERO); - SubnetAlphaInProvided::::insert(netuid_to_register, AlphaBalance::ZERO); - SubnetAlphaOut::::insert(netuid_to_register, owner_alpha_stake); + SubnetAlphaOut::::insert(netuid_to_register, AlphaBalance::ZERO); SubnetVolume::::insert(netuid_to_register, 0u128); RAORecycledForRegistration::::insert(netuid_to_register, tao_recycled_for_registration); @@ -302,6 +298,12 @@ impl Pallet { // --- 3. Fill tempo memory item. Tempo::::insert(netuid, tempo); + // --- 3.1. Initialise `LastEpochBlock` with a per-netuid stagger + let now = Self::get_current_block_as_u64(); + let period = (tempo as u64).max(1); + let stagger = (u16::from(netuid) as u64).checked_rem(period).unwrap_or(0); + LastEpochBlock::::insert(netuid, now.saturating_sub(stagger)); + // --- 4. Increase total network count. TotalNetworks::::mutate(|n| *n = n.saturating_add(1)); diff --git a/pallets/subtensor/src/subnets/weights.rs b/pallets/subtensor/src/subnets/weights.rs index 39bcfb80b5..7a8dc50a60 100644 --- a/pallets/subtensor/src/subnets/weights.rs +++ b/pallets/subtensor/src/subnets/weights.rs @@ -96,17 +96,21 @@ impl Pallet { Error::::CommittingWeightsTooFast ); - // 5. Calculate the reveal blocks based on network tempo and reveal period. - let (first_reveal_block, last_reveal_block) = Self::get_reveal_blocks(netuid, commit_block); + // 5. Resolve the epoch this commit belongs to under the stateful counter. + let commit_epoch = Self::current_epoch_with_lookahead(netuid); // 6. Retrieve or initialize the VecDeque of commits for the hotkey. WeightCommits::::try_mutate(netuid_index, &who, |maybe_commits| -> DispatchResult { + // Tuple shape `(hash, commit_epoch, commit_block, _)`. `commit_epoch` + // drives reveal-window timing; `commit_block` is kept for the epoch's + // commit-reveal weight column-mask. The 4th field is a legacy + // reveal-block bound, now unused and left at 0. let mut commits: VecDeque<(H256, u64, u64, u64)> = maybe_commits.take().unwrap_or_default(); // 7. Remove any expired commits from the front of the queue. - while let Some((_, commit_block_existing, _, _)) = commits.front() { - if Self::is_commit_expired(netuid, *commit_block_existing) { + while let Some((_, commit_epoch_existing, _, _)) = commits.front() { + if Self::is_commit_expired(netuid, *commit_epoch_existing) { commits.pop_front(); } else { break; @@ -116,13 +120,8 @@ impl Pallet { // 8. Verify that the number of unrevealed commits is within the allowed limit. ensure!(commits.len() < 10, Error::::TooManyUnrevealedCommits); - // 9. Append the new commit with calculated reveal blocks. - commits.push_back(( - commit_hash, - commit_block, - first_reveal_block, - last_reveal_block, - )); + // 9. Append the new commit, tagged with its epoch and block. + commits.push_back((commit_hash, commit_epoch, commit_block, 0)); // 10. Store the updated commits queue back to storage. *maybe_commits = Some(commits); @@ -342,10 +341,8 @@ impl Pallet { // 6. Retrieve or initialize the VecDeque of commits for the hotkey. let cur_block = Self::get_current_block_as_u64(); - let cur_epoch = match Self::should_run_epoch(netuid, commit_block) { - true => Self::get_epoch_index(netuid, cur_block).saturating_add(1), - false => Self::get_epoch_index(netuid, cur_block), - }; + // Key the commit by the epoch it belongs to under the stateful counter. + let cur_epoch = Self::current_epoch_with_lookahead(netuid); TimelockedWeightCommits::::try_mutate( netuid_index, @@ -1249,49 +1246,49 @@ impl Pallet { uids.len() <= subnetwork_n as usize } - pub fn is_reveal_block_range(netuid: NetUid, commit_block: u64) -> bool { - let current_block: u64 = Self::get_current_block_as_u64(); - let commit_epoch: u64 = Self::get_epoch_index(netuid, commit_block); - let current_epoch: u64 = Self::get_epoch_index(netuid, current_block); + /// True when the current epoch is exactly `commit_epoch + reveal_period`. + /// + /// `commit_epoch` is the `SubnetEpochIndex` value stored with the commit (CR-v2 + /// `WeightCommits` tuple field 1). The current epoch uses the look-ahead value + /// so a reveal submitted on a fire-block is judged against the about-to-fire + /// epoch, consistent with how the commit was tagged. + pub fn is_reveal_block_range(netuid: NetUid, commit_epoch: u64) -> bool { + let current_epoch: u64 = Self::current_epoch_with_lookahead(netuid); let reveal_period: u64 = Self::get_reveal_period(netuid); - // Reveal is allowed only in the exact epoch `commit_epoch + reveal_period` current_epoch == commit_epoch.saturating_add(reveal_period) } - pub fn get_epoch_index(netuid: NetUid, block_number: u64) -> u64 { - let tempo: u64 = Self::get_tempo(netuid) as u64; - let tempo_plus_one: u64 = tempo.saturating_add(1); - let netuid_plus_one: u64 = (u16::from(netuid) as u64).saturating_add(1); - let block_with_offset: u64 = block_number.saturating_add(netuid_plus_one); - - block_with_offset.checked_div(tempo_plus_one).unwrap_or(0) + /// Canonical epoch index for a subnet — the monotonic `SubnetEpochIndex` counter. + pub fn get_epoch_index(netuid: NetUid, _block_number: u64) -> u64 { + SubnetEpochIndex::::get(netuid) } - pub fn is_commit_expired(netuid: NetUid, commit_block: u64) -> bool { - let current_block: u64 = Self::get_current_block_as_u64(); - let current_epoch: u64 = Self::get_epoch_index(netuid, current_block); - let commit_epoch: u64 = Self::get_epoch_index(netuid, commit_block); - let reveal_period: u64 = Self::get_reveal_period(netuid); - - current_epoch > commit_epoch.saturating_add(reveal_period) + /// Epoch index that a commit or reveal happening at the *current* block + /// belongs to: the `SubnetEpochIndex` counter, plus one if an epoch slot is + /// due to fire this block. + /// + /// The look-ahead is needed because `block_step` runs in `on_initialize`: + /// `reveal_crv3_commits` (which must see the about-to-fire epoch) runs before + /// `run_coinbase` increments the counter, and a commit extrinsic submitted on + /// a deferred fire-block belongs to the next epoch, not the current one. + pub fn current_epoch_with_lookahead(netuid: NetUid) -> u64 { + let block = Self::get_current_block_as_u64(); + let base = SubnetEpochIndex::::get(netuid); + if Self::should_run_epoch(netuid, block) { + base.saturating_add(1) + } else { + base + } } - pub fn get_reveal_blocks(netuid: NetUid, commit_block: u64) -> (u64, u64) { + /// True once the current epoch has moved past the commit's reveal epoch + /// (`commit_epoch + reveal_period`). `commit_epoch` is the stored counter value. + pub fn is_commit_expired(netuid: NetUid, commit_epoch: u64) -> bool { + let current_epoch: u64 = Self::current_epoch_with_lookahead(netuid); let reveal_period: u64 = Self::get_reveal_period(netuid); - let tempo: u64 = Self::get_tempo(netuid) as u64; - let tempo_plus_one: u64 = tempo.saturating_add(1); - let netuid_plus_one: u64 = (u16::from(netuid) as u64).saturating_add(1); - let commit_epoch: u64 = Self::get_epoch_index(netuid, commit_block); - let reveal_epoch: u64 = commit_epoch.saturating_add(reveal_period); - - let first_reveal_block = reveal_epoch - .saturating_mul(tempo_plus_one) - .saturating_sub(netuid_plus_one); - let last_reveal_block = first_reveal_block.saturating_add(tempo); - - (first_reveal_block, last_reveal_block) + current_epoch > commit_epoch.saturating_add(reveal_period) } pub fn set_reveal_period(netuid: NetUid, reveal_period: u64) -> DispatchResult { @@ -1314,6 +1311,11 @@ impl Pallet { RevealPeriodEpochs::::get(netuid) } + /// Legacy modulo first-block-of-epoch: `epoch * (tempo + 1) - (netuid + 1)`. + /// + /// NOT used by live commit-reveal logic — that keys off the stateful + /// `SubnetEpochIndex` counter. Retained solely so the already-executed, + /// one-shot `migrate_crv3_commits_add_block` migration stays untouched. pub fn get_first_block_of_epoch(netuid: NetUid, epoch: u64) -> u64 { let tempo: u64 = Self::get_tempo(netuid) as u64; let tempo_plus_one: u64 = tempo.saturating_add(1); @@ -1334,18 +1336,20 @@ impl Pallet { BlakeTwo256::hash_of(&(who.clone(), netuid_index, uids, values, salt, version_key)) } - pub fn find_commit_block_via_hash(hash: H256) -> Option { + /// Returns the stored `commit_epoch` (CR-v2 `WeightCommits` tuple field 1) for + /// the commit with the given hash, if any. + pub fn find_commit_epoch_via_hash(hash: H256) -> Option { WeightCommits::::iter().find_map(|(_, _, commits)| { commits .iter() .find(|(h, _, _, _)| *h == hash) - .map(|(_, commit_block, _, _)| *commit_block) + .map(|(_, commit_epoch, _, _)| *commit_epoch) }) } - pub fn is_batch_reveal_block_range(netuid: NetUid, commit_block: Vec) -> bool { - commit_block + pub fn is_batch_reveal_epoch_range(netuid: NetUid, commit_epochs: Vec) -> bool { + commit_epochs .iter() - .all(|block| Self::is_reveal_block_range(netuid, *block)) + .all(|epoch| Self::is_reveal_block_range(netuid, *epoch)) } } diff --git a/pallets/subtensor/src/tests/children.rs b/pallets/subtensor/src/tests/children.rs index ec191ba0e7..922fd2c894 100644 --- a/pallets/subtensor/src/tests/children.rs +++ b/pallets/subtensor/src/tests/children.rs @@ -2318,7 +2318,6 @@ fn test_do_remove_stake_clears_pending_childkeys() { assert!(pending_before.1 > 0); // Remove stake - remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); assert_ok!(SubtensorModule::do_remove_stake( RuntimeOrigin::signed(coldkey), hotkey, @@ -3144,6 +3143,9 @@ fn test_parent_child_chain_emission() { PendingValidatorEmission::::insert(netuid, AlphaBalance::ZERO); PendingServerEmission::::insert(netuid, AlphaBalance::ZERO); + // To trigger the epoch, block should be > tempo. So we advance it before + System::set_block_number(2); + // Run epoch with emission value let emission_value = u64::from(emission.peek()); SubtensorModule::run_coinbase(emission); diff --git a/pallets/subtensor/src/tests/claim_root.rs b/pallets/subtensor/src/tests/claim_root.rs index 1b6b9d8c6b..7b40c4d372 100644 --- a/pallets/subtensor/src/tests/claim_root.rs +++ b/pallets/subtensor/src/tests/claim_root.rs @@ -18,7 +18,7 @@ use frame_support::{assert_err, assert_noop, assert_ok}; use sp_core::{H256, U256}; use sp_runtime::DispatchError; use std::collections::BTreeSet; -use substrate_fixed::types::{I96F32, U64F64}; +use substrate_fixed::types::I96F32; use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; use subtensor_swap_interface::SwapHandler; @@ -758,6 +758,7 @@ fn test_claim_root_with_drain_emissions_and_swap_claim_type() { }); } +/// cargo test --package pallet-subtensor --lib -- tests::claim_root::test_claim_root_with_run_coinbase --exact --nocapture #[test] fn test_claim_root_swap_failure_does_not_consume_claim() { new_test_ext(1).execute_with(|| { @@ -885,10 +886,15 @@ fn test_claim_root_with_run_coinbase() { // Set moving price > 1.0 and price > 1.0 // So we turn ON root sell SubnetMovingPrice::::insert(netuid, I96F32::from_num(2)); - pallet_subtensor_swap::AlphaSqrtPrice::::insert( - netuid, - U64F64::saturating_from_num(10.0), - ); + let tao = TaoBalance::from(10_000_000_000_000_u64); + let alpha = AlphaBalance::from(1_000_000_000_000_u64); + SubnetTAO::::insert(netuid, tao); + SubnetAlphaIn::::insert(netuid, alpha); + let current_price = + ::SwapInterface::current_alpha_price(netuid.into()) + .saturating_to_num::(); + assert_eq!(current_price, 10.0f64); + RootClaimableThreshold::::insert(netuid, I96F32::from_num(0)); // Make sure we are root selling, so we have root alpha divs. let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&[netuid]); @@ -901,6 +907,9 @@ fn test_claim_root_with_run_coinbase() { .into(); assert_eq!(initial_stake, 0u64); + // To trigger the epoch, block should be > tempo. So we advance it before + System::set_block_number(2); + let block_emissions = SubtensorModule::mint_tao(1_000_000u64.into()); SubtensorModule::run_coinbase(block_emissions); @@ -997,10 +1006,15 @@ fn test_claim_root_with_block_emissions() { // Set moving price > 1.0 and price > 1.0 // So we turn ON root sell SubnetMovingPrice::::insert(netuid, I96F32::from_num(2)); - pallet_subtensor_swap::AlphaSqrtPrice::::insert( - netuid, - U64F64::saturating_from_num(10.0), - ); + let tao = TaoBalance::from(10_000_000_000_000_u64); + let alpha = AlphaBalance::from(1_000_000_000_000_u64); + SubnetTAO::::insert(netuid, tao); + SubnetAlphaIn::::insert(netuid, alpha); + let current_price = + ::SwapInterface::current_alpha_price(netuid.into()) + .saturating_to_num::(); + assert_eq!(current_price, 10.0f64); + RootClaimableThreshold::::insert(netuid, I96F32::from_num(0)); // Make sure we are root selling, so we have root alpha divs. let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&[netuid]); @@ -1087,6 +1101,7 @@ fn test_populate_staking_maps() { }); } +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::claim_root::test_claim_root_coinbase_distribution --exact --show-output #[test] fn test_claim_root_coinbase_distribution() { new_test_ext(1).execute_with(|| { @@ -1095,7 +1110,10 @@ fn test_claim_root_coinbase_distribution() { let coldkey = U256::from(1003); let netuid = add_dynamic_network(&hotkey, &owner_coldkey); - Tempo::::insert(netuid, 1); + // Period is `tempo`; with `tempo = 2` and the scheduler re-anchored at the + // current block, the epoch fires two steps later (at `run_to_block(3)`). + Tempo::::insert(netuid, 2); + crate::LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 let root_stake = 200_000_000u64; @@ -1117,16 +1135,21 @@ fn test_claim_root_coinbase_distribution() { initial_total_hotkey_alpha.into(), ); - let initial_alpha_issuance = SubtensorModule::get_alpha_issuance(netuid); - let alpha_emissions: AlphaBalance = 1_000_000_000u64.into(); - // Set moving price > 1.0 and price > 1.0 // So we turn ON root sell SubnetMovingPrice::::insert(netuid, I96F32::from_num(2)); - pallet_subtensor_swap::AlphaSqrtPrice::::insert( - netuid, - U64F64::saturating_from_num(10.0), - ); + let tao = TaoBalance::from(100_000_000_000_u64); + let alpha = AlphaBalance::from(100_000_000_000_u64); + SubnetTAO::::insert(netuid, tao); + SubnetAlphaIn::::insert(netuid, alpha); + // let current_price = + // ::SwapInterface::current_alpha_price(netuid.into()) + // .saturating_to_num::(); + // assert_eq!(current_price, 2.0f64); + RootClaimableThreshold::::insert(netuid, I96F32::from_num(0)); + + let initial_alpha_issuance = SubtensorModule::get_alpha_issuance(netuid); + let alpha_emissions: AlphaBalance = 1_000_000_000u64.into(); // Make sure we are root selling, so we have root alpha divs. let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&[netuid]); diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index 45260ef8fc..02d1865905 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -12,8 +12,6 @@ use crate::*; use alloc::collections::BTreeMap; use approx::assert_abs_diff_eq; use frame_support::assert_ok; -use pallet_subtensor_swap::position::PositionId; -use safe_math::FixedExt; use sp_core::U256; use substrate_fixed::{ transcendental::sqrt, @@ -398,20 +396,8 @@ fn test_coinbase_tao_issuance_different_prices() { mock::setup_reserves(netuid2, initial_tao.into(), initial_alpha2.into()); // Force the swap to initialize - SubtensorModule::swap_tao_for_alpha( - netuid1, - TaoBalance::ZERO, - 1_000_000_000_000_u64.into(), - false, - ) - .unwrap(); - SubtensorModule::swap_tao_for_alpha( - netuid2, - TaoBalance::ZERO, - 1_000_000_000_000_u64.into(), - false, - ) - .unwrap(); + ::SwapInterface::init_swap(netuid1, None); + ::SwapInterface::init_swap(netuid2, None); // Make subnets dynamic. SubnetMechanism::::insert(netuid1, 1); @@ -474,20 +460,8 @@ fn test_coinbase_tao_issuance_different_prices() { // mock::setup_reserves(netuid2, initial_tao.into(), initial_alpha2.into()); // // Force the swap to initialize -// SubtensorModule::swap_tao_for_alpha( -// netuid1, -// TaoBalance::ZERO, -// 1_000_000_000_000.into(), -// false, -// ) -// .unwrap(); -// SubtensorModule::swap_tao_for_alpha( -// netuid2, -// TaoBalance::ZERO, -// 1_000_000_000_000.into(), -// false, -// ) -// .unwrap(); +// ::SwapInterface::init_swap(netuid1); +// ::SwapInterface::init_swap(netuid2); // // Set subnet prices to reversed proportion to ensure they don't affect emissions. // SubnetMovingPrice::::insert(netuid1, I96F32::from_num(2)); @@ -575,7 +549,7 @@ fn test_coinbase_moving_prices() { // Run moving 1 times. SubtensorModule::update_moving_price(netuid); // Assert price is ~ 100% of the real price. - assert!(U96F32::from_num(1.0) - SubtensorModule::get_moving_alpha_price(netuid) < 0.05); + assert!(U64F64::from_num(1.0) - SubtensorModule::get_moving_alpha_price(netuid) < 0.05); // Set price to zero. SubnetMovingPrice::::insert(netuid, I96F32::from_num(0)); SubnetMovingAlpha::::set(I96F32::from_num(0.1)); @@ -796,20 +770,8 @@ fn test_coinbase_alpha_issuance_with_cap_trigger_and_block_emission() { SubnetTaoFlow::::insert(netuid2, 200_000_000_i64); // Force the swap to initialize - SubtensorModule::swap_tao_for_alpha( - netuid1, - TaoBalance::ZERO, - 1_000_000_000_000_u64.into(), - false, - ) - .unwrap(); - SubtensorModule::swap_tao_for_alpha( - netuid2, - TaoBalance::ZERO, - 1_000_000_000_000_u64.into(), - false, - ) - .unwrap(); + ::SwapInterface::init_swap(netuid1, None); + ::SwapInterface::init_swap(netuid2, None); // Get the prices before the run_coinbase let price_1_before = ::SwapInterface::current_alpha_price(netuid1); @@ -863,7 +825,7 @@ fn test_owner_cut_base() { 1_000_000_000_000_u64.into(), 1_000_000_000_000_u64.into(), ); - SubtensorModule::set_tempo(netuid, 10000); // Large number (dont drain) + SubtensorModule::set_tempo_unchecked(netuid, 10000); // Large number (dont drain) SubtensorModule::set_subnet_owner_cut(0); SubtensorModule::run_coinbase(SubtensorModule::mint_tao(0.into())); assert_eq!(PendingOwnerCut::::get(netuid), 0.into()); // No cut @@ -873,7 +835,7 @@ fn test_owner_cut_base() { }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_pending_swapped --exact --show-output --nocapture +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_pending_emission --exact --show-output --nocapture #[test] fn test_pending_emission() { new_test_ext(1).execute_with(|| { @@ -885,10 +847,13 @@ fn test_pending_emission() { FirstEmissionBlockNumber::::insert(netuid, 0); mock::setup_reserves(netuid, 1_000_000.into(), 1.into()); + LastEpochBlock::::insert(netuid, 0); + System::set_block_number(10); SubtensorModule::run_coinbase(SubtensorModule::mint_tao(0.into())); SubnetTAO::::insert(NetUid::ROOT, TaoBalance::from(1_000_000_000)); // Add root weight. + System::set_block_number(12); SubtensorModule::run_coinbase(SubtensorModule::mint_tao(0.into())); - SubtensorModule::set_tempo(netuid, 10000); // Large number (dont drain) + SubtensorModule::set_tempo_unchecked(netuid, 10000); // Large number (dont drain) SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 // Set moving price > 1.0 @@ -2665,7 +2630,7 @@ fn test_distribute_emission_zero_emission() { let miner_ck = U256::from(6); let init_stake: u64 = 100_000_000_000_000; let tempo = 2; - SubtensorModule::set_tempo(netuid, tempo); + SubtensorModule::set_tempo_unchecked(netuid, tempo); // Set weight-set limit to 0. SubtensorModule::set_weights_set_rate_limit(netuid, 0); @@ -2753,7 +2718,7 @@ fn test_run_coinbase_not_started() { let miner_ck = U256::from(6); let init_stake: u64 = 100_000_000_000_000; let tempo = 2; - SubtensorModule::set_tempo(netuid, tempo); + SubtensorModule::set_tempo_unchecked(netuid, tempo); // Set weight-set limit to 0. SubtensorModule::set_weights_set_rate_limit(netuid, 0); @@ -2848,7 +2813,7 @@ fn test_run_coinbase_not_started_start_after() { let miner_ck = U256::from(6); let init_stake: u64 = 100_000_000_000_000; let tempo = 2; - SubtensorModule::set_tempo(netuid, tempo); + SubtensorModule::set_tempo_unchecked(netuid, tempo); // Set weight-set limit to 0. SubtensorModule::set_weights_set_rate_limit(netuid, 0); @@ -2916,6 +2881,12 @@ fn test_run_coinbase_not_started_start_after() { Some(current_block + 1) ); + // Advance the block past `LastEpochBlock + tempo` so the state-based + // scheduler is due again (the previous `run_coinbase` advanced it). + next_block_no_epoch(netuid); + next_block_no_epoch(netuid); + next_block_no_epoch(netuid); + // Run coinbase with emission. let emission_credit = SubtensorModule::mint_tao(100_000_000.into()); SubtensorModule::run_coinbase(emission_credit); @@ -2930,55 +2901,6 @@ fn test_run_coinbase_not_started_start_after() { }); } -/// Test that coinbase updates protocol position liquidity -/// cargo test --package pallet-subtensor --lib -- tests::coinbase::test_coinbase_v3_liquidity_update --exact --show-output -#[test] -fn test_coinbase_v3_liquidity_update() { - new_test_ext(1).execute_with(|| { - let owner_hotkey = U256::from(1); - let owner_coldkey = U256::from(2); - - // add network - let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - - // Force the swap to initialize - SubtensorModule::swap_tao_for_alpha( - netuid, - TaoBalance::ZERO, - 1_000_000_000_000_u64.into(), - false, - ) - .unwrap(); - - let protocol_account_id = pallet_subtensor_swap::Pallet::::protocol_account_id(); - let position = pallet_subtensor_swap::Positions::::get(( - netuid, - protocol_account_id, - PositionId::from(1), - )) - .unwrap(); - let liquidity_before = position.liquidity; - - // Enable emissions and run coinbase (which will increase position liquidity) - let emission: u64 = 1_234_567; - let emission_credit = SubtensorModule::mint_tao(emission.into()); - // Set the TAO flow to non-zero - SubnetTaoFlow::::insert(netuid, 8348383_i64); - FirstEmissionBlockNumber::::insert(netuid, 0); - SubtensorModule::run_coinbase(emission_credit); - - let position_after = pallet_subtensor_swap::Positions::::get(( - netuid, - protocol_account_id, - PositionId::from(1), - )) - .unwrap(); - let liquidity_after = position_after.liquidity; - - assert!(liquidity_before < liquidity_after); - }); -} - // SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_drain_alpha_childkey_parentkey_with_burn --exact --show-output --nocapture #[test] fn test_drain_alpha_childkey_parentkey_with_burn() { @@ -3179,6 +3101,7 @@ fn test_zero_shares_zero_emission() { }); } +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::coinbase::test_mining_emission_distribution_with_no_root_sell --exact --show-output --nocapture #[test] fn test_mining_emission_distribution_with_no_root_sell() { new_test_ext(1).execute_with(|| { @@ -3272,11 +3195,8 @@ fn test_mining_emission_distribution_with_no_root_sell() { // Make root sell NOT happen // set price very low, e.g. a lot of alpha in - //SubnetAlphaIn::::insert(netuid, AlphaBalance::from(1_000_000_000_000_000)); - pallet_subtensor_swap::AlphaSqrtPrice::::insert( - netuid, - U64F64::saturating_from_num(0.01), - ); + let alpha = AlphaBalance::from(1_000_000_000_000_000_000_u64); + SubnetAlphaIn::::insert(netuid, alpha); // Make sure we ARE NOT root selling, so we do not have root alpha divs. let root_sell_flag = SubtensorModule::get_network_root_sell_flag(&[netuid]); @@ -3306,17 +3226,20 @@ fn test_mining_emission_distribution_with_no_root_sell() { AlphaBalance::ZERO, "Root alpha divs should be zero" ); + step_block(1); + // Drain to a clean epoch boundary so accumulation starts fresh. + step_epochs(1, netuid); let miner_stake_before_epoch = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( &miner_hotkey, &miner_coldkey, netuid, ); // Run again but with some root stake - step_block(subnet_tempo - 2); + step_block(subnet_tempo - 1); assert_abs_diff_eq!( PendingServerEmission::::get(netuid).to_u64(), U96F32::saturating_from_num(per_block_emission) - .saturating_mul(U96F32::saturating_from_num(subnet_tempo as u64)) + .saturating_mul(U96F32::saturating_from_num((subnet_tempo - 1) as u64)) .saturating_mul(U96F32::saturating_from_num(0.5)) // miner cut .saturating_mul(U96F32::saturating_from_num(0.90)) .saturating_to_num::(), @@ -3363,7 +3286,7 @@ fn test_mining_emission_distribution_with_no_root_sell() { U96F32::saturating_from_num(miner_incentive) .saturating_div(u16::MAX.into()) .saturating_mul(U96F32::saturating_from_num(per_block_emission)) - .saturating_mul(U96F32::saturating_from_num(subnet_tempo + 1)) + .saturating_mul(U96F32::saturating_from_num(subnet_tempo)) .saturating_mul(U96F32::saturating_from_num(0.45)) // miner cut .saturating_to_num::(), epsilon = 1_000_000_u64 @@ -3394,7 +3317,9 @@ fn test_mining_emission_distribution_with_root_sell() { let owner_hotkey = U256::from(10); let owner_coldkey = U256::from(11); let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - Tempo::::insert(netuid, 1); + // Period is `tempo`; `tempo = 2` keeps a one-block gap between epochs so + // pending root-alpha-divs can be observed accumulating before a drain. + Tempo::::insert(netuid, 2); FirstEmissionBlockNumber::::insert(netuid, 0); // Setup large LPs to prevent slippage @@ -3468,10 +3393,8 @@ fn test_mining_emission_distribution_with_root_sell() { // Make root sell happen // Set moving price > 1.0 // Set price > 1.0 - pallet_subtensor_swap::AlphaSqrtPrice::::insert( - netuid, - U64F64::saturating_from_num(10.0), - ); + let alpha = AlphaBalance::from(100_000_000_000_000_u64); + SubnetAlphaIn::::insert(netuid, alpha); SubnetMovingPrice::::insert(netuid, I96F32::from_num(2)); @@ -3482,6 +3405,7 @@ fn test_mining_emission_distribution_with_root_sell() { // Run run_coinbase until emissions are drained step_block(subnet_tempo); + LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); let old_root_alpha_divs = PendingRootAlphaDivs::::get(netuid); let miner_stake_before_epoch = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( &miner_hotkey, @@ -3537,7 +3461,7 @@ fn test_mining_emission_distribution_with_root_sell() { U96F32::saturating_from_num(miner_incentive) .saturating_div(u16::MAX.into()) .saturating_mul(U96F32::saturating_from_num(per_block_emission)) - .saturating_mul(U96F32::saturating_from_num(subnet_tempo + 1)) + .saturating_mul(U96F32::saturating_from_num(subnet_tempo)) .saturating_mul(U96F32::saturating_from_num(0.45)) // miner cut .saturating_to_num::(), epsilon = 1_000_000_u64 @@ -3596,8 +3520,8 @@ fn test_coinbase_subnet_terms_with_alpha_in_gt_alpha_emission() { TaoBalance::from(1_000_000_000_000_000_u64), AlphaBalance::from(1_000_000_000_000_000_u64), ); - // Initialize swap v3 - Swap::maybe_initialize_v3(netuid0); + // Initialize swap + Swap::maybe_initialize_palswap(netuid0, None); // Set netuid0 to have price tao_emission / price > alpha_emission let alpha_emission = U96F32::saturating_from_num( @@ -3608,14 +3532,19 @@ fn test_coinbase_subnet_terms_with_alpha_in_gt_alpha_emission() { ); let price_to_set: U64F64 = U64F64::saturating_from_num(0.01); let price_to_set_fixed: U96F32 = U96F32::saturating_from_num(price_to_set); - let sqrt_price_to_set: U64F64 = sqrt(price_to_set).unwrap(); let tao_emission: U96F32 = U96F32::saturating_from_num(alpha_emission) .saturating_mul(price_to_set_fixed) .saturating_add(U96F32::saturating_from_num(0.01)); // Set the price - pallet_subtensor_swap::AlphaSqrtPrice::::insert(netuid0, sqrt_price_to_set); + let tao = TaoBalance::from(1_000_000_000_u64); + let alpha = AlphaBalance::from( + (U64F64::saturating_from_num(u64::from(tao)) / price_to_set).to_num::(), + ); + SubnetTAO::::insert(netuid0, tao); + SubnetAlphaIn::::insert(netuid0, alpha); + // Check the price is set assert_abs_diff_eq!( pallet_subtensor_swap::Pallet::::current_alpha_price(netuid0).to_num::(), @@ -3672,8 +3601,8 @@ fn test_coinbase_subnet_terms_with_alpha_in_lte_alpha_emission() { TaoBalance::from(1_000_000_000_000_000_u64), AlphaBalance::from(1_000_000_000_000_000_u64), ); - // Initialize swap v3 - Swap::maybe_initialize_v3(netuid0); + // Initialize swap + Swap::maybe_initialize_palswap(netuid0, None); let alpha_emission = U96F32::saturating_from_num( SubtensorModule::get_block_emission_for_issuance( @@ -3683,7 +3612,7 @@ fn test_coinbase_subnet_terms_with_alpha_in_lte_alpha_emission() { ); let tao_emission = U96F32::saturating_from_num(34566756_u64); - let price: U96F32 = Swap::current_alpha_price(netuid0); + let price: U96F32 = U96F32::saturating_from_num(Swap::current_alpha_price(netuid0)); let subnet_emissions = BTreeMap::from([(netuid0, tao_emission)]); @@ -3735,8 +3664,8 @@ fn test_coinbase_inject_and_maybe_swap_does_not_skew_reserves() { TaoBalance::from(1_000_000_000_000_000_u64), AlphaBalance::from(1_000_000_000_000_000_u64), ); - // Initialize swap v3 - Swap::maybe_initialize_v3(netuid0); + // Initialize swap + Swap::maybe_initialize_palswap(netuid0, None); let tao_in = BTreeMap::from([(netuid0, U96F32::saturating_from_num(123))]); let alpha_in = BTreeMap::from([(netuid0, U96F32::saturating_from_num(456))]); @@ -3791,8 +3720,8 @@ fn test_coinbase_drain_pending_resets_blockssincelaststep() { let zero = U96F32::saturating_from_num(0); let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); Tempo::::insert(netuid0, 100); - // Ensure the block number we use is the tempo block - let block_number = 98; + LastEpochBlock::::insert(netuid0, 0); + let block_number = 102; assert!(SubtensorModule::should_run_epoch(netuid0, block_number)); let blocks_since_last_step_before = 12345678; @@ -3804,8 +3733,7 @@ fn test_coinbase_drain_pending_resets_blockssincelaststep() { let blocks_since_last_step_after = BlocksSinceLastStep::::get(netuid0); assert_eq!(blocks_since_last_step_after, 0); - // Also check LastMechansimStepBlock is set to the block number we ran on - assert_eq!(LastMechansimStepBlock::::get(netuid0), block_number); + assert_eq!(LastMechansimStepBlock::::get(netuid0), 12345); }); } @@ -3815,8 +3743,8 @@ fn test_coinbase_drain_pending_gets_counters_and_resets_them() { let zero = U96F32::saturating_from_num(0); let netuid0 = add_dynamic_network(&U256::from(1), &U256::from(2)); Tempo::::insert(netuid0, 100); - // Ensure the block number we use is the tempo block - let block_number = 98; + LastEpochBlock::::insert(netuid0, 0); + let block_number = 102; assert!(SubtensorModule::should_run_epoch(netuid0, block_number)); let pending_server_em = AlphaBalance::from(123434534); @@ -3870,8 +3798,8 @@ fn test_coinbase_emit_to_subnets_with_no_root_sell() { TaoBalance::from(1_000_000_000_000_000_u64), AlphaBalance::from(1_000_000_000_000_000_u64), ); - // Initialize swap v3 - Swap::maybe_initialize_v3(netuid0); + // Initialize swap + Swap::maybe_initialize_palswap(netuid0, None); let tao_emission = U96F32::saturating_from_num(12345678); let subnet_emissions = BTreeMap::from([(netuid0, tao_emission)]); @@ -3885,7 +3813,7 @@ fn test_coinbase_emit_to_subnets_with_no_root_sell() { ) .unwrap_or(0), ); - let price: U96F32 = Swap::current_alpha_price(netuid0); + let price: U96F32 = U96F32::saturating_from_num(Swap::current_alpha_price(netuid0)); let (tao_in, alpha_in, alpha_out, excess_tao) = SubtensorModule::get_subnet_terms(&subnet_emissions); // Based on the price, we should have NO excess TAO @@ -3962,8 +3890,8 @@ fn test_coinbase_emit_to_subnets_with_root_sell() { TaoBalance::from(1_000_000_000_000_000_u64), AlphaBalance::from(1_000_000_000_000_000_u64), ); - // Initialize swap v3 - Swap::maybe_initialize_v3(netuid0); + // Initialize swap + Swap::maybe_initialize_palswap(netuid0, None); let tao_emission = U96F32::saturating_from_num(12345678); let subnet_emissions = BTreeMap::from([(netuid0, tao_emission)]); @@ -3977,7 +3905,7 @@ fn test_coinbase_emit_to_subnets_with_root_sell() { ) .unwrap_or(0), ); - let price: U96F32 = Swap::current_alpha_price(netuid0); + let price: U96F32 = U96F32::saturating_from_num(Swap::current_alpha_price(netuid0)); let (tao_in, alpha_in, alpha_out, excess_tao) = SubtensorModule::get_subnet_terms(&subnet_emissions); // Based on the price, we should have NO excess TAO @@ -4054,10 +3982,11 @@ fn test_disabling_owner_cut_sends_subnet_emission_to_miners_and_validators() { let miner_coldkey = U256::from(5); let miner_hotkey = U256::from(6); let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); let subnet_tempo = 10; let stake = 100_000_000_000u64; - SubtensorModule::set_tempo(netuid, subnet_tempo); + SubtensorModule::set_tempo_unchecked(netuid, subnet_tempo); setup_reserves(netuid, (stake * 10_000).into(), (stake * 10_000).into()); register_ok_neuron(netuid, validator_hotkey, validator_coldkey, 0); @@ -4210,10 +4139,10 @@ fn test_pending_emission_start_call_not_done() { // Make root sell happen // Set moving price > 1.0 // Set price > 1.0 - pallet_subtensor_swap::AlphaSqrtPrice::::insert( - netuid, - U64F64::saturating_from_num(10.0), - ); + let tao = TaoBalance::from(10_000_000_000_u64); + let alpha = AlphaBalance::from(1_000_000_000_u64); + SubnetTAO::::insert(netuid, tao); + SubnetAlphaIn::::insert(netuid, alpha); SubnetMovingPrice::::insert(netuid, I96F32::from_num(2)); @@ -4339,3 +4268,103 @@ fn test_get_subnet_terms_alpha_emissions_cap() { assert_eq!(alpha_in.get(&netuid).copied().unwrap(), tao_block_emission); }); } + +#[test] +fn test_epochs_deferred_this_block_respects_cap() { + new_test_ext(1).execute_with(|| { + let cap = ::MaxEpochsPerBlock::get() as usize; + let n = cap + 2; + + for i in 0..n { + let netuid = NetUid::from((i + 1) as u16); + add_network(netuid, 100, 0); + // Force "due this block". + PendingEpochAt::::insert(netuid, 1); + } + + let block = SubtensorModule::get_current_block_as_u64(); + let subnets: Vec = SubtensorModule::get_all_subnet_netuids() + .into_iter() + .filter(|x| *x != NetUid::ROOT) + .collect(); + + // All `n` subnets are due, but only `cap` may fire — the rest are deferred. + let deferred = SubtensorModule::epochs_deferred_this_block(&subnets, block); + assert_eq!( + deferred.len(), + n - cap, + "exactly the due subnets beyond MaxEpochsPerBlock are deferred" + ); + for netuid in &deferred { + assert!(SubtensorModule::should_run_epoch(*netuid, block)); + } + }); +} + +// Regression test for the dynamic-tempo / CR-v3 interaction: when a subnet's epoch +// is deferred by the per-block cap, its timelock reveal must be held back to the +// deferred fire-block (not run on the originally-scheduled block, which would +// surface weights before the epoch consumes them). +// +// Crypto-free probe: the reveal path removes *expired* commits only when it runs +// for a subnet, so a retained expired (epoch-0) commit means the reveal was skipped. +#[test] +fn test_reveal_crv3_defers_with_capped_epoch() { + new_test_ext(1).execute_with(|| { + let cap = ::MaxEpochsPerBlock::get() as usize; + let n = cap + 2; + let mec0 = subtensor_runtime_common::MechId::from(0); + + for i in 0..n { + let netuid = NetUid::from((i + 1) as u16); + add_network(netuid, 100, 0); + PendingEpochAt::::insert(netuid, 1); // due this block + SubnetEpochIndex::::insert(netuid, 10); // cur_epoch >> reveal_period + // Plant an expired commit at epoch 0 (field types inferred from the queue). + let idx = SubtensorModule::get_mechanism_storage_index(netuid, mec0); + TimelockedWeightCommits::::mutate(idx, 0u64, |q| { + q.push_back((U256::from(1u64), 0u64, Default::default(), 0u64)); + }); + } + + let subnets: Vec = SubtensorModule::get_all_subnet_netuids() + .into_iter() + .filter(|x| *x != NetUid::ROOT) + .collect(); + + let still_holds = |netuid: NetUid| -> bool { + let idx = SubtensorModule::get_mechanism_storage_index(netuid, mec0); + TimelockedWeightCommits::::contains_key(idx, 0u64) + }; + let retained = |subnets: &[NetUid]| subnets.iter().filter(|n| still_holds(**n)).count(); + + // --- Phase 1: cap-deferred subnets must NOT reveal this block. + SubtensorModule::reveal_crv3_commits(); + assert_eq!( + retained(&subnets), + n - cap, + "only cap-deferred subnets keep their commit (their reveal was skipped)" + ); + + let deferred: Vec = subnets + .iter() + .copied() + .filter(|n| still_holds(*n)) + .collect(); + + // --- Phase 2: drop the cap pressure so only the deferred subnets are due; + // they should now reveal (and clean their expired commit). + for netuid in &subnets { + if !deferred.contains(netuid) { + PendingEpochAt::::insert(*netuid, 0); + LastEpochBlock::::insert(*netuid, 1); // blocks_since < tempo => not due + } + } + SubtensorModule::reveal_crv3_commits(); + assert_eq!( + retained(&subnets), + 0, + "deferred subnets reveal once they actually fire" + ); + }); +} diff --git a/pallets/subtensor/src/tests/emission.rs b/pallets/subtensor/src/tests/emission.rs index ecd2df544b..151fd3cddb 100644 --- a/pallets/subtensor/src/tests/emission.rs +++ b/pallets/subtensor/src/tests/emission.rs @@ -1,6 +1,7 @@ use subtensor_runtime_common::NetUid; use super::mock::*; +use crate::LastEpochBlock; // 1. Test Zero Tempo // Description: Verify that when tempo is 0, the function returns u64::MAX. @@ -9,7 +10,7 @@ use super::mock::*; fn test_zero_tempo() { new_test_ext(1).execute_with(|| { assert_eq!( - SubtensorModule::blocks_until_next_epoch(1.into(), 0, 100), + SubtensorModule::blocks_until_next_auto_epoch(1.into(), 0, 100), u64::MAX ); }); @@ -21,14 +22,21 @@ fn test_zero_tempo() { #[test] fn test_regular_case() { new_test_ext(1).execute_with(|| { - assert_eq!(SubtensorModule::blocks_until_next_epoch(1.into(), 10, 5), 3); + LastEpochBlock::::insert(NetUid::from(1), 0); + LastEpochBlock::::insert(NetUid::from(2), 0); + LastEpochBlock::::insert(NetUid::from(3), 0); + // (LastEpochBlock + tempo) - block. assert_eq!( - SubtensorModule::blocks_until_next_epoch(2.into(), 20, 15), - 2 + SubtensorModule::blocks_until_next_auto_epoch(1.into(), 10, 5), + 5 ); assert_eq!( - SubtensorModule::blocks_until_next_epoch(3.into(), 30, 25), - 1 + SubtensorModule::blocks_until_next_auto_epoch(2.into(), 20, 15), + 5 + ); + assert_eq!( + SubtensorModule::blocks_until_next_auto_epoch(3.into(), 30, 25), + 5 ); }); } @@ -39,12 +47,16 @@ fn test_regular_case() { #[test] fn test_boundary_conditions() { new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(u16::MAX); + LastEpochBlock::::insert(netuid, 0); + // Far past the next-auto block — saturating to 0. assert_eq!( - SubtensorModule::blocks_until_next_epoch(u16::MAX.into(), u16::MAX, u64::MAX), + SubtensorModule::blocks_until_next_auto_epoch(netuid, u16::MAX, u64::MAX), 0 ); + // Block 0 — full period until next auto epoch. assert_eq!( - SubtensorModule::blocks_until_next_epoch(u16::MAX.into(), u16::MAX, 0), + SubtensorModule::blocks_until_next_auto_epoch(netuid, u16::MAX, 0), u16::MAX as u64 ); }); @@ -56,9 +68,11 @@ fn test_boundary_conditions() { #[test] fn test_overflow_handling() { new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(u16::MAX); + LastEpochBlock::::insert(netuid, 0); assert_eq!( - SubtensorModule::blocks_until_next_epoch(u16::MAX.into(), u16::MAX, u64::MAX - 1), - 1 + SubtensorModule::blocks_until_next_auto_epoch(netuid, u16::MAX, u64::MAX - 1), + 0 ); }); } @@ -69,13 +83,17 @@ fn test_overflow_handling() { #[test] fn test_epoch_alignment() { new_test_ext(1).execute_with(|| { + LastEpochBlock::::insert(NetUid::from(1), 0); + LastEpochBlock::::insert(NetUid::from(2), 0); + // (LastEpochBlock + tempo) - block_number. assert_eq!( - SubtensorModule::blocks_until_next_epoch(1.into(), 10, 9), - 10 + SubtensorModule::blocks_until_next_auto_epoch(1.into(), 10, 9), + 1 ); + // Block exactly at next-auto — returns 0. assert_eq!( - SubtensorModule::blocks_until_next_epoch(2.into(), 20, 21), - 17 + SubtensorModule::blocks_until_next_auto_epoch(2.into(), 20, 21), + 0 ); }); } @@ -86,9 +104,23 @@ fn test_epoch_alignment() { #[test] fn test_different_network_ids() { new_test_ext(1).execute_with(|| { - assert_eq!(SubtensorModule::blocks_until_next_epoch(1.into(), 10, 5), 3); - assert_eq!(SubtensorModule::blocks_until_next_epoch(2.into(), 10, 5), 2); - assert_eq!(SubtensorModule::blocks_until_next_epoch(3.into(), 10, 5), 1); + // Anchor each subnet identically — proves the new formula does NOT + // depend on `netuid` (only on the per-subnet `LastEpochBlock`). + LastEpochBlock::::insert(NetUid::from(1), 0); + LastEpochBlock::::insert(NetUid::from(2), 0); + LastEpochBlock::::insert(NetUid::from(3), 0); + assert_eq!( + SubtensorModule::blocks_until_next_auto_epoch(1.into(), 10, 5), + 5 + ); + assert_eq!( + SubtensorModule::blocks_until_next_auto_epoch(2.into(), 10, 5), + 5 + ); + assert_eq!( + SubtensorModule::blocks_until_next_auto_epoch(3.into(), 10, 5), + 5 + ); }); } @@ -98,9 +130,11 @@ fn test_different_network_ids() { #[test] fn test_large_tempo_values() { new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + LastEpochBlock::::insert(netuid, 0); assert_eq!( - SubtensorModule::blocks_until_next_epoch(1.into(), u16::MAX - 1, 100), - u16::MAX as u64 - 103 + SubtensorModule::blocks_until_next_auto_epoch(netuid, u16::MAX - 1, 100), + (u16::MAX as u64).saturating_sub(1).saturating_sub(100) ); }); } @@ -113,9 +147,11 @@ fn test_consecutive_blocks() { new_test_ext(1).execute_with(|| { let tempo = 10; let netuid = NetUid::from(1); - let mut last_result = SubtensorModule::blocks_until_next_epoch(netuid, tempo, 0); + LastEpochBlock::::insert(netuid, 0); + let mut last_result = SubtensorModule::blocks_until_next_auto_epoch(netuid, tempo, 0); for i in 1..tempo - 1 { - let current_result = SubtensorModule::blocks_until_next_epoch(netuid, tempo, i as u64); + let current_result = + SubtensorModule::blocks_until_next_auto_epoch(netuid, tempo, i as u64); assert_eq!(current_result, last_result - 1); last_result = current_result; } @@ -128,13 +164,16 @@ fn test_consecutive_blocks() { #[test] fn test_wrap_around_behavior() { new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + LastEpochBlock::::insert(netuid, 0); + // `next_auto - block_number` saturates to 0 for far-future blocks. assert_eq!( - SubtensorModule::blocks_until_next_epoch(1.into(), 10, u64::MAX), - 9 + SubtensorModule::blocks_until_next_auto_epoch(netuid, 10, u64::MAX), + 0 ); assert_eq!( - SubtensorModule::blocks_until_next_epoch(1.into(), 10, u64::MAX - 1), - 10 + SubtensorModule::blocks_until_next_auto_epoch(netuid, 10, u64::MAX - 1), + 0 ); }); } diff --git a/pallets/subtensor/src/tests/ensure.rs b/pallets/subtensor/src/tests/ensure.rs index 1253285306..008be48b15 100644 --- a/pallets/subtensor/src/tests/ensure.rs +++ b/pallets/subtensor/src/tests/ensure.rs @@ -66,16 +66,22 @@ fn ensure_subnet_owner_or_root_distinguishes_root_and_owner() { fn ensure_admin_window_open_blocks_in_freeze_window() { new_test_ext(1).execute_with(|| { let netuid = NetUid::from(0); - let tempo = 10; - add_network(netuid, 10, 0); + let tempo: u16 = 10; + add_network(netuid, tempo, 0); - let freeze_window = 3; + let freeze_window: u16 = 3; crate::Pallet::::set_admin_freeze_window(freeze_window); - System::set_block_number((tempo - freeze_window).into()); + crate::LastEpochBlock::::insert(netuid, 0); + // Period is `tempo`: next auto-epoch fires at `LastEpochBlock + tempo`. + let next_auto = tempo as u64; + + // Inside freeze window: `next_auto - freeze_window + 1`. + System::set_block_number(next_auto - freeze_window as u64 + 1); assert!(crate::Pallet::::ensure_admin_window_open(netuid).is_err()); - System::set_block_number((tempo - freeze_window - 1).into()); + // Outside freeze window: `next_auto - freeze_window`. + System::set_block_number(next_auto - freeze_window as u64); assert!(crate::Pallet::::ensure_admin_window_open(netuid).is_ok()); }); } @@ -93,7 +99,7 @@ fn ensure_owner_or_root_with_limits_checks_rl_and_freeze() { crate::Pallet::::set_admin_freeze_window(0); // Set tempo to 1 so owner hyperparam RL = 2 blocks - crate::Pallet::::set_tempo(netuid, 1); + crate::Pallet::::set_tempo_unchecked(netuid, 1); assert_eq!(OwnerHyperparamRateLimit::::get(), 2); @@ -135,12 +141,12 @@ fn ensure_owner_or_root_with_limits_checks_rl_and_freeze() { // (using loop for clarity, because epoch calculation function uses netuid) // Restore tempo and configure freeze window for this part let freeze_window = 3; - crate::Pallet::::set_tempo(netuid, tempo); + crate::Pallet::::set_tempo_unchecked(netuid, tempo); crate::Pallet::::set_admin_freeze_window(freeze_window); let freeze_window = freeze_window as u64; loop { let cur = crate::Pallet::::get_current_block_as_u64(); - let rem = crate::Pallet::::blocks_until_next_epoch(netuid, tempo, cur); + let rem = crate::Pallet::::blocks_until_next_auto_epoch(netuid, tempo, cur); if rem < freeze_window { break; } diff --git a/pallets/subtensor/src/tests/epoch.rs b/pallets/subtensor/src/tests/epoch.rs index 02236d892d..b0383521a8 100644 --- a/pallets/subtensor/src/tests/epoch.rs +++ b/pallets/subtensor/src/tests/epoch.rs @@ -2052,14 +2052,14 @@ fn test_deregistered_miner_bonds() { } // Set tempo high so we don't automatically run epochs - SubtensorModule::set_tempo(netuid, high_tempo); + SubtensorModule::set_tempo_unchecked(netuid, high_tempo); // Run 2 blocks next_block(); next_block(); // set tempo to 2 blocks - SubtensorModule::set_tempo(netuid, 2); + SubtensorModule::set_tempo_unchecked(netuid, 2); // Run epoch if sparse { SubtensorModule::epoch(netuid, 1_000_000_000.into()); @@ -2077,7 +2077,7 @@ fn test_deregistered_miner_bonds() { assert!(bond_0_3 > 0); // Set tempo high so we don't automatically run epochs - SubtensorModule::set_tempo(netuid, high_tempo); + SubtensorModule::set_tempo_unchecked(netuid, high_tempo); // Run one more block next_block(); @@ -2137,7 +2137,7 @@ fn test_deregistered_miner_bonds() { ); // set tempo to 2 blocks - SubtensorModule::set_tempo(netuid, 2); + SubtensorModule::set_tempo_unchecked(netuid, 2); // Run epoch again. if sparse { SubtensorModule::epoch(netuid, 1_000_000_000.into()); @@ -2465,7 +2465,7 @@ fn test_blocks_since_last_step() { assert!(new_blocks > original_blocks); assert_eq!(new_blocks, 5); - let blocks_to_step: u16 = SubtensorModule::blocks_until_next_epoch( + let blocks_to_step: u16 = SubtensorModule::blocks_until_next_auto_epoch( netuid, tempo, SubtensorModule::get_current_block_as_u64(), @@ -2477,7 +2477,7 @@ fn test_blocks_since_last_step() { assert_eq!(post_blocks, 10); - let blocks_to_step: u16 = SubtensorModule::blocks_until_next_epoch( + let blocks_to_step: u16 = SubtensorModule::blocks_until_next_auto_epoch( netuid, tempo, SubtensorModule::get_current_block_as_u64(), @@ -3784,7 +3784,7 @@ fn test_epoch_does_not_mask_outside_window_but_masks_inside() { SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); SubtensorModule::set_max_allowed_validators(netuid, 1); - run_to_block(tempo as u64 + 1); + run_to_block(tempo as u64); /* first commit */ commit_dummy(v_hot, netuid); @@ -3801,7 +3801,7 @@ fn test_epoch_does_not_mask_outside_window_but_masks_inside() { /* let first commit expire for UID‑1 */ for _ in 0..(reveal + 1) { - run_to_block(System::block_number() + tempo as u64 + 1); + run_to_block(System::block_number() + tempo as u64); } /* second commit — will mask UID‑2 & UID‑3 */ diff --git a/pallets/subtensor/src/tests/locks.rs b/pallets/subtensor/src/tests/locks.rs index 91b87a634f..4eaf01668c 100644 --- a/pallets/subtensor/src/tests/locks.rs +++ b/pallets/subtensor/src/tests/locks.rs @@ -49,7 +49,6 @@ fn setup_subnet_with_stake( amount, ::SwapInterface::max_price(), false, - false, ) .unwrap(); DecayingLock::::insert(coldkey, netuid, false); @@ -79,6 +78,7 @@ fn roll_forward_lock( owner_lock, perpetual_lock, ) + .0 } fn roll_forward_individual_lock( @@ -457,7 +457,6 @@ fn test_mixed_perpetual_and_decaying_non_owner_locks_same_hotkey_update_aggregat 100_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -862,6 +861,311 @@ fn test_available_to_unstake_fully_locked() { }); } +#[test] +fn test_stake_availability_for_coldkeys_empty_coldkeys() { + new_test_ext(1).execute_with(|| { + let result = SubtensorModule::get_stake_availability_for_coldkeys(Vec::new(), None); + assert!(result.is_empty()); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_empty_netuids() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let result = + SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(Vec::new())); + assert_eq!(result.len(), 1); + assert!(result.contains_key(&coldkey)); + assert!(result.get(&coldkey).unwrap().is_empty()); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_filters_empty_rows() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + let result = + SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(vec![netuid])); + + assert_eq!(result.len(), 1); + assert!(result.contains_key(&coldkey)); + assert!(result.get(&coldkey).unwrap().is_empty()); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_stake_without_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + + let result = + SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(vec![netuid])); + + assert_eq!(result.len(), 1); + let availability = result.get(&coldkey).unwrap().get(&netuid).unwrap(); + assert_eq!(availability.total(), total); + assert_eq!(availability.locked(), AlphaBalance::ZERO); + assert_eq!(availability.available(), total); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_partial_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + let lock_amount = total / 2.into(); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount, + )); + + let result = + SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(vec![netuid])); + let availability = result.get(&coldkey).unwrap().get(&netuid).unwrap(); + + assert_eq!(availability.total(), total); + assert_eq!( + availability.locked(), + SubtensorModule::get_current_locked(&coldkey, netuid) + ); + assert_eq!(availability.available(), total - availability.locked()); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_fully_locked() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey, total, + )); + + let result = + SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(vec![netuid])); + let availability = result.get(&coldkey).unwrap().get(&netuid).unwrap(); + + assert_eq!(availability.total(), total); + assert_eq!(availability.locked(), total); + assert_eq!(availability.available(), AlphaBalance::ZERO); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_preserves_coldkey_grouping() { + new_test_ext(1).execute_with(|| { + let coldkey_a = U256::from(1); + let hotkey_a = U256::from(2); + let coldkey_b = U256::from(3); + let hotkey_b = U256::from(4); + let netuid_a = setup_subnet_with_stake(coldkey_a, hotkey_a, 100_000_000_000); + let netuid_b = setup_subnet_with_stake(coldkey_b, hotkey_b, 100_000_000_000); + + let result = SubtensorModule::get_stake_availability_for_coldkeys( + vec![coldkey_a, coldkey_b], + Some(vec![netuid_a, netuid_b]), + ); + + assert_eq!(result.len(), 2); + assert_eq!(result.get(&coldkey_a).unwrap().len(), 1); + assert!(result.get(&coldkey_a).unwrap().contains_key(&netuid_a)); + assert_eq!(result.get(&coldkey_b).unwrap().len(), 1); + assert!(result.get(&coldkey_b).unwrap().contains_key(&netuid_b)); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_none_netuids_uses_all_subnets() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let result = SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], None); + + assert_eq!(result.len(), 1); + assert!(result.get(&coldkey).unwrap().contains_key(&netuid)); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_one_coldkey_two_subnets() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey_a = U256::from(2); + let hotkey_b = U256::from(3); + let netuid_a = setup_subnet_with_stake(coldkey, hotkey_a, 100_000_000_000); + let netuid_b = setup_subnet_with_stake(coldkey, hotkey_b, 100_000_000_000); + let total_a = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid_a); + let total_b = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid_b); + + let result = SubtensorModule::get_stake_availability_for_coldkeys( + vec![coldkey], + Some(vec![netuid_a, netuid_b]), + ); + + assert_eq!(result.len(), 1); + let subnets = result.get(&coldkey).unwrap(); + assert_eq!(subnets.len(), 2); + assert!(subnets.contains_key(&netuid_a)); + assert!(subnets.contains_key(&netuid_b)); + + let row_a = subnets.get(&netuid_a).unwrap(); + assert_eq!(row_a.total(), total_a); + assert_eq!(row_a.locked(), AlphaBalance::ZERO); + assert_eq!(row_a.available(), total_a); + + let row_b = subnets.get(&netuid_b).unwrap(); + assert_eq!(row_b.total(), total_b); + assert_eq!(row_b.locked(), AlphaBalance::ZERO); + assert_eq!(row_b.available(), total_b); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_filters_to_requested_netuid() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey_a = U256::from(2); + let hotkey_b = U256::from(3); + let netuid_a = setup_subnet_with_stake(coldkey, hotkey_a, 100_000_000_000); + let netuid_b = setup_subnet_with_stake(coldkey, hotkey_b, 100_000_000_000); + + let result = SubtensorModule::get_stake_availability_for_coldkeys( + vec![coldkey], + Some(vec![netuid_b]), + ); + + assert_eq!(result.len(), 1); + let subnets = result.get(&coldkey).unwrap(); + assert_eq!(subnets.len(), 1); + assert!(subnets.contains_key(&netuid_b)); + assert!(!subnets.contains_key(&netuid_a)); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_dedups_netuids() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + let result = SubtensorModule::get_stake_availability_for_coldkeys( + vec![coldkey], + Some(vec![netuid, netuid]), + ); + + assert_eq!(result.len(), 1); + assert_eq!(result.get(&coldkey).unwrap().len(), 1); + assert!(result.get(&coldkey).unwrap().contains_key(&netuid)); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_skips_nonexistent_netuid() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + let nonexistent = subtensor_runtime_common::NetUid::from(99); + + let result = SubtensorModule::get_stake_availability_for_coldkeys( + vec![coldkey], + Some(vec![nonexistent]), + ); + assert_eq!(result.len(), 1); + assert!(result.get(&coldkey).unwrap().is_empty()); + + // Mix real + fake requires at least two subnets on chain so len(requested) <= subnet_count. + let subnet_owner_coldkey = U256::from(2001); + let subnet_owner_hotkey = U256::from(2002); + let _other_netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + let result = SubtensorModule::get_stake_availability_for_coldkeys( + vec![coldkey], + Some(vec![netuid, nonexistent]), + ); + assert_eq!(result.len(), 1); + let subnets = result.get(&coldkey).unwrap(); + assert_eq!(subnets.len(), 1); + assert!(subnets.contains_key(&netuid)); + assert!(!subnets.contains_key(&nonexistent)); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_rejects_oversized_netuid_list() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + let subnet_count = SubtensorModule::get_all_subnet_netuids().len(); + let requested: Vec = (0..=subnet_count as u16) + .map(subtensor_runtime_common::NetUid::from) + .collect(); + + let result = + SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(requested)); + assert_eq!(result.len(), 1); + assert!(result.contains_key(&coldkey)); + assert!(result.get(&coldkey).unwrap().is_empty()); + + let result = + SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(vec![netuid])); + assert_eq!(result.get(&coldkey).unwrap().len(), 1); + assert!(result.get(&coldkey).unwrap().contains_key(&netuid)); + }); +} + +#[test] +fn test_stake_availability_for_coldkeys_uses_rolled_forward_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + let total = SubtensorModule::total_coldkey_alpha_on_subnet(&coldkey, netuid); + let lock_amount = total / 2.into(); + + DecayingLock::::remove(coldkey, netuid); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount, + )); + let raw_lock = Lock::::get((coldkey, netuid, hotkey)).unwrap(); + + step_block(1000); + + let result = + SubtensorModule::get_stake_availability_for_coldkeys(vec![coldkey], Some(vec![netuid])); + let availability = result.get(&coldkey).unwrap().get(&netuid).unwrap(); + let rolled_locked = SubtensorModule::get_current_locked(&coldkey, netuid); + + assert!(rolled_locked < raw_lock.locked_mass); + assert_eq!(availability.locked(), rolled_locked); + assert_eq!(availability.available(), total - rolled_locked); + }); +} + // ========================================================================= // GROUP 3: Incremental locks (top-up) // ========================================================================= @@ -976,6 +1280,83 @@ fn test_lock_stake_topup_same_block() { }); } +#[test] +fn test_locking_coldkeys_added_once_by_lock_stake() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + 100u64.into(), + )); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + 50u64.into(), + )); + + assert!(LockingColdkeys::::contains_key(( + netuid, hotkey, coldkey + ))); + assert_eq!( + LockingColdkeys::::iter_prefix((netuid, hotkey)).count(), + 1 + ); + }); +} + +#[test] +fn test_locking_coldkeys_removed_when_lock_is_fully_reduced() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + let amount = 100u64.into(); + + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, netuid, &hotkey, amount + )); + assert!(LockingColdkeys::::contains_key(( + netuid, hotkey, coldkey + ))); + + SubtensorModule::force_reduce_lock(&coldkey, netuid, amount); + + assert!(Lock::::get((coldkey, netuid, hotkey)).is_none()); + assert!(!LockingColdkeys::::contains_key(( + netuid, hotkey, coldkey + ))); + }); +} + +#[test] +fn test_lock_state_is_zero_uses_dust_threshold() { + let below_threshold = LockState { + locked_mass: AlphaBalance::from(99u64), + conviction: U64F64::from_num(99), + last_update: 0, + }; + let locked_mass_at_threshold = LockState { + locked_mass: AlphaBalance::from(100u64), + conviction: U64F64::from_num(99), + last_update: 0, + }; + let conviction_at_threshold = LockState { + locked_mass: AlphaBalance::from(99u64), + conviction: U64F64::from_num(100), + last_update: 0, + }; + + assert!(below_threshold.is_zero()); + assert!(!locked_mass_at_threshold.is_zero()); + assert!(!conviction_at_threshold.is_zero()); +} + // ========================================================================= // GROUP 4: Lock rejection cases // ========================================================================= @@ -1138,7 +1519,8 @@ fn test_roll_forward_individual_lock_uses_lock_owner_and_decay_mode() { MaturityRate::::get(), true, false, - ); + ) + .0; assert_eq!(rolled, expected); }); @@ -1162,7 +1544,8 @@ fn test_roll_forward_hotkey_lock_uses_perpetual_general_mode() { MaturityRate::::get(), false, true, - ); + ) + .0; assert_eq!(rolled, expected); }); @@ -1186,7 +1569,8 @@ fn test_roll_forward_decaying_hotkey_lock_uses_decaying_general_mode() { MaturityRate::::get(), false, false, - ); + ) + .0; assert_eq!(rolled, expected); }); @@ -1461,6 +1845,23 @@ fn test_roll_forward_conviction_converges_to_zero() { }); } +#[test] +fn test_roll_forward_normalizes_dust_to_zero() { + new_test_ext(1).execute_with(|| { + let lock = LockState { + locked_mass: 99u64.into(), + conviction: U64F64::from_num(99), + last_update: 100, + }; + + let rolled = roll_forward_lock(lock, 100, false, false); + + assert_eq!(rolled.locked_mass, AlphaBalance::ZERO); + assert_eq!(rolled.conviction, U64F64::from_num(0)); + assert_eq!(rolled.last_update, 100); + }); +} + #[test] fn test_roll_forward_no_change_when_now_equals_last_update() { new_test_ext(1).execute_with(|| { @@ -1529,6 +1930,158 @@ fn test_unstake_allowed_up_to_available() { }); } +#[test] +fn test_unstake_rolls_forward_existing_lock() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); + let lock_amount = AlphaBalance::from(1_000_000_000u64); + + DecayingLock::::remove(coldkey, netuid); + let lock_block = SubtensorModule::get_current_block_as_u64(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount, + )); + + step_block(100); + let now = SubtensorModule::get_current_block_as_u64(); + let expected = roll_forward_decaying_hotkey_lock( + LockState { + locked_mass: lock_amount, + conviction: U64F64::from_num(0), + last_update: lock_block, + }, + now, + ); + + assert_ok!(SubtensorModule::do_remove_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + lock_amount, + )); + + assert_eq!( + Lock::::get((coldkey, netuid, hotkey)).expect("lock should remain"), + expected + ); + let aggregate = + DecayingHotkeyLock::::get(netuid, hotkey).expect("aggregate should remain"); + assert_eq!(aggregate.locked_mass, expected.locked_mass); + assert_eq!(aggregate.last_update, now); + }); +} + +#[test] +fn test_unstake_roll_forward_collects_decaying_lock_dust_from_hotkey_aggregate() { + new_test_ext(1).execute_with(|| { + const ONE_ALPHA: u64 = 1_000_000_000; + const DUST_ALPHA: u64 = 100; + const STAKE_TAO_RAO: u64 = 1_000 * 1_000_000_000; + + let subnet_owner_coldkey = U256::from(1001); + let subnet_owner_hotkey = U256::from(1002); + let coldkey_1 = U256::from(2001); + let coldkey_2 = U256::from(2002); + let hotkey_1 = U256::from(3001); + let hotkey_2 = U256::from(3002); + let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + + setup_reserves( + netuid, + (STAKE_TAO_RAO * 1_000).into(), + (STAKE_TAO_RAO * 10_000).into(), + ); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey_1, &hotkey_1 + )); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &coldkey_1, &hotkey_2 + )); + + for coldkey in [coldkey_1, coldkey_2] { + add_balance_to_coldkey_account(&coldkey, STAKE_TAO_RAO.into()); + SubtensorModule::stake_into_subnet( + &hotkey_1, + &coldkey, + netuid, + STAKE_TAO_RAO.into(), + ::SwapInterface::max_price(), + false, + ) + .unwrap(); + } + + let lock_block = SubtensorModule::get_current_block_as_u64(); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey_1, + netuid, + &hotkey_2, + ONE_ALPHA.into(), + )); + assert_ok!(SubtensorModule::do_lock_stake( + &coldkey_2, + netuid, + &hotkey_2, + DUST_ALPHA.into(), + )); + + assert_eq!( + DecayingHotkeyLock::::get(netuid, hotkey_2) + .expect("decaying aggregate should exist") + .locked_mass, + AlphaBalance::from(ONE_ALPHA + DUST_ALPHA) + ); + + step_block(100); + let now = SubtensorModule::get_current_block_as_u64(); + let rolled_large_lock = roll_forward_decaying_hotkey_lock( + LockState { + locked_mass: ONE_ALPHA.into(), + conviction: U64F64::from_num(0), + last_update: lock_block, + }, + now, + ); + + assert_ok!(SubtensorModule::do_remove_stake( + RuntimeOrigin::signed(coldkey_1), + hotkey_1, + netuid, + ONE_ALPHA.into(), + )); + assert_eq!( + Lock::::get((coldkey_1, netuid, hotkey_2)).expect("coldkey1 lock should remain"), + rolled_large_lock + ); + assert_eq!( + DecayingHotkeyLock::::get(netuid, hotkey_2) + .expect("decaying aggregate should remain") + .locked_mass, + rolled_large_lock + .locked_mass + .saturating_add(AlphaBalance::from(DUST_ALPHA)) + ); + + assert_ok!(SubtensorModule::do_remove_stake( + RuntimeOrigin::signed(coldkey_2), + hotkey_1, + netuid, + ONE_ALPHA.into(), + )); + assert_eq!( + DecayingHotkeyLock::::get(netuid, hotkey_2) + .expect("decaying aggregate should remain") + .locked_mass, + rolled_large_lock.locked_mass + ); + }); +} + #[test] fn test_unstake_blocked_by_lock() { new_test_ext(1).execute_with(|| { @@ -1770,7 +2323,6 @@ fn test_lock_on_multiple_subnets() { 100_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); DecayingLock::::insert(coldkey, netuid_b, false); @@ -1839,7 +2391,6 @@ fn test_unstake_one_subnet_does_not_affect_other() { 100_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1913,7 +2464,6 @@ fn test_hotkey_conviction_multiple_lockers() { 50_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1967,7 +2517,6 @@ fn test_mixed_perpetual_owner_and_decaying_non_owner_locks_roll_forward() { 100_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -2037,7 +2586,6 @@ fn test_total_conviction_equals_sum_of_participating_aggregate_convictions() { 100_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -2097,7 +2645,6 @@ fn test_total_conviction_equals_sum_of_individual_lock_convictions_for_many_lock 50_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); lockers.push((coldkey, hotkey)); @@ -2176,7 +2723,6 @@ fn test_subnet_king_highest_conviction_wins() { 50_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -2393,6 +2939,7 @@ fn test_swap_hotkey_locks_moves_owner_hotkey_aggregate_to_owner_lock() { last_update: now, }, ); + SubtensorModule::add_locking_coldkey(&old_owner_hotkey, netuid, &locking_coldkey); OwnerLock::::insert( netuid, LockState { @@ -2412,6 +2959,16 @@ fn test_swap_hotkey_locks_moves_owner_hotkey_aggregate_to_owner_lock() { OwnerLock::::get(netuid).unwrap().locked_mass, 500u64.into() ); + assert!(!LockingColdkeys::::contains_key(( + netuid, + old_owner_hotkey, + locking_coldkey + ))); + assert!(LockingColdkeys::::contains_key(( + netuid, + new_owner_hotkey, + locking_coldkey + ))); }); } @@ -2515,8 +3072,8 @@ fn test_reduce_lock_partial_reduction() { let coldkey = U256::from(1); let hotkey = U256::from(2); let netuid = setup_subnet_with_stake(coldkey, hotkey, 100_000_000_000); - let lock_amount = AlphaBalance::from(100u64); - let reduce_amount = AlphaBalance::from(40u64); + let lock_amount = AlphaBalance::from(1_000u64); + let reduce_amount = AlphaBalance::from(400u64); let now = SubtensorModule::get_current_block_as_u64(); assert_ok!(SubtensorModule::do_lock_stake( @@ -2526,7 +3083,7 @@ fn test_reduce_lock_partial_reduction() { lock_amount, )); - let conviction = U64F64::from_num(100); + let conviction = U64F64::from_num(1_000); Lock::::insert( (coldkey, netuid, hotkey), LockState { @@ -2548,15 +3105,19 @@ fn test_reduce_lock_partial_reduction() { SubtensorModule::force_reduce_lock(&coldkey, netuid, reduce_amount); let lock = Lock::::get((coldkey, netuid, hotkey)).expect("lock should remain"); - assert_eq!(lock.locked_mass, 60u64.into()); - assert_abs_diff_eq!(lock.conviction.to_num::(), 60., epsilon = 0.0000000001); + assert_eq!(lock.locked_mass, 600u64.into()); + assert_abs_diff_eq!( + lock.conviction.to_num::(), + 600., + epsilon = 0.0000000001 + ); let hotkey_lock = HotkeyLock::::get(netuid, hotkey).expect("hotkey lock should remain"); - assert_eq!(hotkey_lock.locked_mass, 60u64.into()); + assert_eq!(hotkey_lock.locked_mass, 600u64.into()); assert_abs_diff_eq!( hotkey_lock.conviction.to_num::(), - 60., + 600., epsilon = 0.0000000001 ); }); @@ -2597,7 +3158,6 @@ fn test_reduce_lock_two_coldkeys() { 100_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); DecayingLock::::insert(coldkey2, netuid, false); @@ -2671,16 +3231,16 @@ fn test_force_reduce_lock_does_not_over_reduce_hotkey_lock() { Lock::::insert( (coldkey1, netuid, hotkey), LockState { - locked_mass: 1u64.into(), - conviction: U64F64::from_num(10), + locked_mass: 1_000u64.into(), + conviction: U64F64::from_num(1_000), last_update: now, }, ); Lock::::insert( (coldkey2, netuid, hotkey), LockState { - locked_mass: 50u64.into(), - conviction: U64F64::from_num(20), + locked_mass: 5_000u64.into(), + conviction: U64F64::from_num(2_000), last_update: now, }, ); @@ -2688,21 +3248,21 @@ fn test_force_reduce_lock_does_not_over_reduce_hotkey_lock() { netuid, hotkey, LockState { - locked_mass: 51u64.into(), - conviction: U64F64::from_num(30), + locked_mass: 6_000u64.into(), + conviction: U64F64::from_num(3_000), last_update: now, }, ); - SubtensorModule::force_reduce_lock(&coldkey1, netuid, 20u64.into()); + SubtensorModule::force_reduce_lock(&coldkey1, netuid, 2_000u64.into()); assert!(Lock::::get((coldkey1, netuid, hotkey)).is_none()); assert!(Lock::::get((coldkey2, netuid, hotkey)).is_some()); let hotkey_lock = HotkeyLock::::get(netuid, hotkey).expect("hotkey lock should remain"); - assert_eq!(hotkey_lock.locked_mass, 50u64.into()); - assert_eq!(hotkey_lock.conviction, U64F64::from_num(20)); + assert_eq!(hotkey_lock.locked_mass, 5_000u64.into()); + assert_eq!(hotkey_lock.conviction, U64F64::from_num(2_000)); }); } @@ -2734,8 +3294,12 @@ fn test_coldkey_swap_swaps_lock() { .next() .is_none() ); + assert!(!DecayingLock::::contains_key(old_coldkey, netuid)); // New coldkey now has the lock assert!(Lock::::get((new_coldkey, netuid, hotkey)).is_some()); + assert_eq!(DecayingLock::::get(new_coldkey, netuid), Some(false)); + assert!(HotkeyLock::::contains_key(netuid, hotkey)); + assert!(!DecayingHotkeyLock::::contains_key(netuid, hotkey)); }); } @@ -2785,8 +3349,8 @@ fn test_coldkey_swap_allows_destination_conviction_only_lock() { let new_hotkey = U256::from(20); let netuid = subtensor_runtime_common::NetUid::from(1); - let old_conviction = U64F64::from_num(77); - let new_conviction = U64F64::from_num(11); + let old_conviction = U64F64::from_num(777); + let new_conviction = U64F64::from_num(111); SubtensorModule::insert_lock_state( &old_coldkey, @@ -2920,7 +3484,7 @@ fn test_failed_coldkey_swap_extrinsic_rolls_back_state_changes() { netuid, &blocked_hotkey, LockState { - locked_mass: 1u64.into(), + locked_mass: 1_000u64.into(), conviction: U64F64::from_num(0), last_update: SubtensorModule::get_current_block_as_u64(), }, @@ -2976,6 +3540,13 @@ fn test_hotkey_swap_swaps_locks_and_convictions() { &old_hotkey, 5000u64.into(), )); + assert!(LockingColdkeys::::contains_key(( + netuid, old_hotkey, coldkey + ))); + assert_eq!( + LockingColdkeys::::iter_prefix((netuid, old_hotkey)).count(), + 1 + ); // Mock a non-zero conviction let mut lock = Lock::::get((coldkey, netuid, old_hotkey)).unwrap(); @@ -2999,6 +3570,12 @@ fn test_hotkey_swap_swaps_locks_and_convictions() { let lock = Lock::::get((coldkey, netuid, new_hotkey)).unwrap(); assert_eq!(lock.locked_mass, 5000u64.into()); assert!(lock.conviction > U64F64::from_num(0)); + assert!(!LockingColdkeys::::contains_key(( + netuid, old_hotkey, coldkey + ))); + assert!(LockingColdkeys::::contains_key(( + netuid, new_hotkey, coldkey + ))); // Hotkey lock data also updated, conviction is not reset let hotkey_lock = HotkeyLock::::get(netuid, new_hotkey).unwrap(); @@ -3224,7 +3801,6 @@ fn test_clear_small_nomination_checks_lock() { 50_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -3294,7 +3870,6 @@ fn test_clear_small_nomination_reduces_only_tiny_amount_from_lock_state() { large_tao, ::SwapInterface::max_price(), false, - false, ) .unwrap(); SubtensorModule::stake_into_subnet( @@ -3304,7 +3879,6 @@ fn test_clear_small_nomination_reduces_only_tiny_amount_from_lock_state() { tiny_tao, ::SwapInterface::max_price(), false, - false, ) .unwrap(); DecayingLock::::insert(nominator, netuid, false); @@ -3433,7 +4007,7 @@ fn test_epoch_distribution_auto_locks_owner_cut() { let subnet_tempo = 10; let stake = 100_000_000_000u64; - SubtensorModule::set_tempo(netuid, subnet_tempo); + SubtensorModule::set_tempo_unchecked(netuid, subnet_tempo); SubtensorModule::set_ck_burn(0); setup_reserves(netuid, (stake * 10_000).into(), (stake * 10_000).into()); @@ -3497,7 +4071,7 @@ fn test_epoch_distribution_auto_locks_owner_cut() { ); // Advance to the next epoch so owner cut is distributed and auto-locked. - step_block(subnet_tempo); + step_epochs(1, netuid); let owner_stake_after = get_alpha(&subnet_owner_hotkey, &subnet_owner_coldkey, netuid); let owner_cut_locked = owner_stake_after - owner_stake_before; @@ -3710,7 +4284,6 @@ fn test_moving_partial_lock() { 50_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); DecayingLock::::insert(coldkey2, netuid, false); @@ -3795,7 +4368,6 @@ fn test_moving_partial_lock_same_owners() { 50_000_000_000u64.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); DecayingLock::::insert(coldkey2, netuid, false); diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index 0ab260ebc3..ec24697522 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -1321,6 +1321,130 @@ fn test_migrate_remove_add_stake_burn_rate_limit() { }); } +#[test] +fn test_migrate_populate_locking_coldkeys() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &[u8] = b"migrate_populate_locking_coldkeys"; + + let netuid = NetUid::from(1); + let coldkey_1 = U256::from(1001); + let coldkey_2 = U256::from(1002); + let hotkey = U256::from(2001); + let expired_hotkey = U256::from(2002); + + Lock::::insert( + (coldkey_1, netuid, hotkey), + LockState { + locked_mass: AlphaBalance::from(1_000_u64), + conviction: U64F64::from_num(0), + last_update: 1, + }, + ); + Lock::::insert( + (coldkey_2, netuid, hotkey), + LockState { + locked_mass: AlphaBalance::from(2_000_u64), + conviction: U64F64::from_num(0), + last_update: 1, + }, + ); + Lock::::insert( + (coldkey_1, netuid, expired_hotkey), + LockState { + locked_mass: AlphaBalance::ZERO, + conviction: U64F64::from_num(1), + last_update: 1, + }, + ); + + assert_eq!( + LockingColdkeys::::iter_prefix((netuid, hotkey)).count(), + 0 + ); + assert_eq!( + LockingColdkeys::::iter_prefix((netuid, expired_hotkey)).count(), + 0 + ); + assert!(!HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + + let weight = + crate::migrations::migrate_populate_locking_coldkeys::migrate_populate_locking_coldkeys::(); + + assert!(!weight.is_zero(), "migration weight should be non-zero"); + assert!(LockingColdkeys::::contains_key(( + netuid, hotkey, coldkey_1 + ))); + assert!(LockingColdkeys::::contains_key(( + netuid, hotkey, coldkey_2 + ))); + assert_eq!( + LockingColdkeys::::iter_prefix((netuid, hotkey)).count(), + 2 + ); + assert_eq!( + LockingColdkeys::::iter_prefix((netuid, expired_hotkey)).count(), + 0 + ); + assert!(Lock::::get((coldkey_1, netuid, expired_hotkey)).is_none()); + assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + + let _ = LockingColdkeys::::clear_prefix((netuid, hotkey), u32::MAX, None); + let second_weight = + crate::migrations::migrate_populate_locking_coldkeys::migrate_populate_locking_coldkeys::(); + + assert_eq!( + second_weight, + ::DbWeight::get().reads(1), + "second run should only read the migration flag" + ); + assert_eq!( + LockingColdkeys::::iter_prefix((netuid, hotkey)).count(), + 0 + ); + }); +} + +#[test] +fn test_migrate_populate_locking_coldkeys_removes_dust_from_aggregate() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1); + let coldkey_1 = U256::from(1101); + let coldkey_2 = U256::from(1102); + let hotkey = U256::from(2101); + let dust_lock = LockState { + locked_mass: AlphaBalance::from(60_u64), + conviction: U64F64::from_num(0), + last_update: 1, + }; + + DecayingLock::::insert(coldkey_1, netuid, false); + DecayingLock::::insert(coldkey_2, netuid, false); + Lock::::insert((coldkey_1, netuid, hotkey), dust_lock.clone()); + Lock::::insert((coldkey_2, netuid, hotkey), dust_lock); + HotkeyLock::::insert( + netuid, + hotkey, + LockState { + locked_mass: AlphaBalance::from(120_u64), + conviction: U64F64::from_num(0), + last_update: 1, + }, + ); + + crate::migrations::migrate_populate_locking_coldkeys::migrate_populate_locking_coldkeys::< + Test, + >(); + + assert!(Lock::::get((coldkey_1, netuid, hotkey)).is_none()); + assert!(Lock::::get((coldkey_2, netuid, hotkey)).is_none()); + assert!(HotkeyLock::::get(netuid, hotkey).is_none()); + assert_eq!( + LockingColdkeys::::iter_prefix((netuid, hotkey)).count(), + 0 + ); + }); +} + #[test] fn test_migrate_fix_staking_hot_keys() { new_test_ext(1).execute_with(|| { @@ -2800,9 +2924,11 @@ fn test_migrate_reset_unactive_sn() { PendingRootAlphaDivs::::get(netuid), AlphaBalance::ZERO ); - assert!(pallet_subtensor_swap::AlphaSqrtPrice::::contains_key( - netuid - )); + assert_eq!( + // not modified + RAORecycledForRegistration::::get(netuid), + *rao_recycled_before.get(&netuid).unwrap() + ); assert_eq!(PendingOwnerCut::::get(netuid), AlphaBalance::ZERO); assert_ne!(SubnetTAO::::get(netuid), initial_tao); assert_ne!(SubnetAlphaIn::::get(netuid), initial_alpha); @@ -2884,9 +3010,6 @@ fn test_migrate_reset_unactive_sn() { SubnetAlphaOutEmission::::get(netuid), AlphaBalance::ZERO ); - assert!(pallet_subtensor_swap::AlphaSqrtPrice::::contains_key( - netuid - )); assert_ne!(PendingOwnerCut::::get(netuid), AlphaBalance::ZERO); assert_ne!(SubnetTAO::::get(netuid), initial_tao); assert_ne!(SubnetAlphaIn::::get(netuid), initial_alpha); @@ -3052,6 +3175,54 @@ fn test_migrate_remove_unknown_neuron_axon_cert_prom() { } } +// cargo test --package pallet-subtensor --lib -- tests::migration::test_migrate_cleanup_swap_v3 --exact --nocapture +#[test] +fn test_migrate_cleanup_swap_v3() { + use crate::migrations::migrate_cleanup_swap_v3::deprecated_swap_maps; + use substrate_fixed::types::U64F64; + + new_test_ext(1).execute_with(|| { + let migration = crate::migrations::migrate_cleanup_swap_v3::migrate_cleanup_swap_v3::; + + const MIGRATION_NAME: &str = "migrate_cleanup_swap_v3"; + + let provided: u64 = 9876; + let reserves: u64 = 1_000_000; + + SubnetTAO::::insert(NetUid::from(1), TaoBalance::from(reserves)); + SubnetAlphaIn::::insert(NetUid::from(1), AlphaBalance::from(reserves)); + + // Insert deprecated maps values + deprecated_swap_maps::SubnetTaoProvided::::insert( + NetUid::from(1), + TaoBalance::from(provided), + ); + deprecated_swap_maps::SubnetAlphaInProvided::::insert( + NetUid::from(1), + AlphaBalance::from(provided), + ); + + // Run migration + let weight = migration(); + + // Test that values are removed from state + assert!(!deprecated_swap_maps::SubnetTaoProvided::::contains_key(NetUid::from(1)),); + assert!( + !deprecated_swap_maps::SubnetAlphaInProvided::::contains_key(NetUid::from(1)), + ); + + // Provided got added to reserves + assert_eq!( + u64::from(SubnetTAO::::get(NetUid::from(1))), + reserves + provided + ); + assert_eq!( + u64::from(SubnetAlphaIn::::get(NetUid::from(1))), + reserves + provided + ); + }); +} + #[test] fn test_migrate_coldkey_swap_scheduled_to_announcements() { new_test_ext(1000).execute_with(|| { @@ -4647,3 +4818,119 @@ fn test_migrate_reset_tnet_conviction_locks() { ); }); } + +#[test] +fn test_migrate_dynamic_tempo_aligns_first_post_upgrade_fire() { + new_test_ext(1).execute_with(|| { + const MIGRATION_NAME: &str = "dynamic_tempo_v1"; + let netuid = NetUid::from(7u16); + let tempo: u16 = 360; + + add_network(netuid, tempo, 0); + let current_block = 1234u64; + run_to_block(current_block); + + // Compute next-fire block + let netuid_plus_one = (u16::from(netuid) as u64) + 1; + let tempo_plus_one = (tempo as u64) + 1; + let adjusted = current_block + netuid_plus_one; + let remainder = adjusted % tempo_plus_one; + let legacy_blocks_until_next = (tempo as u64) - remainder; + let expected_next_fire = current_block + legacy_blocks_until_next; + + crate::migrations::migrate_dynamic_tempo::migrate_dynamic_tempo::(); + + // New formula: next fire = LastEpochBlock + tempo. + let last_epoch = LastEpochBlock::::get(netuid); + assert_eq!( + last_epoch + tempo as u64, + expected_next_fire, + "back-fill should make new scheduler fire at the same block as legacy modulo" + ); + assert!(HasMigrationRun::::get( + MIGRATION_NAME.as_bytes().to_vec() + )); + }); +} + +#[test] +fn test_migrate_dynamic_tempo_preserves_non_standard_tempo() { + new_test_ext(1).execute_with(|| { + // Three subnets — one standard, two with non-standard tempo + // (simulates the 2 mainnet subnets root configured outside MIN/MAX bounds). + let standard = NetUid::from(1u16); + let small = NetUid::from(2u16); + let large = NetUid::from(3u16); + + add_network(standard, 360, 0); + add_network(small, 10, 0); // < MIN_TEMPO (360) + add_network(large, 60_000, 0); // > MAX_TEMPO (50_400) + + crate::migrations::migrate_dynamic_tempo::migrate_dynamic_tempo::(); + + // Tempo values preserved as-is — no clamp. + assert_eq!(Tempo::::get(standard), 360); + assert_eq!(Tempo::::get(small), 10); + assert_eq!(Tempo::::get(large), 60_000); + + // All non-zero tempos got LastEpochBlock seeded. + assert!(LastEpochBlock::::contains_key(standard)); + assert!(LastEpochBlock::::contains_key(small)); + assert!(LastEpochBlock::::contains_key(large)); + }); +} + +#[test] +fn test_migrate_dynamic_tempo_activity_cutoff_round_trips_production_values() { + new_test_ext(1).execute_with(|| { + // (cutoff_blocks, tempo) combinations from production data. + let cases: [(u16, u16); 6] = [ + (5000, 360), + (6000, 360), + (7200, 360), + (12000, 360), + (1000, 360), + (360, 360), + ]; + + for (i, &(cutoff, tempo)) in cases.iter().enumerate() { + let netuid = NetUid::from((i + 1) as u16); + add_network(netuid, tempo, 0); + ActivityCutoff::::insert(netuid, cutoff); + } + + crate::migrations::migrate_dynamic_tempo::migrate_dynamic_tempo::(); + + for (i, &(cutoff, _)) in cases.iter().enumerate() { + let netuid = NetUid::from((i + 1) as u16); + // get_activity_cutoff_blocks = factor * tempo / 1000 must equal original cutoff exactly. + assert_eq!( + crate::Pallet::::get_activity_cutoff_blocks(netuid), + cutoff as u64, + "ceiling division must round-trip cutoff exactly for netuid {}", + u16::from(netuid) + ); + } + }); +} + +#[test] +fn test_migrate_dynamic_tempo_idempotent() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + add_network(netuid, 360, 0); + + crate::migrations::migrate_dynamic_tempo::migrate_dynamic_tempo::(); + let last_epoch_first = LastEpochBlock::::get(netuid); + + // Mutate state to verify second run is a no-op. + run_to_block(crate::Pallet::::get_current_block_as_u64() + 100); + crate::migrations::migrate_dynamic_tempo::migrate_dynamic_tempo::(); + + assert_eq!( + LastEpochBlock::::get(netuid), + last_epoch_first, + "second migration call must be a no-op" + ); + }); +} diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 277162dde4..49ec0c1638 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -9,7 +9,7 @@ use core::num::NonZeroU64; use crate::utils::rate_limiting::TransactionType; use crate::*; pub use frame_support::traits::Imbalance; -use frame_support::traits::{Contains, Everything, InherentBuilder, InsideBoth, InstanceFilter}; +use frame_support::traits::{Contains, Everything, InsideBoth, InstanceFilter}; use frame_support::weights::Weight; use frame_support::weights::constants::RocksDbWeight; use frame_support::{PalletId, derive_impl}; @@ -18,7 +18,7 @@ use frame_support::{ traits::{Hooks, PrivilegeCmp}, }; use frame_system as system; -use frame_system::{EnsureRoot, RawOrigin, limits, offchain::CreateTransactionBase}; +use frame_system::{EnsureRoot, RawOrigin, limits}; use pallet_subtensor_proxy as pallet_proxy; use pallet_subtensor_utility as pallet_utility; use share_pool::SafeFloat; @@ -214,6 +214,10 @@ parameter_types! { pub const InitialMaxBurn: u64 = 1_000_000_000; pub const MinBurnUpperBound: TaoBalance = TaoBalance::new(1_000_000_000); // 1 TAO pub const MaxBurnLowerBound: TaoBalance = TaoBalance::new(100_000_000); // 0.1 TAO + pub const MinTempo: u16 = crate::MIN_TEMPO; + pub const MaxTempo: u16 = crate::MAX_TEMPO; + pub const MinActivityCutoffFactorMilli: u32 = crate::MIN_ACTIVITY_CUTOFF_FACTOR_MILLI; + pub const MaxActivityCutoffFactorMilli: u32 = crate::MAX_ACTIVITY_CUTOFF_FACTOR_MILLI; pub const InitialValidatorPruneLen: u64 = 0; pub const InitialScalingLawPower: u16 = 50; pub const InitialMaxAllowedValidators: u16 = 100; @@ -253,6 +257,7 @@ parameter_types! { pub const EvmKeyAssociateRateLimit: u64 = 10; pub const SubtensorPalletId: PalletId = PalletId(*b"subtensr"); pub const BurnAccountId: PalletId = PalletId(*b"burntnsr"); + pub const MaxEpochsPerBlock: u32 = 32; } impl crate::Config for Test { @@ -301,6 +306,10 @@ impl crate::Config for Test { type InitialMinStake = InitialMinStake; type MinBurnUpperBound = MinBurnUpperBound; type MaxBurnLowerBound = MaxBurnLowerBound; + type MinTempo = MinTempo; + type MaxTempo = MaxTempo; + type MinActivityCutoffFactorMilli = MinActivityCutoffFactorMilli; + type MaxActivityCutoffFactorMilli = MaxActivityCutoffFactorMilli; type InitialRAORecycledForRegistration = InitialRAORecycledForRegistration; type InitialNetworkImmunityPeriod = InitialNetworkImmunityPeriod; type InitialNetworkMinLockCost = InitialNetworkMinLockCost; @@ -332,6 +341,7 @@ impl crate::Config for Test { type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; type BurnAccountId = BurnAccountId; + type MaxEpochsPerBlock = MaxEpochsPerBlock; type WeightInfo = (); } @@ -339,7 +349,6 @@ impl crate::Config for Test { 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(); } @@ -351,7 +360,6 @@ impl pallet_subtensor_swap::Config for Test { type TaoReserve = TaoBalanceReserve; type AlphaReserve = AlphaBalanceReserve; type MaxFeeRate = SwapMaxFeeRate; - type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; type MinimumReserve = SwapMinimumReserve; type WeightInfo = (); @@ -564,28 +572,12 @@ where type RuntimeCall = RuntimeCall; } -impl frame_system::offchain::CreateInherent for Test +impl frame_system::offchain::CreateBare 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(), (), ())) + UncheckedExtrinsic::new_bare(call) } } @@ -696,9 +688,9 @@ pub(crate) fn next_block_no_epoch(netuid: NetUid) -> u64 { let high_tempo: u16 = u16::MAX - 1; let old_tempo: u16 = SubtensorModule::get_tempo(netuid); - SubtensorModule::set_tempo(netuid, high_tempo); + SubtensorModule::set_tempo_unchecked(netuid, high_tempo); let new_block = next_block(); - SubtensorModule::set_tempo(netuid, old_tempo); + SubtensorModule::set_tempo_unchecked(netuid, old_tempo); new_block } @@ -709,27 +701,27 @@ pub(crate) fn run_to_block_no_epoch(netuid: NetUid, n: u64) { let high_tempo: u16 = u16::MAX - 1; let old_tempo: u16 = SubtensorModule::get_tempo(netuid); - SubtensorModule::set_tempo(netuid, high_tempo); + SubtensorModule::set_tempo_unchecked(netuid, high_tempo); run_to_block(n); - SubtensorModule::set_tempo(netuid, old_tempo); + SubtensorModule::set_tempo_unchecked(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() - )); + const STEP_EPOCHS_MAX_BLOCKS: u32 = 50_000; + + // Advance block-by-block until exactly `count` more epoch slots have been + // consumed for `netuid`, observed via the `SubnetEpochIndex` counter. Robust + // to any tempo (including `tempo == 1`) and to the per-block epoch cap. + let target = crate::SubnetEpochIndex::::get(netuid) + count as u64; + let mut blocks_advanced: u32 = 0; + while crate::SubnetEpochIndex::::get(netuid) < target { step_block(1); + blocks_advanced += 1; + assert!( + blocks_advanced < STEP_EPOCHS_MAX_BLOCKS, + "step_epochs: epoch counter never advanced (tempo == 0?)" + ); } } @@ -756,8 +748,7 @@ pub fn register_ok_neuron( SubtensorModule::set_burn(netuid, TaoBalance::from(0)); let reserve: u64 = 1_000_000_000_000; let tao_reserve = SubnetTAO::::get(netuid); - let alpha_reserve = - SubnetAlphaIn::::get(netuid) + SubnetAlphaInProvided::::get(netuid); + let alpha_reserve = SubnetAlphaIn::::get(netuid); if tao_reserve.is_zero() && alpha_reserve.is_zero() { setup_reserves(netuid, reserve.into(), reserve.into()); @@ -996,7 +987,6 @@ pub fn increase_stake_on_coldkey_hotkey_account( tao_staked, ::SwapInterface::max_price(), false, - false, ) .unwrap(); } @@ -1016,10 +1006,6 @@ pub fn increase_stake_on_hotkey_account(hotkey: &U256, increment: TaoBalance, ne ); } -pub(crate) fn remove_stake_rate_limit_for_tests(hotkey: &U256, coldkey: &U256, netuid: NetUid) { - StakingOperationRateLimiter::::remove((hotkey, coldkey, netuid)); -} - pub(crate) fn setup_reserves(netuid: NetUid, tao: TaoBalance, alpha: AlphaBalance) { SubnetTAO::::set(netuid, tao); SubnetAlphaIn::::set(netuid, alpha); diff --git a/pallets/subtensor/src/tests/mock_high_ed.rs b/pallets/subtensor/src/tests/mock_high_ed.rs index 5f19edf455..cb381a3f81 100644 --- a/pallets/subtensor/src/tests/mock_high_ed.rs +++ b/pallets/subtensor/src/tests/mock_high_ed.rs @@ -7,13 +7,13 @@ use core::num::NonZeroU64; use crate::*; -use frame_support::traits::{Everything, InherentBuilder, InstanceFilter}; +use frame_support::traits::{Everything, InstanceFilter}; use frame_support::weights::Weight; use frame_support::weights::constants::RocksDbWeight; use frame_support::{PalletId, derive_impl}; use frame_support::{parameter_types, traits::PrivilegeCmp}; use frame_system as system; -use frame_system::{EnsureRoot, limits, offchain::CreateTransactionBase}; +use frame_system::{EnsureRoot, limits}; use pallet_subtensor_proxy as pallet_proxy; use sp_core::{ConstU64, H256, U256, offchain::KeyTypeId}; use sp_runtime::Perbill; @@ -174,6 +174,10 @@ parameter_types! { pub const InitialMaxBurn: u64 = 1_000_000_000; pub const MinBurnUpperBound: TaoBalance = TaoBalance::new(1_000_000_000); // 1 TAO pub const MaxBurnLowerBound: TaoBalance = TaoBalance::new(100_000_000); // 0.1 TAO + pub const MinTempo: u16 = crate::MIN_TEMPO; + pub const MaxTempo: u16 = crate::MAX_TEMPO; + pub const MinActivityCutoffFactorMilli: u32 = crate::MIN_ACTIVITY_CUTOFF_FACTOR_MILLI; + pub const MaxActivityCutoffFactorMilli: u32 = crate::MAX_ACTIVITY_CUTOFF_FACTOR_MILLI; pub const InitialValidatorPruneLen: u64 = 0; pub const InitialScalingLawPower: u16 = 50; pub const InitialMaxAllowedValidators: u16 = 100; @@ -213,6 +217,7 @@ parameter_types! { pub const EvmKeyAssociateRateLimit: u64 = 10; pub const SubtensorPalletId: PalletId = PalletId(*b"subtensr"); pub const BurnAccountId: PalletId = PalletId(*b"burntnsr"); + pub const MaxEpochsPerBlock: u32 = 32; } impl crate::Config for Test { @@ -261,6 +266,10 @@ impl crate::Config for Test { type InitialMinStake = InitialMinStake; type MinBurnUpperBound = MinBurnUpperBound; type MaxBurnLowerBound = MaxBurnLowerBound; + type MinTempo = MinTempo; + type MaxTempo = MaxTempo; + type MinActivityCutoffFactorMilli = MinActivityCutoffFactorMilli; + type MaxActivityCutoffFactorMilli = MaxActivityCutoffFactorMilli; type InitialRAORecycledForRegistration = InitialRAORecycledForRegistration; type InitialNetworkImmunityPeriod = InitialNetworkImmunityPeriod; type InitialNetworkMinLockCost = InitialNetworkMinLockCost; @@ -292,6 +301,7 @@ impl crate::Config for Test { type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; type BurnAccountId = BurnAccountId; + type MaxEpochsPerBlock = MaxEpochsPerBlock; type WeightInfo = (); } @@ -299,7 +309,6 @@ impl crate::Config for Test { 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(); } @@ -311,7 +320,6 @@ impl pallet_subtensor_swap::Config for Test { type TaoReserve = TaoBalanceReserve; type AlphaReserve = AlphaBalanceReserve; type MaxFeeRate = SwapMaxFeeRate; - type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; type MinimumReserve = SwapMinimumReserve; type WeightInfo = (); @@ -493,28 +501,12 @@ where type RuntimeCall = RuntimeCall; } -impl frame_system::offchain::CreateInherent for Test +impl frame_system::offchain::CreateBare 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(), (), ())) + UncheckedExtrinsic::new_bare(call) } } diff --git a/pallets/subtensor/src/tests/mod.rs b/pallets/subtensor/src/tests/mod.rs index be37a9227b..c6444c3b9e 100644 --- a/pallets/subtensor/src/tests/mod.rs +++ b/pallets/subtensor/src/tests/mod.rs @@ -32,6 +32,7 @@ mod swap_coldkey; mod swap_hotkey; mod swap_hotkey_with_subnet; mod tao; +mod tempo_control; mod transaction_extension_pays_no; mod uids; mod voting_power; diff --git a/pallets/subtensor/src/tests/move_stake.rs b/pallets/subtensor/src/tests/move_stake.rs index a991df20a5..8265ec5e9d 100644 --- a/pallets/subtensor/src/tests/move_stake.rs +++ b/pallets/subtensor/src/tests/move_stake.rs @@ -36,7 +36,6 @@ fn test_do_move_success() { stake_amount, ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -114,7 +113,6 @@ fn test_do_move_different_subnets() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -183,7 +181,6 @@ fn test_do_move_nonexistent_subnet() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -290,7 +287,6 @@ fn test_do_move_nonexistent_destination_hotkey() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -356,7 +352,6 @@ fn test_do_move_partial_stake() { total_stake.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -427,7 +422,6 @@ fn test_do_move_multiple_times() { initial_stake.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = @@ -439,7 +433,6 @@ fn test_do_move_multiple_times() { let alpha1 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( &hotkey1, &coldkey, netuid, ); - remove_stake_rate_limit_for_tests(&hotkey1, &coldkey, netuid); assert_ok!(SubtensorModule::do_move_stake( RuntimeOrigin::signed(coldkey), hotkey1, @@ -451,7 +444,6 @@ fn test_do_move_multiple_times() { let alpha2 = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( &hotkey2, &coldkey, netuid, ); - remove_stake_rate_limit_for_tests(&hotkey2, &coldkey, netuid); assert_ok!(SubtensorModule::do_move_stake( RuntimeOrigin::signed(coldkey), hotkey2, @@ -502,7 +494,6 @@ fn test_do_move_wrong_origin() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -570,7 +561,6 @@ fn test_do_move_same_hotkey_fails() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = @@ -622,7 +612,6 @@ fn test_do_move_event_emission() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -633,8 +622,9 @@ fn test_do_move_event_emission() { // Move stake and capture events System::reset_events(); - let current_price = - ::SwapInterface::current_alpha_price(netuid.into()); + let current_price = U96F32::from_num( + ::SwapInterface::current_alpha_price(netuid.into()), + ); let tao_equivalent = (current_price * U96F32::from_num(alpha)).to_num::(); // no fee conversion assert_ok!(SubtensorModule::do_move_stake( RuntimeOrigin::signed(coldkey), @@ -684,7 +674,6 @@ fn test_do_move_storage_updates() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -752,7 +741,6 @@ fn test_move_full_amount_same_netuid() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -802,8 +790,8 @@ fn test_do_move_max_values() { let coldkey = U256::from(1); let origin_hotkey = U256::from(2); let destination_hotkey = U256::from(3); - let max_stake = u64::MAX; let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); + let max_stake = 20_000_000_000_000_000_u64; // Set up initial stake with maximum value let _ = SubtensorModule::create_account_if_non_existent(&coldkey, &origin_hotkey); @@ -811,7 +799,7 @@ fn test_do_move_max_values() { add_balance_to_coldkey_account(&coldkey, max_stake.into()); // Add lots of liquidity to bypass low liquidity check - let reserve = u64::MAX / 1000; + let reserve = max_stake / 1000; mock::setup_reserves(netuid, reserve.into(), reserve.into()); SubtensorModule::stake_into_subnet( @@ -821,7 +809,6 @@ fn test_do_move_max_values() { max_stake.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -885,7 +872,6 @@ fn test_moving_too_little_unstakes() { (amount.to_u64() + fee * 2).into() )); - remove_stake_rate_limit_for_tests(&hotkey_account_id, &coldkey_account_id, netuid); assert_err!( SubtensorModule::move_stake( RuntimeOrigin::signed(coldkey_account_id), @@ -925,7 +911,6 @@ fn test_do_transfer_success() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -1035,7 +1020,6 @@ fn test_do_transfer_insufficient_stake() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1076,7 +1060,6 @@ fn test_do_transfer_wrong_origin() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1115,7 +1098,6 @@ fn test_do_transfer_minimum_stake_check() { stake_amount, ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1163,7 +1145,6 @@ fn test_do_transfer_different_subnets() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1230,7 +1211,6 @@ fn test_do_swap_success() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -1339,7 +1319,6 @@ fn test_do_swap_insufficient_stake() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1375,7 +1354,6 @@ fn test_do_swap_wrong_origin() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1414,7 +1392,6 @@ fn test_do_swap_minimum_stake_check() { total_stake, ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1451,7 +1428,6 @@ fn test_do_swap_same_subnet() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1497,7 +1473,6 @@ fn test_do_swap_partial_stake() { total_stake_tao.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let total_stake_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -1550,7 +1525,6 @@ fn test_do_swap_storage_updates() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1611,7 +1585,6 @@ fn test_do_swap_multiple_times() { initial_stake.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -1621,7 +1594,6 @@ fn test_do_swap_multiple_times() { &hotkey, &coldkey, netuid1, ); if !alpha1.is_zero() { - remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid1); assert_ok!(SubtensorModule::do_swap_stake( RuntimeOrigin::signed(coldkey), hotkey, @@ -1637,7 +1609,6 @@ fn test_do_swap_multiple_times() { let (tao_equivalent, _) = mock::swap_alpha_to_tao_ext(netuid2, alpha2, true); // we do this in the loop, because we need the value before the swap expected_alpha = mock::swap_tao_to_alpha(netuid1, tao_equivalent).0; - remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid2); assert_ok!(SubtensorModule::do_swap_stake( RuntimeOrigin::signed(coldkey), hotkey, @@ -1683,7 +1654,6 @@ fn test_do_swap_allows_non_owned_hotkey() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -1775,7 +1745,6 @@ fn test_move_stake_specific_stake_into_subnet_fail() { // Move stake to destination subnet let (tao_equivalent, _) = mock::swap_alpha_to_tao_ext(origin_netuid, alpha_to_move, true); let (expected_value, _) = mock::swap_tao_to_alpha(netuid, tao_equivalent); - remove_stake_rate_limit_for_tests(&hotkey_account_id, &coldkey_account_id, origin_netuid); assert_ok!(SubtensorModule::move_stake( RuntimeOrigin::signed(coldkey_account_id), hotkey_account_id, @@ -1808,57 +1777,11 @@ fn test_move_stake_specific_stake_into_subnet_fail() { } #[test] -fn test_transfer_stake_rate_limited() { - new_test_ext(1).execute_with(|| { - let subnet_owner_coldkey = U256::from(1001); - let subnet_owner_hotkey = U256::from(1002); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - - let origin_coldkey = U256::from(1); - let destination_coldkey = U256::from(2); - let hotkey = U256::from(3); - let stake_amount = DefaultMinStake::::get().to_u64() * 10; - - let _ = SubtensorModule::create_account_if_non_existent(&origin_coldkey, &hotkey); - let _ = SubtensorModule::create_account_if_non_existent(&destination_coldkey, &hotkey); - add_balance_to_coldkey_account(&origin_coldkey, stake_amount.into()); - SubtensorModule::stake_into_subnet( - &hotkey, - &origin_coldkey, - netuid, - stake_amount.into(), - ::SwapInterface::max_price(), - true, - false, - ) - .unwrap(); - let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( - &hotkey, - &origin_coldkey, - netuid, - ); - - assert_err!( - SubtensorModule::do_transfer_stake( - RuntimeOrigin::signed(origin_coldkey), - destination_coldkey, - hotkey, - netuid, - netuid, - alpha - ), - Error::::StakingOperationRateLimitExceeded - ); - }); -} - -#[test] -fn test_transfer_stake_doesnt_limit_destination_coldkey() { +fn test_transfer_stake_same_netuid_not_rate_limited() { new_test_ext(1).execute_with(|| { let subnet_owner_coldkey = U256::from(1001); let subnet_owner_hotkey = U256::from(1002); let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - let netuid2 = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); let origin_coldkey = U256::from(1); let destination_coldkey = U256::from(2); @@ -1875,7 +1798,6 @@ fn test_transfer_stake_doesnt_limit_destination_coldkey() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -1884,25 +1806,39 @@ fn test_transfer_stake_doesnt_limit_destination_coldkey() { netuid, ); + // add_stake set the limiter for (hotkey, origin_coldkey, netuid), but a same-netuid + // transfer performs no AMM swap (no price impact), so it is NOT rate limited assert_ok!(SubtensorModule::do_transfer_stake( RuntimeOrigin::signed(origin_coldkey), destination_coldkey, hotkey, netuid, - netuid2, + netuid, alpha - ),); + )); - assert!(!StakingOperationRateLimiter::::contains_key(( - hotkey, - destination_coldkey, - netuid2 - ))); + // The whole position was moved to the destination coldkey on the same subnet. + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &origin_coldkey, + netuid + ), + AlphaBalance::ZERO + ); + assert_ne!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, + &destination_coldkey, + netuid + ), + AlphaBalance::ZERO + ); }); } #[test] -fn test_swap_stake_limits_destination_netuid() { +fn test_transfer_stake_doesnt_limit_destination_coldkey() { new_test_ext(1).execute_with(|| { let subnet_owner_coldkey = U256::from(1001); let subnet_owner_hotkey = U256::from(1002); @@ -1910,10 +1846,12 @@ fn test_swap_stake_limits_destination_netuid() { let netuid2 = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); let origin_coldkey = U256::from(1); + let destination_coldkey = U256::from(2); let hotkey = U256::from(3); let stake_amount = DefaultMinStake::::get().to_u64() * 10; let _ = SubtensorModule::create_account_if_non_existent(&origin_coldkey, &hotkey); + let _ = SubtensorModule::create_account_if_non_existent(&destination_coldkey, &hotkey); add_balance_to_coldkey_account(&origin_coldkey, stake_amount.into()); SubtensorModule::stake_into_subnet( &hotkey, @@ -1922,7 +1860,6 @@ fn test_swap_stake_limits_destination_netuid() { stake_amount.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -1931,24 +1868,13 @@ fn test_swap_stake_limits_destination_netuid() { netuid, ); - assert_ok!(SubtensorModule::do_swap_stake( + assert_ok!(SubtensorModule::do_transfer_stake( RuntimeOrigin::signed(origin_coldkey), + destination_coldkey, hotkey, netuid, netuid2, alpha ),); - - assert!(!StakingOperationRateLimiter::::contains_key(( - hotkey, - origin_coldkey, - netuid - ))); - - assert!(StakingOperationRateLimiter::::contains_key(( - hotkey, - origin_coldkey, - netuid2 - ))); }); } diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index 4bd1a9cb19..4696507e2e 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -267,8 +267,9 @@ fn dissolve_owner_cut_refund_logic() { // Use the current alpha price to estimate the TAO equivalent. let owner_emission_tao = { - let price: U96F32 = - ::SwapInterface::current_alpha_price(net.into()); + let price: U96F32 = U96F32::saturating_from_num( + ::SwapInterface::current_alpha_price(net.into()), + ); U96F32::from_num(owner_alpha_u64) .saturating_mul(price) .floor() @@ -278,6 +279,8 @@ fn dissolve_owner_cut_refund_logic() { let expected_refund: TaoBalance = lock.saturating_sub(owner_emission_tao); + println!("expected_refund = {:?}", expected_refund); + let before = SubtensorModule::get_coldkey_balance(&oc); assert_ok!(SubtensorModule::do_dissolve_network(net)); let after = SubtensorModule::get_coldkey_balance(&oc); @@ -384,8 +387,6 @@ fn dissolve_clears_all_per_subnet_storages() { // Token / price / provided reserves TokenSymbol::::insert(net, b"XX".to_vec()); SubnetMovingPrice::::insert(net, substrate_fixed::types::I96F32::from_num(1)); - SubnetTaoProvided::::insert(net, TaoBalance::from(1)); - SubnetAlphaInProvided::::insert(net, AlphaBalance::from(1)); // TAO Flow SubnetTaoFlow::::insert(net, 0i64); @@ -547,8 +548,6 @@ fn dissolve_clears_all_per_subnet_storages() { // Token / price / provided reserves assert!(!TokenSymbol::::contains_key(net)); assert!(!SubnetMovingPrice::::contains_key(net)); - assert!(!SubnetTaoProvided::::contains_key(net)); - assert!(!SubnetAlphaInProvided::::contains_key(net)); // Subnet locks assert!(!TransferToggle::::contains_key(net)); @@ -974,6 +973,91 @@ fn destroy_alpha_out_multiple_stakers_pro_rata() { }); } +#[test] +fn destroy_alpha_in_out_stakes_cleans_locking_coldkeys() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(10); + let owner_hot = U256::from(20); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + remove_owner_registration_stake(netuid); + + let coldkey = U256::from(111); + let hotkey = U256::from(222); + let other_netuid = NetUid::from(u16::from(netuid) + 1); + let lock = LockState { + locked_mass: 10u64.into(), + conviction: U64F64::from_num(1), + last_update: 1, + }; + + Lock::::insert((coldkey, netuid, hotkey), lock.clone()); + LockingColdkeys::::insert((netuid, hotkey, coldkey), ()); + Lock::::insert((coldkey, other_netuid, hotkey), lock); + LockingColdkeys::::insert((other_netuid, hotkey, coldkey), ()); + + assert_ok!(SubtensorModule::destroy_alpha_in_out_stakes(netuid)); + + assert!(!Lock::::contains_key((coldkey, netuid, hotkey))); + assert!(!LockingColdkeys::::contains_key(( + netuid, hotkey, coldkey + ))); + assert!(Lock::::contains_key((coldkey, other_netuid, hotkey))); + assert!(LockingColdkeys::::contains_key(( + other_netuid, + hotkey, + coldkey + ))); + }); +} + +#[test] +fn destroy_alpha_in_out_stakes_cleans_all_lock_aggregates() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(10); + let owner_hot = U256::from(20); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + remove_owner_registration_stake(netuid); + + let coldkey = U256::from(111); + let hotkey = U256::from(222); + let other_netuid = NetUid::from(u16::from(netuid) + 1); + let lock = LockState { + locked_mass: 10u64.into(), + conviction: U64F64::from_num(1), + last_update: 1, + }; + + HotkeyLock::::insert(netuid, hotkey, lock.clone()); + DecayingHotkeyLock::::insert(netuid, hotkey, lock.clone()); + OwnerLock::::insert(netuid, lock.clone()); + DecayingOwnerLock::::insert(netuid, lock.clone()); + DecayingLock::::insert(coldkey, netuid, false); + + HotkeyLock::::insert(other_netuid, hotkey, lock.clone()); + DecayingHotkeyLock::::insert(other_netuid, hotkey, lock.clone()); + OwnerLock::::insert(other_netuid, lock.clone()); + DecayingOwnerLock::::insert(other_netuid, lock); + DecayingLock::::insert(coldkey, other_netuid, false); + + assert_ok!(SubtensorModule::destroy_alpha_in_out_stakes(netuid)); + + assert!(!HotkeyLock::::contains_key(netuid, hotkey)); + assert!(!DecayingHotkeyLock::::contains_key(netuid, hotkey)); + assert!(!OwnerLock::::contains_key(netuid)); + assert!(!DecayingOwnerLock::::contains_key(netuid)); + assert!(!DecayingLock::::contains_key(coldkey, netuid)); + + assert!(HotkeyLock::::contains_key(other_netuid, hotkey)); + assert!(DecayingHotkeyLock::::contains_key( + other_netuid, + hotkey + )); + assert!(OwnerLock::::contains_key(other_netuid)); + assert!(DecayingOwnerLock::::contains_key(other_netuid)); + assert!(DecayingLock::::contains_key(coldkey, other_netuid)); + }); +} + #[allow(clippy::indexing_slicing)] #[test] fn destroy_alpha_out_many_stakers_complex_distribution() { @@ -1081,8 +1165,9 @@ fn destroy_alpha_out_many_stakers_complex_distribution() { let owner_emission_tao: u64 = { // Fallback matches the pallet's fallback - let price: U96F32 = - ::SwapInterface::current_alpha_price(netuid.into()); + let price: U96F32 = U96F32::from_num( + ::SwapInterface::current_alpha_price(netuid.into()), + ); U96F32::from_num(owner_alpha_u64) .saturating_mul(price) .floor() @@ -1162,8 +1247,9 @@ fn destroy_alpha_out_refund_gating_by_registration_block() { .saturating_to_num::(); let owner_emission_tao_u64 = { - let price: U96F32 = - ::SwapInterface::current_alpha_price(netuid.into()); + let price: U96F32 = U96F32::from_num( + ::SwapInterface::current_alpha_price(netuid.into()), + ); U96F32::from_num(owner_alpha_u64) .saturating_mul(price) .floor() @@ -2243,8 +2329,8 @@ fn massive_dissolve_refund_and_reregistration_flow_is_lossless_and_cleans_state( "subnet {net:?} still exists" ); assert!( - !pallet_subtensor_swap::SwapV3Initialized::::get(net), - "SwapV3Initialized still set" + !pallet_subtensor_swap::PalSwapInitialized::::get(net), + "PalSwapInitialized still set" ); } @@ -2460,10 +2546,13 @@ fn dissolve_clears_all_lock_maps_for_removed_network() { // --- Lock: (coldkey, netuid, hotkey) Lock::::insert((cold_1, net, hot_1), lock_a.clone()); + LockingColdkeys::::insert((net, hot_1, cold_1), ()); Lock::::insert((cold_2, net, hot_2), lock_b.clone()); + LockingColdkeys::::insert((net, hot_2, cold_2), ()); // Same cold/hot on another net should survive. Lock::::insert((cold_1, other_net, hot_1), lock_a.clone()); + LockingColdkeys::::insert((other_net, hot_1, cold_1), ()); // --- HotkeyLock HotkeyLock::::insert(net, hot_1, lock_a.clone()); @@ -2487,6 +2576,8 @@ fn dissolve_clears_all_lock_maps_for_removed_network() { // Sanity checks before dissolve assert!(Lock::::contains_key((cold_1, net, hot_1))); assert!(Lock::::contains_key((cold_2, net, hot_2))); + assert!(LockingColdkeys::::contains_key((net, hot_1, cold_1))); + assert!(LockingColdkeys::::contains_key((net, hot_2, cold_2))); assert!(HotkeyLock::::contains_key(net, hot_1)); assert!(HotkeyLock::::contains_key(net, hot_2)); @@ -2501,6 +2592,9 @@ fn dissolve_clears_all_lock_maps_for_removed_network() { // Sanity: other net keys are present before dissolve. assert!(Lock::::contains_key((cold_1, other_net, hot_1))); + assert!(LockingColdkeys::::contains_key(( + other_net, hot_1, cold_1 + ))); assert!(HotkeyLock::::contains_key(other_net, hot_1)); assert!(DecayingHotkeyLock::::contains_key(other_net, hot_1)); assert!(OwnerLock::::contains_key(other_net)); @@ -2512,6 +2606,8 @@ fn dissolve_clears_all_lock_maps_for_removed_network() { // Ensure removed assert!(!Lock::::contains_key((cold_1, net, hot_1))); assert!(!Lock::::contains_key((cold_2, net, hot_2))); + assert!(!LockingColdkeys::::contains_key((net, hot_1, cold_1))); + assert!(!LockingColdkeys::::contains_key((net, hot_2, cold_2))); assert!(!HotkeyLock::::contains_key(net, hot_1)); assert!(!HotkeyLock::::contains_key(net, hot_2)); @@ -2532,6 +2628,9 @@ fn dissolve_clears_all_lock_maps_for_removed_network() { // Ensure other_net is untouched assert!(Lock::::contains_key((cold_1, other_net, hot_1))); + assert!(LockingColdkeys::::contains_key(( + other_net, hot_1, cold_1 + ))); assert!(HotkeyLock::::contains_key(other_net, hot_1)); assert!(DecayingHotkeyLock::::contains_key(other_net, hot_1)); assert!(OwnerLock::::contains_key(other_net)); @@ -2539,13 +2638,13 @@ fn dissolve_clears_all_lock_maps_for_removed_network() { }); } -fn owner_alpha_from_lock_and_price(lock_cost_u64: u64, price: U96F32) -> u64 { - let alpha = (U96F32::from_num(lock_cost_u64) +fn owner_alpha_from_lock_and_price(lock_cost_u64: u64, price: U64F64) -> u64 { + let alpha = (U64F64::from_num(lock_cost_u64) .checked_div(price) .unwrap_or_default()) .floor(); - if alpha > U96F32::from_num(u64::MAX) { + if alpha > U64F64::from_num(u64::MAX) { u64::MAX } else { alpha.to_num::() @@ -2555,7 +2654,7 @@ fn owner_alpha_from_lock_and_price(lock_cost_u64: u64, price: U96F32) -> u64 { #[test] fn median_subnet_alpha_price_returns_one_when_no_eligible_subnet_prices() { new_test_ext(0).execute_with(|| { - let one = U96F32::from_num(1u64); + let one = U64F64::from_num(1u64); // Empty state. assert_eq!(SubtensorModule::get_median_subnet_alpha_price(), one); @@ -2571,7 +2670,7 @@ fn median_subnet_alpha_price_returns_one_when_no_eligible_subnet_prices() { setup_reserves(zero_netuid, TaoBalance::ZERO, AlphaBalance::from(100u64)); assert_eq!( ::SwapInterface::current_alpha_price(zero_netuid.into()), - U96F32::from_num(0u64) + U64F64::from_num(0u64) ); assert_eq!(SubtensorModule::get_median_subnet_alpha_price(), one); @@ -2747,11 +2846,6 @@ fn register_network_seeds_first_subnet_from_fallback_price_one_and_keeps_lock_in RAORecycledForRegistration::::get(new_netuid), expected_recycled ); - assert_eq!(SubnetTaoProvided::::get(new_netuid), TaoBalance::ZERO); - assert_eq!( - SubnetAlphaInProvided::::get(new_netuid), - AlphaBalance::ZERO - ); assert_eq!( ::SwapInterface::current_alpha_price(new_netuid.into()), diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index 0fe951a29b..660b6957d7 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -6,7 +6,6 @@ use frame_support::dispatch::{DispatchClass, GetDispatchInfo, Pays}; use frame_support::sp_runtime::DispatchError; use frame_support::{assert_err, assert_noop, assert_ok, traits::Currency}; use frame_system::RawOrigin; -use pallet_subtensor_swap::tick::TickIndex; use safe_math::FixedExt; use share_pool::SafeFloat; use sp_core::{Get, H256, U256}; @@ -557,13 +556,7 @@ fn test_add_stake_partial_below_min_stake_fails() { mock::setup_reserves(netuid, (amount * 10).into(), (amount * 10).into()); // Force the swap to initialize - SubtensorModule::swap_tao_for_alpha( - netuid, - TaoBalance::ZERO, - 1_000_000_000_000_u64.into(), - false, - ) - .unwrap(); + ::SwapInterface::init_swap(netuid, None); // Get the current price let current_price = @@ -679,6 +672,7 @@ fn test_remove_stake_total_balance_no_change() { // Set fee rate to 0 so that alpha fee is not moved to block producer pallet_subtensor_swap::FeeRate::::insert(netuid, 0); + let fee: u64 = 0; // Clear any implicit existing stake so the test is deterministic let existing = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -707,10 +701,14 @@ fn test_remove_stake_total_balance_no_change() { amount.into(), ); - // Ensure SubnetTAO / TotalStake can pay out on remove - let (amount_tao, fee) = mock::swap_alpha_to_tao(netuid, amount.into()); - SubnetTAO::::mutate(netuid, |v| *v += amount_tao + fee.into()); - TotalStake::::mutate(|v| *v += amount_tao + fee.into()); + // Add subnet TAO for the equivalent amount added at price + let amount_tao = U96F32::from_num(amount) + * U96F32::from_num( + ::SwapInterface::current_alpha_price(netuid.into()), + ); + let amount_tao: TaoBalance = amount_tao.to_num::().into(); + SubnetTAO::::mutate(netuid, |v| *v += amount_tao); + TotalStake::::mutate(|v| *v += amount_tao); // Remove stake assert_ok!(SubtensorModule::remove_stake( @@ -741,8 +739,8 @@ fn test_remove_stake_total_balance_no_change() { ); assert_abs_diff_eq!( - total_balance_after.saturating_sub(total_balance_before), - amount_tao.saturating_sub(fee.into()), + total_balance_after - total_balance_before, + amount_tao - fee.into(), epsilon = TaoBalance::from(amount) / 1000.into() ); }); @@ -793,7 +791,7 @@ fn test_add_stake_insufficient_liquidity_one_side_ok() { add_balance_to_coldkey_account(&coldkey, amount_staked.into()); // Set the liquidity at lowest possible value so that all staking requests fail - let reserve_alpha = u64::from(mock::SwapMinimumReserve::get()); + let reserve_alpha = 1_000_000_000_u64; let reserve_tao = u64::from(mock::SwapMinimumReserve::get()) - 1; mock::setup_reserves(netuid, reserve_tao.into(), reserve_alpha.into()); @@ -863,7 +861,6 @@ fn test_remove_stake_insufficient_liquidity() { amount_staked.into(), ::SwapInterface::max_price(), false, - false, ) .unwrap(); @@ -877,9 +874,9 @@ fn test_remove_stake_insufficient_liquidity() { Error::::InsufficientLiquidity ); - // Mock provided liquidity - remove becomes successful - SubnetTaoProvided::::insert(netuid, TaoBalance::from(amount_staked + 1)); - SubnetAlphaInProvided::::insert(netuid, AlphaBalance::from(1)); + // Mock more liquidity - remove becomes successful + SubnetTAO::::insert(netuid, TaoBalance::from(amount_staked + 1)); + SubnetAlphaIn::::insert(netuid, AlphaBalance::from(1)); assert_ok!(SubtensorModule::remove_stake( RuntimeOrigin::signed(coldkey), hotkey, @@ -934,8 +931,6 @@ fn test_remove_stake_total_issuance_no_change() { netuid, ); - remove_stake_rate_limit_for_tests(&hotkey_account_id, &coldkey_account_id, netuid); - assert_ok!(SubtensorModule::remove_stake( RuntimeOrigin::signed(coldkey_account_id), hotkey_account_id, @@ -1038,7 +1033,6 @@ fn test_remove_prev_epoch_stake() { netuid, ); - remove_stake_rate_limit_for_tests(&hotkey_account_id, &coldkey_account_id, netuid); let fee = mock::swap_alpha_to_tao(netuid, stake).1 + fee; assert_ok!(SubtensorModule::remove_stake( RuntimeOrigin::signed(coldkey_account_id), @@ -1103,7 +1097,7 @@ fn test_staking_sets_div_variables() { ); // Wait for 1 epoch - step_block(tempo + 1); + step_epochs(1, netuid); // Verify that divident variables have been set let stake = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( @@ -1694,7 +1688,6 @@ fn test_clear_small_nominations() { SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold1, netuid); let unstake_amount1 = AlphaBalance::from(alpha_stake1.to_u64() * 997 / 1000); let small1 = alpha_stake1 - unstake_amount1; - remove_stake_rate_limit_for_tests(&hot1, &cold1, netuid); assert_ok!(SubtensorModule::remove_stake( RuntimeOrigin::signed(cold1), hot1, @@ -1718,7 +1711,6 @@ fn test_clear_small_nominations() { SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hot1, &cold2, netuid); let unstake_amount2 = AlphaBalance::from(alpha_stake2.to_u64() * 997 / 1000); let small2 = alpha_stake2 - unstake_amount2; - remove_stake_rate_limit_for_tests(&hot1, &cold2, netuid); assert_ok!(SubtensorModule::remove_stake( RuntimeOrigin::signed(cold2), hot1, @@ -2201,18 +2193,17 @@ fn test_get_total_delegated_stake_after_unstaking() { &delegator, netuid, ); - remove_stake_rate_limit_for_tests(&delegator, &delegate_hotkey, netuid); // Unstake part of the delegation let unstake_amount_alpha = delegated_alpha / 2.into(); - remove_stake_rate_limit_for_tests(&delegate_hotkey, &delegator, netuid); assert_ok!(SubtensorModule::remove_stake( RuntimeOrigin::signed(delegator), delegate_hotkey, netuid, unstake_amount_alpha.into() )); - let current_price = - ::SwapInterface::current_alpha_price(netuid.into()); + let current_price = U96F32::from_num( + ::SwapInterface::current_alpha_price(netuid.into()), + ); // Calculate the expected delegated stake let unstake_amount = @@ -2714,13 +2705,12 @@ fn test_stake_overflow() { let coldkey_account_id = U256::from(435445); let hotkey_account_id = U256::from(54544); let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - let ed = u64::from(ExistentialDeposit::get()); - // Maximum possible: Max TAO supply less locked balance less ED (that's on owner's coldkey) - let amount = - 21_000_000_000_000_000_u64 - u64::from(SubtensorModule::get_network_last_lock()) - ed; register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + // Maximum possible: Max TAO supply less already-issued balance. + let amount = 21_000_000_000_000_000_u64 - u64::from(Balances::total_issuance()); + // Give it some $$$ in his coldkey balance add_balance_to_coldkey_account(&coldkey_account_id, amount.into()); @@ -2760,13 +2750,13 @@ fn test_max_amount_add_root() { // 0 price on root => max is 0 assert_eq!( SubtensorModule::get_max_amount_add(NetUid::ROOT, TaoBalance::ZERO), - Err(Error::::ZeroMaxStakeAmount.into()) + Ok(0u64.into()) ); // 0.999999... price on root => max is 0 assert_eq!( SubtensorModule::get_max_amount_add(NetUid::ROOT, TaoBalance::from(999_999_999)), - Err(Error::::ZeroMaxStakeAmount.into()) + Ok(0u64.into()) ); // 1.0 price on root => max is u64::MAX @@ -2798,13 +2788,13 @@ fn test_max_amount_add_stable() { // 0 price => max is 0 assert_eq!( SubtensorModule::get_max_amount_add(netuid, TaoBalance::ZERO), - Err(Error::::ZeroMaxStakeAmount.into()) + Ok(0u64.into()) ); // 0.999999... price => max is 0 assert_eq!( SubtensorModule::get_max_amount_add(netuid, TaoBalance::from(999_999_999)), - Err(Error::::ZeroMaxStakeAmount.into()) + Ok(0u64.into()) ); // 1.0 price => max is u64::MAX @@ -2888,8 +2878,15 @@ fn test_max_amount_add_dynamic() { pallet_subtensor_swap::Error::::PriceLimitExceeded, )), ), - (150_000_000_000, 100_000_000_000, 1_500_000_000, Ok(5)), - (150_000_000_000, 100_000_000_000, 1_500_000_001, Ok(51)), + ( + 150_000_000_000, + 100_000_000_000, + 1_500_000_000, + Err(DispatchError::from( + pallet_subtensor_swap::Error::::PriceLimitExceeded, + )), + ), + (150_000_000_000, 100_000_000_000, 1_500_000_001, Ok(49)), ( 150_000_000_000, 100_000_000_000, @@ -2912,13 +2909,7 @@ fn test_max_amount_add_dynamic() { SubnetAlphaIn::::insert(netuid, alpha_in); // Force the swap to initialize - SubtensorModule::swap_tao_for_alpha( - netuid, - TaoBalance::ZERO, - 1_000_000_000_000_u64.into(), - false, - ) - .unwrap(); + ::SwapInterface::init_swap(netuid, None); if !alpha_in.is_zero() { let expected_price = U96F32::from_num(tao_in) / U96F32::from_num(alpha_in); @@ -2975,13 +2966,13 @@ fn test_max_amount_remove_root() { // 1.000...001 price on root => max is 0 assert_eq!( SubtensorModule::get_max_amount_remove(NetUid::ROOT, TaoBalance::from(1_000_000_001)), - Err(Error::::ZeroMaxStakeAmount.into()) + Ok(0u64.into()) ); // 2.0 price on root => max is 0 assert_eq!( SubtensorModule::get_max_amount_remove(NetUid::ROOT, TaoBalance::from(2_000_000_000)), - Err(Error::::ZeroMaxStakeAmount.into()) + Ok(0u64.into()) ); }); } @@ -3013,13 +3004,13 @@ fn test_max_amount_remove_stable() { // 1.000...001 price => max is 0 assert_eq!( SubtensorModule::get_max_amount_remove(netuid, TaoBalance::from(1_000_000_001)), - Err(Error::::ZeroMaxStakeAmount.into()) + Ok(0u64.into()) ); // 2.0 price => max is 0 assert_eq!( SubtensorModule::get_max_amount_remove(netuid, TaoBalance::from(2_000_000_000)), - Err(Error::::ZeroMaxStakeAmount.into()) + Ok(0u64.into()) ); }); } @@ -3054,13 +3045,16 @@ fn test_max_amount_remove_dynamic() { (10_000_000_000, 10_000_000_000, 0, Ok(u64::MAX)), // Low bounds (numbers are empirical, it is only important that result // is sharply decreasing when limit price increases) - (1_000, 1_000, 0, Ok(4_308_000_000_000)), - (1_001, 1_001, 0, Ok(4_310_000_000_000)), - (1_001, 1_001, 1, Ok(31_750_000)), - (1_001, 1_001, 2, Ok(22_500_000)), - (1_001, 1_001, 1_001, Ok(1_000_000)), - (1_001, 1_001, 10_000, Ok(316_000)), - (1_001, 1_001, 100_000, Ok(100_000)), + (1_000, 1_000, 0, Ok(u64::MAX)), + (1_001, 1_001, 0, Ok(u64::MAX)), + (1_001, 1_001, 1, Ok(17_472)), + (1_001, 1_001, 2, Ok(17_472)), + (1_001, 1_001, 1_001, Ok(17_472)), + (1_001, 1_001, 10_000, Ok(17_472)), + (1_001, 1_001, 100_000, Ok(17_472)), + (1_001, 1_001, 1_000_000, Ok(17_472)), + (1_001, 1_001, 10_000_000, Ok(9_013)), + (1_001, 1_001, 100_000_000, Ok(2_165)), // Basic math (1_000_000, 1_000_000, 250_000_000, Ok(1_010_000)), (1_000_000, 1_000_000, 62_500_000, Ok(3_030_000)), @@ -3107,7 +3101,7 @@ fn test_max_amount_remove_dynamic() { 21_000_000_000_000_000, 1_000_000, 21_000_000_000_000_000, - Ok(30_700_000), + Ok(17_455_533), ), (21_000_000_000_000_000, 1_000_000, u64::MAX, Ok(67_000)), ( @@ -3145,7 +3139,7 @@ fn test_max_amount_remove_dynamic() { SubnetAlphaIn::::insert(netuid, alpha_in); if !alpha_in.is_zero() { - let expected_price = I96F32::from_num(tao_in) / I96F32::from_num(alpha_in); + let expected_price = U64F64::from_num(tao_in) / U64F64::from_num(alpha_in); assert_eq!( ::SwapInterface::current_alpha_price(netuid.into()), expected_price @@ -3217,7 +3211,7 @@ fn test_max_amount_move_root_root() { NetUid::ROOT, TaoBalance::from(1_000_000_001) ), - Err(Error::::ZeroMaxStakeAmount.into()) + Ok(0u64.into()) ); // 2.0 price on (root, root) => max is 0 @@ -3227,7 +3221,7 @@ fn test_max_amount_move_root_root() { NetUid::ROOT, TaoBalance::from(2_000_000_000) ), - Err(Error::::ZeroMaxStakeAmount.into()) + Ok(0u64.into()) ); }); } @@ -3282,7 +3276,7 @@ fn test_max_amount_move_root_stable() { netuid, TaoBalance::from(1_000_000_001) ), - Err(Error::::ZeroMaxStakeAmount.into()) + Ok(0u64.into()) ); // 2.0 price on (root, stable) => max is 0 @@ -3292,7 +3286,7 @@ fn test_max_amount_move_root_stable() { netuid, TaoBalance::from(2_000_000_000) ), - Err(Error::::ZeroMaxStakeAmount.into()) + Ok(0u64.into()) ); }); } @@ -3334,7 +3328,7 @@ fn test_max_amount_move_stable_dynamic() { dynamic_netuid, TaoBalance::from(2_000_000_000) ), - Err(Error::::ZeroMaxStakeAmount.into()) + Err(pallet_subtensor_swap::Error::::PriceLimitExceeded.into()) ); // 3.0 price => max is 0 @@ -3710,29 +3704,27 @@ fn test_max_amount_move_dynamic_dynamic() { expected_max_swappable, precision, )| { - let alpha_in_1 = AlphaBalance::from(alpha_in_1); - let alpha_in_2 = AlphaBalance::from(alpha_in_2); let expected_max_swappable = AlphaBalance::from(expected_max_swappable); // Forse-set alpha in and tao reserve to achieve relative price of subnets SubnetTAO::::insert(origin_netuid, TaoBalance::from(tao_in_1)); - SubnetAlphaIn::::insert(origin_netuid, alpha_in_1); + SubnetAlphaIn::::insert(origin_netuid, AlphaBalance::from(alpha_in_1)); SubnetTAO::::insert(destination_netuid, TaoBalance::from(tao_in_2)); - SubnetAlphaIn::::insert(destination_netuid, alpha_in_2); + SubnetAlphaIn::::insert(destination_netuid, AlphaBalance::from(alpha_in_2)); if !alpha_in_1.is_zero() && !alpha_in_2.is_zero() { - let origin_price = - I96F32::from_num(tao_in_1) / I96F32::from_num(u64::from(alpha_in_1)); - let dest_price = - I96F32::from_num(tao_in_2) / I96F32::from_num(u64::from(alpha_in_2)); - if dest_price != 0 { + let origin_price = tao_in_1 as f64 / alpha_in_1 as f64; + let dest_price = tao_in_2 as f64 / alpha_in_2 as f64; + if dest_price != 0. { let expected_price = origin_price / dest_price; - assert_eq!( - ::SwapInterface::current_alpha_price( + assert_abs_diff_eq!( + (::SwapInterface::current_alpha_price( origin_netuid.into() ) / ::SwapInterface::current_alpha_price( destination_netuid.into() - ), - expected_price + )) + .to_num::(), + expected_price, + epsilon = 0.000_000_001 ); } } @@ -3868,7 +3860,7 @@ fn test_add_stake_limit_fill_or_kill() { ); // Lower the amount and it should succeed now - let amount_ok = TaoBalance::from(450_000_000_000_u64); // fits the maximum + let amount_ok = TaoBalance::from(150_000_000_000_u64); // fits the maximum assert_ok!(SubtensorModule::add_stake_limit( RuntimeOrigin::signed(coldkey_account_id), hotkey_account_id, @@ -3956,7 +3948,6 @@ fn test_remove_stake_limit_ok() { let fee: u64 = (expected_alpha_reduction as f64 * 0.003) as u64; // Remove stake with slippage safety - remove_stake_rate_limit_for_tests(&hotkey_account_id, &coldkey_account_id, netuid); assert_ok!(SubtensorModule::remove_stake_limit( RuntimeOrigin::signed(coldkey_account_id), hotkey_account_id, @@ -4151,8 +4142,6 @@ fn test_remove_99_9991_per_cent_stake_works_precisely() { let coldkey_balance_before_remove = SubtensorModule::get_coldkey_balance(&coldkey_account_id); - remove_stake_rate_limit_for_tests(&hotkey_account_id, &coldkey_account_id, netuid); - let remove_amount = AlphaBalance::from( (U64F64::from_num(alpha) * U64F64::from_num(0.999991)).to_num::(), ); @@ -4223,7 +4212,6 @@ fn test_remove_99_9989_per_cent_stake_leaves_a_little() { )); // Remove 99.9989% stake - remove_stake_rate_limit_for_tests(&hotkey_account_id, &coldkey_account_id, netuid); let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( &hotkey_account_id, &coldkey_account_id, @@ -4305,13 +4293,9 @@ fn test_move_stake_limit_partial() { SubnetTAO::::insert(origin_netuid, tao_reserve); SubnetAlphaIn::::insert(origin_netuid, alpha_in); - SubnetTaoProvided::::insert(origin_netuid, TaoBalance::from(0_u64)); - SubnetAlphaInProvided::::insert(origin_netuid, AlphaBalance::from(0_u64)); SubnetTAO::::insert(destination_netuid, tao_reserve * 100_000.into()); SubnetAlphaIn::::insert(destination_netuid, alpha_in * 100_000.into()); - SubnetTaoProvided::::insert(destination_netuid, TaoBalance::from(0_u64)); - SubnetAlphaInProvided::::insert(destination_netuid, AlphaBalance::from(0_u64)); let origin_price = ::SwapInterface::current_alpha_price(origin_netuid.into()); @@ -4464,8 +4448,6 @@ fn test_unstake_all_alpha_works() { stake_amount )); - remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); - // Setup the pool so that removing all the TAO will keep liq above min mock::setup_reserves( netuid, @@ -4519,8 +4501,6 @@ fn test_unstake_all_works() { stake_amount * 10.into(), u64::from(stake_amount * 100.into()).into(), ); - remove_stake_rate_limit_for_tests(&hotkey, &coldkey, netuid); - // Unstake all alpha to free balance assert_ok!(SubtensorModule::unstake_all( RuntimeOrigin::signed(coldkey), @@ -4566,14 +4546,14 @@ fn test_stake_into_subnet_ok() { )); // Add stake with slippage safety and check if the result is ok - add_balance_to_coldkey_account(&coldkey, TaoBalance::MAX); + let large_balance = 20_000_000_000_000_000_u64; + add_balance_to_coldkey_account(&coldkey, large_balance.into()); assert_ok!(SubtensorModule::stake_into_subnet( &hotkey, &coldkey, netuid, amount.into(), - TaoBalance::MAX, - false, + large_balance.into(), false, )); let fee_rate = pallet_subtensor_swap::FeeRate::::get(NetUid::from(netuid)) as f64 @@ -4621,23 +4601,25 @@ fn test_stake_into_subnet_low_amount() { )); // Add stake with slippage safety and check if the result is ok - add_balance_to_coldkey_account(&coldkey, TaoBalance::MAX); + let large_balance = 20_000_000_000_000_000_u64; + add_balance_to_coldkey_account(&coldkey, large_balance.into()); assert_ok!(SubtensorModule::stake_into_subnet( &hotkey, &coldkey, netuid, amount.into(), - TaoBalance::MAX, - false, + large_balance.into(), false, )); - let expected_stake = AlphaBalance::from(((amount as f64) * 0.997 / current_price) as u64); + let expected_stake = (amount as f64) * 0.997 / current_price; // Check if stake has increased assert_abs_diff_eq!( - SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid), + u64::from(SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid + )) as f64, expected_stake, - epsilon = 1.into() + epsilon = expected_stake / 100. ); }); } @@ -4670,14 +4652,14 @@ fn test_unstake_from_subnet_low_amount() { )); // Add stake and check if the result is ok - add_balance_to_coldkey_account(&coldkey, TaoBalance::MAX); + let large_balance = 20_000_000_000_000_000_u64; + add_balance_to_coldkey_account(&coldkey, large_balance.into()); assert_ok!(SubtensorModule::stake_into_subnet( &hotkey, &coldkey, netuid, amount.into(), - TaoBalance::MAX, - false, + large_balance.into(), false, )); @@ -4796,7 +4778,6 @@ fn test_unstake_from_subnet_prohibitive_limit() { amount.into(), TaoBalance::MAX, false, - false, )); // Remove stake @@ -4872,7 +4853,6 @@ fn test_unstake_full_amount() { amount.into(), TaoBalance::MAX, false, - false, )); // Remove stake @@ -4914,34 +4894,9 @@ fn test_unstake_full_amount() { }); } -fn price_to_tick(price: f64) -> TickIndex { - let price_sqrt: U64F64 = U64F64::from_num(price.sqrt()); - // Handle potential errors in the conversion - match TickIndex::try_from_sqrt_price(price_sqrt) { - Ok(mut tick) => { - // Ensure the tick is within bounds - if tick > TickIndex::MAX { - tick = TickIndex::MAX; - } else if tick < TickIndex::MIN { - tick = TickIndex::MIN; - } - tick - } - // Default to a reasonable value when conversion fails - Err(_) => { - if price > 1.0 { - TickIndex::MAX - } else { - TickIndex::MIN - } - } - } -} - /// Test correctness of swap fees: /// 1. TAO is not minted or burned /// 2. Fees match FeeRate -/// #[test] fn test_swap_fees_tao_correctness() { new_test_ext(1).execute_with(|| { @@ -4957,7 +4912,6 @@ fn test_swap_fees_tao_correctness() { let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); add_balance_to_coldkey_account(&owner_coldkey, owner_balance_before); add_balance_to_coldkey_account(&coldkey, user_balance_before); - pallet_subtensor_swap::EnabledUserLiquidity::::insert(NetUid::from(netuid), true); // Forse-set alpha in and tao reserve to make price equal 0.25 let tao_reserve = TaoBalance::from(100_000_000_000_u64); @@ -4986,18 +4940,6 @@ fn test_swap_fees_tao_correctness() { .to_num::() + 0.0001; let limit_price = current_price + 0.01; - let tick_low = price_to_tick(current_price); - let tick_high = price_to_tick(limit_price); - let liquidity = amount; - - assert_ok!(::SwapInterface::do_add_liquidity( - netuid.into(), - &owner_coldkey, - &owner_hotkey, - tick_low, - tick_high, - u64::from(liquidity), - )); // Limit-buy and then sell all alpha for user to hit owner liquidity assert_ok!(SubtensorModule::add_stake_limit( @@ -5014,7 +4956,6 @@ fn test_swap_fees_tao_correctness() { &coldkey, netuid, ); - remove_stake_rate_limit_for_tests(&owner_hotkey, &coldkey, netuid); assert_ok!(SubtensorModule::remove_stake( RuntimeOrigin::signed(coldkey), owner_hotkey, @@ -5275,7 +5216,6 @@ fn test_default_min_stake_sufficiency() { let fee_stake = (fee_rate * u64::from(amount) as f64) as u64; let current_price_after_stake = ::SwapInterface::current_alpha_price(netuid.into()); - remove_stake_rate_limit_for_tests(&owner_hotkey, &coldkey, netuid); let user_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( &owner_hotkey, &coldkey, @@ -5299,50 +5239,30 @@ fn test_default_min_stake_sufficiency() { } #[test] -fn test_stake_rate_limits() { - new_test_ext(0).execute_with(|| { - // Create subnet and accounts. - let subnet_owner_coldkey = U256::from(10); - let subnet_owner_hotkey = U256::from(20); - let hot1 = U256::from(1); - let cold1 = U256::from(3); - let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - let amount = DefaultMinStake::::get() * 10.into(); - let fee = DefaultMinStake::::get(); - let init_balance = amount + fee + ExistentialDeposit::get(); +fn test_large_swap() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(1); + let owner_coldkey = U256::from(2); + let coldkey = U256::from(100); - register_ok_neuron(netuid, hot1, cold1, 0); - Delegates::::insert(hot1, SubtensorModule::get_min_delegate_take()); - assert_eq!(SubtensorModule::get_owning_coldkey_for_hotkey(&hot1), cold1); + // add network + let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_000_u64.into()); + let tao = TaoBalance::from(100_000_000u64); + let alpha = AlphaBalance::from(1_000_000_000_000_000_u64); + SubnetTAO::::insert(netuid, tao); + SubnetAlphaIn::::insert(netuid, alpha); - add_balance_to_coldkey_account(&cold1, init_balance); + // Force the swap to initialize + ::SwapInterface::init_swap(netuid, None); + + let swap_amount = TaoBalance::from(100_000_000_000_000_u64); assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(cold1), - hot1, + RuntimeOrigin::signed(coldkey), + owner_hotkey, netuid, - (amount + fee).into() + swap_amount, )); - - assert_err!( - SubtensorModule::remove_stake( - RuntimeOrigin::signed(cold1), - hot1, - netuid, - AlphaBalance::from(amount.to_u64()) - ), - Error::::StakingOperationRateLimitExceeded - ); - - // Test limit clear each block - assert!(StakingOperationRateLimiter::::contains_key(( - hot1, cold1, netuid - ))); - - next_block(); - - assert!(!StakingOperationRateLimiter::::contains_key(( - hot1, cold1, netuid - ))); }); } @@ -5494,14 +5414,14 @@ fn test_staking_records_flow() { .unwrap(); // Add stake with slippage safety and check if the result is ok - add_balance_to_coldkey_account(&coldkey, TaoBalance::MAX); + let large_balance = 20_000_000_000_000_000_u64; + add_balance_to_coldkey_account(&coldkey, large_balance.into()); assert_ok!(SubtensorModule::stake_into_subnet( &hotkey, &coldkey, netuid, amount.into(), - TaoBalance::MAX, - false, + large_balance.into(), false, )); let fee_rate = pallet_subtensor_swap::FeeRate::::get(NetUid::from(netuid)) as f64 diff --git a/pallets/subtensor/src/tests/subnet.rs b/pallets/subtensor/src/tests/subnet.rs index 12b23c74e4..be1487b4ec 100644 --- a/pallets/subtensor/src/tests/subnet.rs +++ b/pallets/subtensor/src/tests/subnet.rs @@ -633,8 +633,6 @@ fn test_subtoken_enable_trading_ok_with_enable() { stake_amount )); - remove_stake_rate_limit_for_tests(&hotkey_account_id, &coldkey_account_id, netuid); - assert_ok!(SubtensorModule::remove_stake( RuntimeOrigin::signed(coldkey_account_id), hotkey_account_id, @@ -665,8 +663,6 @@ fn test_subtoken_enable_trading_ok_with_enable() { unstake_amount, )); - remove_stake_rate_limit_for_tests(&hotkey_account_2_id, &coldkey_account_id, netuid); - assert_ok!(SubtensorModule::transfer_stake( RuntimeOrigin::signed(coldkey_account_id), hotkey_account_id, @@ -742,63 +738,6 @@ fn test_subtoken_enable_ok_for_burn_register_before_enable() { }); } -// #[test] -// fn test_user_liquidity_access_control() { -// new_test_ext(1).execute_with(|| { -// let owner_hotkey = U256::from(1); -// let owner_coldkey = U256::from(2); -// let not_owner = U256::from(999); // arbitrary non-owner - -// // add network -// let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); - -// // Not owner, not root: should fail -// assert_noop!( -// Swap::toggle_user_liquidity(RuntimeOrigin::signed(not_owner), netuid, true), -// DispatchError::BadOrigin -// ); - -// // Subnet owner can enable -// assert_ok!(Swap::toggle_user_liquidity( -// RuntimeOrigin::signed(owner_coldkey), -// netuid, -// true -// )); -// assert!(pallet_subtensor_swap::EnabledUserLiquidity::::get( -// NetUid::from(netuid) -// )); - -// // Root can disable -// assert_ok!(Swap::toggle_user_liquidity( -// RuntimeOrigin::root(), -// netuid, -// false -// )); -// assert!(!pallet_subtensor_swap::EnabledUserLiquidity::::get( -// NetUid::from(netuid) -// )); - -// // Root can enable again -// assert_ok!(Swap::toggle_user_liquidity( -// RuntimeOrigin::root(), -// netuid, -// true -// )); -// assert!(pallet_subtensor_swap::EnabledUserLiquidity::::get( -// NetUid::from(netuid) -// )); - -// // Subnet owner cannot disable (only root can disable) -// assert_noop!( -// Swap::toggle_user_liquidity(RuntimeOrigin::signed(owner_coldkey), netuid, false), -// DispatchError::BadOrigin -// ); -// assert!(pallet_subtensor_swap::EnabledUserLiquidity::::get( -// NetUid::from(netuid) -// )); -// }); -// } - // cargo test --package pallet-subtensor --lib -- tests::subnet::test_no_duplicates_in_symbol_static --exact --show-output #[test] fn test_no_duplicates_in_symbol_static() { diff --git a/pallets/subtensor/src/tests/subnet_info.rs b/pallets/subtensor/src/tests/subnet_info.rs index fcf597f4ff..25079a9a7b 100644 --- a/pallets/subtensor/src/tests/subnet_info.rs +++ b/pallets/subtensor/src/tests/subnet_info.rs @@ -20,6 +20,7 @@ const EXPECTED_V3_NAMES: &[&[u8]] = &[ b"weights_version", b"weights_rate_limit", b"activity_cutoff", + b"activity_cutoff_factor", b"registration_allowed", b"target_regs_per_interval", b"min_burn", @@ -43,6 +44,7 @@ const EXPECTED_V3_NAMES: &[&[u8]] = &[ b"user_liquidity_enabled", b"owner_cut_enabled", b"owner_cut_auto_lock_enabled", + b"min_childkey_take", ]; fn find<'a>(params: &'a [HyperparamEntry], name: &[u8]) -> &'a HyperparamValue { @@ -100,10 +102,12 @@ fn test_get_subnet_hyperparams_v3_values_reflect_storage() { SubtensorModule::set_kappa(netuid, 12); SubtensorModule::set_immunity_period(netuid, 13); SubtensorModule::set_min_allowed_weights(netuid, 14); - SubtensorModule::set_tempo(netuid, 16); + SubtensorModule::set_tempo_unchecked(netuid, 16); SubtensorModule::set_weights_version_key(netuid, 19); SubtensorModule::set_weights_set_rate_limit(netuid, 20); - SubtensorModule::set_activity_cutoff(netuid, 22); + // `activity_cutoff` is derived: factor_milli * tempo / 1000. With tempo=16, + // factor 1375 yields 1375 * 16 / 1000 = 22 effective cutoff blocks. + SubtensorModule::set_activity_cutoff_factor_milli(netuid, 1375); SubtensorModule::set_network_registration_allowed(netuid, false); SubtensorModule::set_target_registrations_per_interval(netuid, 24); SubtensorModule::set_min_burn(netuid, TaoBalance::from(25u64)); @@ -121,6 +125,8 @@ fn test_get_subnet_hyperparams_v3_values_reflect_storage() { SubtensorModule::set_bonds_reset(netuid, true); SubtensorModule::set_owner_cut_enabled_flag(netuid, true); SubtensorModule::set_owner_cut_auto_lock_enabled(netuid, true); + SubtensorModule::set_min_childkey_take(31); + SubtensorModule::set_min_childkey_take_for_subnet(netuid, 32); let result = SubtensorModule::get_subnet_hyperparams_v3(netuid).unwrap(); let p = &result; @@ -161,7 +167,11 @@ fn test_get_subnet_hyperparams_v3_values_reflect_storage() { assert_eq!(find(p, b"tempo"), &HyperparamValue::U16(Compact(16))); assert_eq!( find(p, b"activity_cutoff"), - &HyperparamValue::U16(Compact(22)) + &HyperparamValue::U64(Compact(22)) + ); + assert_eq!( + find(p, b"activity_cutoff_factor"), + &HyperparamValue::U32(Compact(1375)) ); assert_eq!( find(p, b"target_regs_per_interval"), @@ -180,6 +190,11 @@ fn test_get_subnet_hyperparams_v3_values_reflect_storage() { &HyperparamValue::U16(Compact(30)) ); assert_eq!(find(p, b"yuma_version"), &HyperparamValue::U16(Compact(3))); + // Effective min childkey take = max(global, per-subnet). + assert_eq!( + find(p, b"min_childkey_take"), + &HyperparamValue::U16(Compact(32)) + ); // U64 variants assert_eq!( diff --git a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs index 212df1e48a..1e200aaedd 100644 --- a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs +++ b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs @@ -2928,7 +2928,7 @@ fn test_swap_hotkey_root_claims_unchanged_if_not_root() { let netuid = add_dynamic_network(&neuron_hotkey, &owner_coldkey); let new_hotkey = U256::from(10030); - add_balance_to_coldkey_account(&owner_coldkey, u64::MAX.into()); + add_balance_to_coldkey_account(&owner_coldkey, 20_000_000_000_000_000_u64.into()); SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 let root_stake = 2_000_000_000u64; @@ -3014,7 +3014,7 @@ fn test_swap_hotkey_root_claims_changed_if_root() { // Use neuron_hotkey as subnet creator so it receives root dividends let netuid_1 = add_dynamic_network(&neuron_hotkey, &owner_coldkey); - add_balance_to_coldkey_account(&owner_coldkey, u64::MAX.into()); + add_balance_to_coldkey_account(&owner_coldkey, 20_000_000_000_000_000_u64.into()); SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 let root_stake = 2_000_000_000u64; @@ -3103,7 +3103,7 @@ fn test_swap_hotkey_root_claims_changed_if_all_subnets() { // Use neuron_hotkey as subnet creator so it receives root dividends let netuid_1 = add_dynamic_network(&neuron_hotkey, &owner_coldkey); - add_balance_to_coldkey_account(&owner_coldkey, u64::MAX.into()); + add_balance_to_coldkey_account(&owner_coldkey, 20_000_000_000_000_000_u64.into()); SubtensorModule::set_tao_weight(u64::MAX); // Set TAO weight to 1.0 let root_stake = 2_000_000_000u64; @@ -3184,7 +3184,7 @@ fn test_swap_hotkey_auto_parent_delegation_transferred_on_root() { let new_hotkey = U256::from(1005); let _ = add_dynamic_network(&old_hotkey, &owner_coldkey); - add_balance_to_coldkey_account(&owner_coldkey, u64::MAX.into()); + add_balance_to_coldkey_account(&owner_coldkey, 20_000_000_000_000_000_u64.into()); // Opt out of auto parent delegation on the old hotkey. AutoParentDelegationEnabled::::insert(old_hotkey, false); @@ -3225,7 +3225,7 @@ fn test_swap_hotkey_auto_parent_delegation_transferred_on_all_subnets() { NetworksAdded::::insert(NetUid::ROOT, true); let _ = add_dynamic_network(&old_hotkey, &owner_coldkey); - add_balance_to_coldkey_account(&owner_coldkey, u64::MAX.into()); + add_balance_to_coldkey_account(&owner_coldkey, 20_000_000_000_000_000_u64.into()); AutoParentDelegationEnabled::::insert(old_hotkey, false); @@ -3257,7 +3257,7 @@ fn test_swap_hotkey_auto_parent_delegation_not_transferred_on_non_root() { let new_hotkey = U256::from(1005); let netuid = add_dynamic_network(&old_hotkey, &owner_coldkey); - add_balance_to_coldkey_account(&owner_coldkey, u64::MAX.into()); + add_balance_to_coldkey_account(&owner_coldkey, 20_000_000_000_000_000_u64.into()); AutoParentDelegationEnabled::::insert(old_hotkey, false); diff --git a/pallets/subtensor/src/tests/tempo_control.rs b/pallets/subtensor/src/tests/tempo_control.rs new file mode 100644 index 0000000000..25d3abc691 --- /dev/null +++ b/pallets/subtensor/src/tests/tempo_control.rs @@ -0,0 +1,245 @@ +#![allow(clippy::expect_used)] +use frame_support::{assert_noop, assert_ok}; +use frame_system::Config; +use sp_core::U256; +use subtensor_runtime_common::NetUid; + +use super::mock::*; +use crate::{ + ActivityCutoffFactorMilli, AdminFreezeWindow, CommitRevealWeightsEnabled, LastEpochBlock, + PendingEpochAt, SubnetOwner, SubtokenEnabled, Tempo, +}; + +const DEFAULT_TEMPO: u16 = 360; +const NEW_TEMPO: u16 = 720; + +fn setup_subnet(owner: U256) -> NetUid { + let netuid = NetUid::from(1); + add_network(netuid, DEFAULT_TEMPO, 0); + SubnetOwner::::insert(netuid, owner); + SubtokenEnabled::::insert(netuid, true); + crate::Pallet::::set_admin_freeze_window(0); + netuid +} + +#[test] +fn do_set_tempo_works_with_commit_reveal_enabled() { + new_test_ext(1).execute_with(|| { + let owner = U256::from(1); + let netuid = setup_subnet(owner); + + // CR is enabled by default; `set_tempo` is no longer blocked for CR + // subnets — CR timing keys off the stateful `SubnetEpochIndex` counter. + assert!(CommitRevealWeightsEnabled::::get(netuid)); + + assert_ok!(crate::Pallet::::do_set_tempo( + <::RuntimeOrigin>::signed(owner), + netuid, + NEW_TEMPO, + )); + + assert_eq!(Tempo::::get(netuid), NEW_TEMPO); + }); +} + +#[test] +fn do_trigger_epoch_blocked_with_commit_reveal_enabled() { + new_test_ext(1).execute_with(|| { + let owner = U256::from(1); + let netuid = setup_subnet(owner); + + // CR enabled by default; an out-of-band epoch would desync the CRv3 reveal + // window from the Drand schedule and drop committed weights, so it is blocked. + assert!(CommitRevealWeightsEnabled::::get(netuid)); + AdminFreezeWindow::::set(5); + + assert_noop!( + crate::Pallet::::do_trigger_epoch( + <::RuntimeOrigin>::signed(owner), + netuid, + ), + crate::Error::::DynamicTempoBlockedByCommitReveal + ); + + // No pending epoch was scheduled. + assert_eq!(PendingEpochAt::::get(netuid), 0); + }); +} + +#[test] +fn do_trigger_epoch_works_with_commit_reveal_disabled() { + new_test_ext(1).execute_with(|| { + let owner = U256::from(1); + let netuid = setup_subnet(owner); + + // With CR disabled there is no reveal window to protect, so the trigger fires. + CommitRevealWeightsEnabled::::insert(netuid, false); + AdminFreezeWindow::::set(5); + + assert_ok!(crate::Pallet::::do_trigger_epoch( + <::RuntimeOrigin>::signed(owner), + netuid, + )); + + let now = crate::Pallet::::get_current_block_as_u64(); + assert_eq!(PendingEpochAt::::get(netuid), now + 5); + }); +} + +#[test] +fn do_set_activity_cutoff_factor_works_for_root_bypassing_freeze_window() { + new_test_ext(1).execute_with(|| { + let owner = U256::from(1); + let netuid = setup_subnet(owner); + + // Engage the admin freeze window so an owner-call would fail. + Tempo::::insert(netuid, 10u16); + LastEpochBlock::::insert(netuid, 1u64); + AdminFreezeWindow::::set(8); + run_to_block(5); + + // Owner cannot bypass the freeze window. + assert_noop!( + crate::Pallet::::do_set_activity_cutoff_factor( + <::RuntimeOrigin>::signed(owner), + netuid, + 5_000u32, + ), + crate::Error::::AdminActionProhibitedDuringWeightsWindow + ); + + // Root bypasses both freeze window and rate limit. + assert_ok!(crate::Pallet::::do_set_activity_cutoff_factor( + <::RuntimeOrigin>::root(), + netuid, + 5_000u32, + )); + assert_eq!(ActivityCutoffFactorMilli::::get(netuid), 5_000u32); + }); +} + +#[test] +fn do_trigger_epoch_rejects_when_auto_epoch_already_imminent() { + new_test_ext(1).execute_with(|| { + let owner = U256::from(1); + let netuid = setup_subnet(owner); + + // Disable CR so the trigger reaches the imminent-auto-epoch check rather than + // being short-circuited by the commit-reveal guard. + CommitRevealWeightsEnabled::::insert(netuid, false); + + // Make the next auto epoch closer than AdminFreezeWindow. + // remaining = (LastEpochBlock + tempo) - now = (1 + 10) - 5 = 6, window = 8 => reject. + Tempo::::insert(netuid, 10u16); + LastEpochBlock::::insert(netuid, 1u64); + AdminFreezeWindow::::set(8); + run_to_block(5); + + assert_noop!( + crate::Pallet::::do_trigger_epoch( + <::RuntimeOrigin>::signed(owner), + netuid, + ), + crate::Error::::AutoEpochAlreadyImminent + ); + + // Nothing was scheduled. + assert_eq!(PendingEpochAt::::get(netuid), 0); + }); +} + +#[test] +fn get_next_epoch_start_block_returns_none_when_tempo_zero() { + new_test_ext(1).execute_with(|| { + let owner = U256::from(1); + let netuid = setup_subnet(owner); + + Tempo::::insert(netuid, 0); + + assert_eq!( + crate::Pallet::::get_next_epoch_start_block(netuid), + None + ); + }); +} + +#[test] +fn get_next_epoch_start_block_uses_last_epoch_block_plus_tempo() { + new_test_ext(1).execute_with(|| { + let owner = U256::from(1); + let netuid = setup_subnet(owner); + + LastEpochBlock::::insert(netuid, 100u64); + Tempo::::insert(netuid, 50u16); + PendingEpochAt::::insert(netuid, 0u64); + + // last (100) + tempo (50) = 150 + assert_eq!( + crate::Pallet::::get_next_epoch_start_block(netuid), + Some(150) + ); + }); +} + +#[test] +fn get_next_epoch_start_block_returns_pending_when_pending_is_earlier() { + new_test_ext(1).execute_with(|| { + let owner = U256::from(1); + let netuid = setup_subnet(owner); + + LastEpochBlock::::insert(netuid, 100u64); + Tempo::::insert(netuid, 50u16); + // Owner-triggered manual fire scheduled before automatic next. + PendingEpochAt::::insert(netuid, 120u64); + + // min(150, 120) = 120 + assert_eq!( + crate::Pallet::::get_next_epoch_start_block(netuid), + Some(120) + ); + }); +} + +#[test] +fn get_next_epoch_start_block_ignores_pending_when_auto_is_earlier() { + new_test_ext(1).execute_with(|| { + let owner = U256::from(1); + let netuid = setup_subnet(owner); + + LastEpochBlock::::insert(netuid, 100u64); + Tempo::::insert(netuid, 50u16); + // Pending scheduled after the next automatic fire. + PendingEpochAt::::insert(netuid, 200u64); + + // min(150, 200) = 150 + assert_eq!( + crate::Pallet::::get_next_epoch_start_block(netuid), + Some(150) + ); + }); +} + +#[test] +fn get_next_epoch_start_block_reflects_set_tempo_cycle_reset() { + new_test_ext(1).execute_with(|| { + let owner = U256::from(1); + let netuid = setup_subnet(owner); + + run_to_block(10); + let new_tempo: u16 = 720; + + assert_ok!(crate::Pallet::::do_set_tempo( + <::RuntimeOrigin>::signed(owner), + netuid, + new_tempo, + )); + + let now = crate::Pallet::::get_current_block_as_u64(); + // apply_tempo_with_cycle_reset sets LastEpochBlock = now; + // next fire is now + tempo. + assert_eq!( + crate::Pallet::::get_next_epoch_start_block(netuid), + Some(now + new_tempo as u64) + ); + }); +} diff --git a/pallets/subtensor/src/tests/transaction_extension_pays_no.rs b/pallets/subtensor/src/tests/transaction_extension_pays_no.rs index fd8c8ccc85..cea918ddd9 100644 --- a/pallets/subtensor/src/tests/transaction_extension_pays_no.rs +++ b/pallets/subtensor/src/tests/transaction_extension_pays_no.rs @@ -238,7 +238,7 @@ fn extension_reveal_weights_rejects_commit_not_found() { crate::Owner::::insert(hotkey, coldkey); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); SubtensorModule::set_stake_threshold(0); - add_balance_to_coldkey_account(&hotkey, u64::MAX.into()); + add_balance_to_coldkey_account(&hotkey, 1_000_000_000_000_u64.into()); assert_ok!(SubtensorModule::do_add_stake( RuntimeOrigin::signed(hotkey), hotkey, @@ -274,7 +274,7 @@ fn extension_reveal_mechanism_weights_rejects_commit_not_found() { crate::Owner::::insert(hotkey, coldkey); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); SubtensorModule::set_stake_threshold(0); - add_balance_to_coldkey_account(&hotkey, u64::MAX.into()); + add_balance_to_coldkey_account(&hotkey, 1_000_000_000_000_u64.into()); assert_ok!(SubtensorModule::do_add_stake( RuntimeOrigin::signed(hotkey), hotkey, @@ -333,7 +333,7 @@ fn assert_reveal_mechanism_weights_accepts_valid_commit( } SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); SubtensorModule::set_stake_threshold(0); - add_balance_to_coldkey_account(&hotkey, u64::MAX.into()); + add_balance_to_coldkey_account(&hotkey, 1_000_000_000_000_u64.into()); assert_ok!(SubtensorModule::do_add_stake( RuntimeOrigin::signed(hotkey), hotkey, diff --git a/pallets/subtensor/src/tests/weights.rs b/pallets/subtensor/src/tests/weights.rs index f9afd96033..6490b5f824 100644 --- a/pallets/subtensor/src/tests/weights.rs +++ b/pallets/subtensor/src/tests/weights.rs @@ -120,7 +120,7 @@ fn test_commit_weights_validate() { SubtensorModule::append_neuron(netuid, &hotkey, 0); crate::Owner::::insert(hotkey, coldkey); - add_balance_to_coldkey_account(&hotkey, u64::MAX.into()); + add_balance_to_coldkey_account(&hotkey, 20_000_000_000_000_000_u64.into()); let min_stake = 500_000_000_000_u64; let reserve = min_stake * 1000; @@ -256,7 +256,7 @@ fn test_set_weights_validate() { SubtensorModule::append_neuron(netuid, &hotkey, 0); crate::Owner::::insert(hotkey, coldkey); - add_balance_to_coldkey_account(&hotkey, u64::MAX.into()); + add_balance_to_coldkey_account(&hotkey, 20_000_000_000_000_000_u64.into()); let min_stake = TaoBalance::from(500_000_000_000_u64); @@ -351,9 +351,8 @@ fn test_reveal_weights_validate() { &salt, version_key, ); - let commit_block = SubtensorModule::get_current_block_as_u64(); - let (first_reveal_block, last_reveal_block) = - SubtensorModule::get_reveal_blocks(netuid, commit_block); + // Counter is 0 on a fresh subnet; tag the commit with epoch 0. + let commit_epoch: u64 = 0; // Create netuid add_network(netuid, tempo, 0); @@ -362,7 +361,7 @@ fn test_reveal_weights_validate() { SubtensorModule::append_neuron(netuid, &hotkey2, 0); crate::Owner::::insert(hotkey, coldkey); crate::Owner::::insert(hotkey2, coldkey); - add_balance_to_coldkey_account(&hotkey, u64::MAX.into()); + add_balance_to_coldkey_account(&hotkey, 20_000_000_000_000_000_u64.into()); let min_stake = TaoBalance::from(500_000_000_000_u64); // Set the minimum stake @@ -424,12 +423,7 @@ fn test_reveal_weights_validate() { WeightCommits::::mutate(NetUidStorageIndex::from(netuid), hotkey, |maybe_commits| { let mut commits: VecDeque<(H256, u64, u64, u64)> = maybe_commits.take().unwrap_or_default(); - commits.push_back(( - commit_hash, - commit_block, - first_reveal_block, - last_reveal_block, - )); + commits.push_back((commit_hash, commit_epoch, 0, 0)); *maybe_commits = Some(commits); }); @@ -448,7 +442,13 @@ fn test_reveal_weights_validate() { CustomTransactionError::CommitBlockNotInRevealRange.into() ); - System::set_block_number(commit_block + 2 * tempo as u64); + // Advance the epoch counter into the commit's reveal epoch + // (`commit_epoch + reveal_period`); pin the scheduler so the look-ahead + // does not overshoot. + let reveal_period = SubtensorModule::get_reveal_period(netuid); + SubnetEpochIndex::::insert(netuid, commit_epoch + reveal_period); + LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); + PendingEpochAt::::insert(netuid, 0); // Submit to the signed extension validate function let result_valid_stake = extension.validate( @@ -486,7 +486,9 @@ fn test_reveal_weights_validate() { // The call should still pass assert_ok!(result_more_stake); - System::set_block_number(commit_block + 10 * tempo as u64); + // Advance the counter past the commit's reveal epoch — now too late. + SubnetEpochIndex::::insert(netuid, commit_epoch + reveal_period + 1); + LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); // Submit to the signed extension validate function let result_too_late = extension.validate( @@ -545,7 +547,7 @@ fn test_batch_reveal_weights_validate() { SubtensorModule::append_neuron(netuid, &hotkey2, 0); crate::Owner::::insert(hotkey, coldkey); crate::Owner::::insert(hotkey2, coldkey); - add_balance_to_coldkey_account(&hotkey, u64::MAX.into()); + add_balance_to_coldkey_account(&hotkey, 20_000_000_000_000_000_u64.into()); SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); let min_stake = TaoBalance::from(500_000_000_000_u64); @@ -651,7 +653,12 @@ fn test_batch_reveal_weights_validate() { )); } - let commit_block = SubtensorModule::get_current_block_as_u64(); + // Epoch all the commits were tagged with (committed in a tight loop). + let commit_epoch = + crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey) + .and_then(|q| q.back().map(|(_, e, _, _)| *e)) + .expect("commit stored"); + let reveal_period = SubtensorModule::get_reveal_period(netuid); // Test 5: CommitBlockNotInRevealRange - Try to reveal too early let result_too_early = extension.validate( @@ -668,8 +675,10 @@ fn test_batch_reveal_weights_validate() { CustomTransactionError::CommitBlockNotInRevealRange.into() ); - // Move to valid reveal period - System::set_block_number(commit_block + 2 * tempo as u64); + // Advance the epoch counter into the commits' reveal epoch. + SubnetEpochIndex::::insert(netuid, commit_epoch + reveal_period); + LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); + PendingEpochAt::::insert(netuid, 0); // Now the call should pass the signed extension validation let result_valid_time = extension.validate( @@ -683,8 +692,9 @@ fn test_batch_reveal_weights_validate() { ); assert_ok!(result_valid_time); - // Test 6: CommitBlockNotInRevealRange - Try to reveal too late - System::set_block_number(commit_block + 10 * tempo as u64); + // Test 6: CommitBlockNotInRevealRange - reveal too late (counter past reveal epoch) + SubnetEpochIndex::::insert(netuid, commit_epoch + reveal_period + 1); + LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); let result_too_late = extension.validate( RawOrigin::Signed(who).into(), @@ -783,7 +793,7 @@ fn test_set_stake_threshold_failed() { add_network_disable_commit_reveal(netuid, 1, 0); register_ok_neuron(netuid, hotkey, coldkey, 2143124); SubtensorModule::set_stake_threshold(20_000_000_000_000); - add_balance_to_coldkey_account(&hotkey, u64::MAX.into()); + add_balance_to_coldkey_account(&hotkey, 20_000_000_000_000_000_u64.into()); // Check the signed extension function. assert_eq!(SubtensorModule::get_stake_threshold(), 20_000_000_000_000); @@ -2231,7 +2241,7 @@ fn test_tempo_change_during_commit_reveal_process() { let tempo_before_next_reveal: u16 = 200; log::info!("Changing tempo to {tempo_before_next_reveal}"); - SubtensorModule::set_tempo(netuid, tempo_before_next_reveal); + SubtensorModule::set_tempo_unchecked(netuid, tempo_before_next_reveal); step_epochs(1, netuid); log::info!( @@ -2264,7 +2274,7 @@ fn test_tempo_change_during_commit_reveal_process() { let tempo: u16 = 150; log::info!("Changing tempo to {tempo}"); - SubtensorModule::set_tempo(netuid, tempo); + SubtensorModule::set_tempo_unchecked(netuid, tempo); step_epochs(1, netuid); log::info!( @@ -2287,7 +2297,7 @@ fn test_tempo_change_during_commit_reveal_process() { let tempo: u16 = 1050; log::info!("Changing tempo to {tempo}"); - SubtensorModule::set_tempo(netuid, tempo); + SubtensorModule::set_tempo_unchecked(netuid, tempo); assert_ok!(SubtensorModule::commit_weights( RuntimeOrigin::signed(hotkey), @@ -2301,7 +2311,7 @@ fn test_tempo_change_during_commit_reveal_process() { let tempo: u16 = 805; log::info!("Changing tempo to {tempo}"); - SubtensorModule::set_tempo(netuid, tempo); + SubtensorModule::set_tempo_unchecked(netuid, tempo); step_epochs(1, netuid); log::info!( @@ -3149,7 +3159,7 @@ fn test_tempo_and_reveal_period_change_during_commit_reveal_process() { // Step 2: Change tempo and reveal period after commit let new_tempo: u16 = 50; let new_reveal_period: u64 = 2; - SubtensorModule::set_tempo(netuid, new_tempo); + SubtensorModule::set_tempo_unchecked(netuid, new_tempo); assert_ok!(SubtensorModule::set_reveal_period(netuid, new_reveal_period)); log::info!( "Changed tempo to {new_tempo} and reveal period to {new_reveal_period}" @@ -3203,7 +3213,7 @@ fn test_tempo_and_reveal_period_change_during_commit_reveal_process() { // Step 4: Change tempo and reveal period again after reveal let new_tempo_after_reveal: u16 = 200; let new_reveal_period_after_reveal: u64 = 1; - SubtensorModule::set_tempo(netuid, new_tempo_after_reveal); + SubtensorModule::set_tempo_unchecked(netuid, new_tempo_after_reveal); assert_ok!(SubtensorModule::set_reveal_period( netuid, new_reveal_period_after_reveal @@ -3427,49 +3437,31 @@ fn test_reveal_at_exact_block() { commit_hash )); - let commit_block = SubtensorModule::get_current_block_as_u64(); - let commit_epoch = SubtensorModule::get_epoch_index(netuid, commit_block); - let reveal_epoch = commit_epoch.saturating_add(reveal_period); + // Epoch the commit was tagged with (counter is the canonical index). + let commit_epoch = + crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey) + .and_then(|q| q.back().map(|(_, e, _, _)| *e)) + .expect("commit stored"); - // Calculate the block number where the reveal epoch starts - let tempo_plus_one = (tempo as u64).saturating_add(1); - let netuid_plus_one = (u16::from(netuid) as u64).saturating_add(1); - let reveal_epoch_start_block = reveal_epoch - .saturating_mul(tempo_plus_one) - .saturating_sub(netuid_plus_one); - - // Attempt to reveal before the reveal epoch starts - let current_block = SubtensorModule::get_current_block_as_u64(); - if current_block < reveal_epoch_start_block { - // Advance to one block before the reveal epoch starts - let blocks_to_advance = reveal_epoch_start_block - current_block; - if blocks_to_advance > 1 { - // Advance to one block before the reveal epoch - let new_block_number = current_block + blocks_to_advance - 1; - System::set_block_number(new_block_number); - } - - // Attempt to reveal too early - assert_err!( - SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key - ), - Error::::RevealTooEarly - ); + // Attempt to reveal before the reveal epoch — too early. + assert_err!( + SubtensorModule::reveal_weights( + RuntimeOrigin::signed(hotkey), + netuid, + uids.clone(), + weight_values.clone(), + salt.clone(), + version_key + ), + Error::::RevealTooEarly + ); - // Advance one more block to reach the exact reveal epoch start block - System::set_block_number(reveal_epoch_start_block); - } else { - // If we're already at or past the reveal epoch start block - System::set_block_number(reveal_epoch_start_block); - } + // Advance the epoch counter into the reveal epoch; pin the scheduler. + SubnetEpochIndex::::insert(netuid, commit_epoch + reveal_period); + LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); + PendingEpochAt::::insert(netuid, 0); - // Reveal at the exact allowed block + // Reveal at the exact allowed epoch assert_ok!(SubtensorModule::reveal_weights( RuntimeOrigin::signed(hotkey), netuid, @@ -3508,18 +3500,13 @@ fn test_reveal_at_exact_block() { new_commit_hash )); - // Advance blocks to after the commit expires - let commit_block = SubtensorModule::get_current_block_as_u64(); - let commit_epoch = SubtensorModule::get_epoch_index(netuid, commit_block); - let reveal_epoch = commit_epoch.saturating_add(reveal_period); - let expiration_epoch = reveal_epoch.saturating_add(1); - let expiration_epoch_start_block = expiration_epoch * tempo_plus_one - netuid_plus_one; - - let current_block = SubtensorModule::get_current_block_as_u64(); - if current_block < expiration_epoch_start_block { - // Advance to the block where the commit expires - System::set_block_number(expiration_epoch_start_block); - } + // Advance the epoch counter past the reveal epoch — commit expired. + let new_commit_epoch = + crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey) + .and_then(|q| q.back().map(|(_, e, _, _)| *e)) + .expect("commit stored"); + SubnetEpochIndex::::insert(netuid, new_commit_epoch + reveal_period + 1); + LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); // Attempt to reveal after the commit has expired assert_err!( @@ -4272,7 +4259,7 @@ fn test_highly_concurrent_commits_and_reveals_with_multiple_hotkeys() { } // ==== Modify Network Parameters During Commits ==== - SubtensorModule::set_tempo(netuid, 150); + SubtensorModule::set_tempo_unchecked(netuid, 150); assert_ok!(SubtensorModule::set_reveal_period(netuid, 7)); log::info!("Changed tempo to 150 and reveal_period to 7 during commits."); @@ -4318,7 +4305,7 @@ fn test_highly_concurrent_commits_and_reveals_with_multiple_hotkeys() { } // ==== Change Network Parameters Again ==== - SubtensorModule::set_tempo(netuid, 200); + SubtensorModule::set_tempo_unchecked(netuid, 200); assert_ok!(SubtensorModule::set_reveal_period(netuid, 10)); log::info!("Changed tempo to 200 and reveal_period to 10 after initial reveals."); @@ -4421,146 +4408,6 @@ fn test_highly_concurrent_commits_and_reveals_with_multiple_hotkeys() { }) } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_get_reveal_blocks --exact --show-output --nocapture -#[test] -fn test_get_reveal_blocks() { - new_test_ext(1).execute_with(|| { - // **1. Define Test Parameters** - let netuid = NetUid::from(1); - let uids: Vec = vec![0, 1]; - let weight_values: Vec = vec![10, 10]; - let salt: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8]; - let version_key: u64 = 0; - let hotkey: U256 = U256::from(1); - - // **2. Generate the Commit Hash** - let commit_hash: H256 = BlakeTwo256::hash_of(&( - hotkey, - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - )); - - // **3. Initialize the Block Number to 0** - System::set_block_number(0); - - // **4. Define Network Parameters** - let tempo: u16 = 5; - add_network(netuid, tempo, 0); - - // **5. Register Neurons and Configure the Network** - register_ok_neuron(netuid, U256::from(3), U256::from(4), 300_000); - register_ok_neuron(netuid, U256::from(1), U256::from(2), 100_000); - SubtensorModule::set_stake_threshold(0); - SubtensorModule::set_weights_set_rate_limit(netuid, 5); - SubtensorModule::set_validator_permit_for_uid(netuid, 0, true); - SubtensorModule::set_validator_permit_for_uid(netuid, 1, true); - SubtensorModule::set_commit_reveal_weights_enabled(netuid, true); - add_balance_to_coldkey_account(&U256::from(0), 1.into()); - add_balance_to_coldkey_account(&U256::from(1), 1.into()); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(0)), - &(U256::from(0)), - netuid, - 1.into(), - ); - SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( - &(U256::from(1)), - &(U256::from(1)), - netuid, - 1.into(), - ); - - // **6. Commit Weights at Block 0** - assert_ok!(SubtensorModule::commit_weights( - RuntimeOrigin::signed(hotkey), - netuid, - commit_hash - )); - - // **7. Retrieve the Reveal Blocks Using `get_reveal_blocks`** - let (first_reveal_block, last_reveal_block) = SubtensorModule::get_reveal_blocks(netuid, 0); - - // **8. Assert Correct Calculation of Reveal Blocks** - // With tempo=5, netuid=1, reveal_period=1: - // commit_epoch = (0 + 2) / 6 = 0 - // reveal_epoch = 0 + 1 = 1 - // first_reveal_block = 1 * 6 - 2 = 4 - // last_reveal_block = 4 + 5 = 9 - assert_eq!(first_reveal_block, 4); - assert_eq!(last_reveal_block, 9); - - // **9. Attempt to Reveal Before `first_reveal_block` (Block 3)** - step_block(3); // Advance to block 3 - let result = SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - ); - assert_err!(result, Error::::RevealTooEarly); - - // **10. Advance to `first_reveal_block` (Block 4)** - step_block(1); // Advance to block 4 - let result = SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - ); - assert_ok!(result); - - // **11. Attempt to Reveal Again at Block 4 (Should Fail)** - let result = SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - ); - assert_err!(result, Error::::NoWeightsCommitFound); - - // **12. Advance to After `last_reveal_block` (Block 10)** - step_block(6); // Advance from block 4 to block 10 - - // **13. Attempt to Reveal at Block 10 (Should Fail)** - let result = SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - ); - assert_err!(result, Error::::NoWeightsCommitFound); - - // **14. Attempt to Reveal Outside of Any Reveal Window (No Commit)** - let result = SubtensorModule::reveal_weights( - RuntimeOrigin::signed(hotkey), - netuid, - uids.clone(), - weight_values.clone(), - salt.clone(), - version_key, - ); - assert_err!(result, Error::::NoWeightsCommitFound); - - // **15. Verify that All Commits Have Been Removed from Storage** - let commits = crate::WeightCommits::::get(NetUidStorageIndex::from(netuid), hotkey); - assert!( - commits.is_none(), - "Commits should be cleared after successful reveal" - ); - }) -} - // SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::weights::test_commit_weights_rate_limit --exact --show-output --nocapture #[test] fn test_commit_weights_rate_limit() { @@ -5946,8 +5793,13 @@ fn test_reveal_crv3_commits_removes_past_epoch_commits() { // --------------------------------------------------------------------- // Put dummy commits into the two epochs immediately *before* current. // --------------------------------------------------------------------- + // Establish a non-zero epoch counter and pin the scheduler so the reveal + // pass sees exactly this epoch (no look-ahead increment). + let cur_epoch: u64 = 10; + SubnetEpochIndex::::insert(netuid, cur_epoch); + LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); + PendingEpochAt::::insert(netuid, 0); let cur_block = SubtensorModule::get_current_block_as_u64(); - let cur_epoch = SubtensorModule::get_epoch_index(netuid, cur_block); let past_epoch = cur_epoch.saturating_sub(2); // definitely < reveal_epoch let reveal_epoch = cur_epoch.saturating_sub(1); // == cur_epoch - reveal_period @@ -6226,18 +6078,16 @@ fn test_reveal_crv3_commits_max_neurons() { }); } +// `get_first_block_of_epoch` is a legacy modulo helper — NOT used by live +// commit-reveal logic #[test] fn test_get_first_block_of_epoch_epoch_zero() { new_test_ext(1).execute_with(|| { let netuid: NetUid = NetUid::from(1); - let tempo: u16 = 10; - add_network(netuid, tempo, 0); - - let first_block = SubtensorModule::get_first_block_of_epoch(netuid, 0); - assert_eq!(first_block, 0); + add_network(netuid, 10, 0); - // Cross-check: epoch at block 0 should be 0 - assert_eq!(SubtensorModule::get_epoch_index(netuid, 0), 0); + // 0 * 11 - 2, saturating at 0. + assert_eq!(SubtensorModule::get_first_block_of_epoch(netuid, 0), 0); }); } @@ -6245,15 +6095,10 @@ fn test_get_first_block_of_epoch_epoch_zero() { fn test_get_first_block_of_epoch_small_epoch() { new_test_ext(1).execute_with(|| { let netuid: NetUid = NetUid::from(0); - let tempo: u16 = 1; - add_network(netuid, tempo, 0); - - let first_block = SubtensorModule::get_first_block_of_epoch(netuid, 1); - assert_eq!(first_block, 1); // 1 * 2 - 1 = 1 + add_network(netuid, 1, 0); - // Cross-check - assert_eq!(SubtensorModule::get_epoch_index(netuid, 1), 1); - assert_eq!(SubtensorModule::get_epoch_index(netuid, 0), 0); + // 1 * 2 - 1 = 1. + assert_eq!(SubtensorModule::get_first_block_of_epoch(netuid, 1), 1); }); } @@ -6261,15 +6106,10 @@ fn test_get_first_block_of_epoch_small_epoch() { fn test_get_first_block_of_epoch_with_offset() { new_test_ext(1).execute_with(|| { let netuid: NetUid = NetUid::from(1); - let tempo: u16 = 10; - add_network(netuid, tempo, 0); - - let first_block = SubtensorModule::get_first_block_of_epoch(netuid, 1); - assert_eq!(first_block, 9); // 1 * 11 - 2 = 9 + add_network(netuid, 10, 0); - // Cross-check - assert_eq!(SubtensorModule::get_epoch_index(netuid, 9), 1); - assert_eq!(SubtensorModule::get_epoch_index(netuid, 8), 0); + // 1 * 11 - 2 = 9. + assert_eq!(SubtensorModule::get_first_block_of_epoch(netuid, 1), 9); }); } @@ -6277,61 +6117,14 @@ fn test_get_first_block_of_epoch_with_offset() { fn test_get_first_block_of_epoch_large_epoch() { new_test_ext(1).execute_with(|| { let netuid: NetUid = NetUid::from(0); - let tempo: u16 = 100; - add_network(netuid, tempo, 0); + add_network(netuid, 100, 0); let epoch: u64 = 1000; - let first_block = SubtensorModule::get_first_block_of_epoch(netuid, epoch); - assert_eq!(first_block, epoch * 101 - 1); // No overflow for this size - - // Cross-check (simulate, as large block not runnable, but math holds) - assert_eq!(first_block + 1, epoch * 101); - }); -} - -#[test] -fn test_get_first_block_of_epoch_step_blocks_and_assert_with_until_next() { - new_test_ext(1).execute_with(|| { - let netuid: NetUid = NetUid::from(1); - let tempo: u16 = 10; - add_network(netuid, tempo, 0); - - let mut current_block: u64 = 0; - for expected_epoch in 0..10u64 { - let expected_first = SubtensorModule::get_first_block_of_epoch(netuid, expected_epoch); - - // Step blocks until we reach the start of this epoch - while current_block < expected_first { - run_to_block(current_block + 1); - current_block += 1; - } - - // Assert we are at the first block of the epoch - assert_eq!(current_block, expected_first); - assert_eq!( - SubtensorModule::get_epoch_index(netuid, current_block), - expected_epoch - ); - - // From here, blocks_until_next_epoch should point to the start of next epoch - let until_next = SubtensorModule::blocks_until_next_epoch(netuid, tempo, current_block); - let next_first = SubtensorModule::get_first_block_of_epoch(netuid, expected_epoch + 1); - assert_eq!(current_block + until_next + 1, next_first); // +1 since until is blocks to end, +1 to start next - - // Advance to near end of this epoch - let last_block = next_first.saturating_sub(1); - run_to_block(last_block); - current_block = System::block_number(); - assert_eq!( - SubtensorModule::get_epoch_index(netuid, current_block), - expected_epoch - ); - - // Until next from near end - let until_next_end = - SubtensorModule::blocks_until_next_epoch(netuid, tempo, current_block); - assert_eq!(current_block + until_next_end + 1, next_first); - } + // 1000 * 101 - 1. + assert_eq!( + SubtensorModule::get_first_block_of_epoch(netuid, epoch), + epoch * 101 - 1 + ); }); } @@ -6671,11 +6464,14 @@ fn test_reveal_crv3_commits_retry_on_missing_pulse() { .map(|(e, _)| e) .expect("commit stored"); - // first block of reveal epoch (commit_epoch + RP) - let first_reveal_epoch = stored_epoch + SubtensorModule::get_reveal_period(netuid); - let first_reveal_block = - SubtensorModule::get_first_block_of_epoch(netuid, first_reveal_epoch); - run_to_block_no_epoch(netuid, first_reveal_block); + // Place the subnet's epoch counter at the commit's reveal epoch + // (`commit_epoch + reveal_period`). The counter is the canonical epoch + // index; pin `LastEpochBlock`/`PendingEpochAt` so `should_run_epoch` stays + // false and the look-ahead does not skip past the reveal epoch. + let reveal_epoch = stored_epoch + SubtensorModule::get_reveal_period(netuid); + SubnetEpochIndex::::insert(netuid, reveal_epoch); + LastEpochBlock::::insert(netuid, SubtensorModule::get_current_block_as_u64()); + PendingEpochAt::::insert(netuid, 0); // run *one* block inside reveal epoch without pulse → commit should stay queued step_block(1); diff --git a/pallets/subtensor/src/utils/misc.rs b/pallets/subtensor/src/utils/misc.rs index a1c0309b24..a2b0fa2627 100644 --- a/pallets/subtensor/src/utils/misc.rs +++ b/pallets/subtensor/src/utils/misc.rs @@ -54,13 +54,18 @@ impl Pallet { /// Returns true if the current block is within the terminal freeze window of the tempo for the /// given subnet. During this window, admin ops are prohibited to avoid interference with - /// validator weight submissions. + /// validator weight submissions. Engages immediately on a pending manual trigger (so the trigger + /// arms the freeze for the entire countdown to `PendingEpochAt`). pub fn is_in_admin_freeze_window(netuid: NetUid, current_block: u64) -> bool { let tempo = Self::get_tempo(netuid); if tempo == 0 { return false; } - let remaining = Self::blocks_until_next_epoch(netuid, tempo, current_block); + let pending = PendingEpochAt::::get(netuid); + if pending > 0 && pending > current_block { + return true; + } + let remaining = Self::blocks_until_next_auto_epoch(netuid, tempo, current_block); let window = AdminFreezeWindow::::get() as u64; remaining < window } @@ -102,10 +107,23 @@ impl Pallet { // ======================== // ==== Global Setters ==== // ======================== - pub fn set_tempo(netuid: NetUid, tempo: u16) { + /// Unchecked tempo write used by tests, precompiles, and internal helpers. + /// Does NOT reset `LastEpochBlock` — that is the responsibility of the owner-side + /// `set_tempo` extrinsic and `sudo_set_tempo` (root), both of which perform the cycle + /// reset explicitly. + pub fn set_tempo_unchecked(netuid: NetUid, tempo: u16) { Tempo::::insert(netuid, tempo); Self::deposit_event(Event::TempoSet(netuid, tempo)); } + + /// Sets `Tempo` and resets the state-based scheduler anchor `LastEpochBlock` + /// to the current block + pub fn apply_tempo_with_cycle_reset(netuid: NetUid, tempo: u16) { + Self::set_tempo_unchecked(netuid, tempo); + let now = Self::get_current_block_as_u64(); + LastEpochBlock::::insert(netuid, now); + } + pub fn set_last_adjustment_block(netuid: NetUid, last_adjustment_block: u64) { LastAdjustmentBlock::::insert(netuid, last_adjustment_block); } @@ -582,6 +600,30 @@ impl Pallet { Self::deposit_event(Event::ActivityCutoffSet(netuid, activity_cutoff)); } + /// Effective activity cutoff in blocks, derived from `ActivityCutoffFactorMilli` and `Tempo`. + /// `cutoff_blocks = (factor × tempo) / 1000`, clamped to ≥ 1. + pub fn get_activity_cutoff_blocks(netuid: NetUid) -> u64 { + let factor_milli = ActivityCutoffFactorMilli::::get(netuid) as u64; + let tempo = Self::get_tempo(netuid) as u64; + factor_milli + .saturating_mul(tempo) + .checked_div(1000) + .unwrap_or(0) + .max(1) + } + + pub fn get_activity_cutoff_factor_milli(netuid: NetUid) -> u32 { + ActivityCutoffFactorMilli::::get(netuid) + } + + pub fn set_activity_cutoff_factor_milli(netuid: NetUid, factor_milli: u32) { + ActivityCutoffFactorMilli::::insert(netuid, factor_milli); + Self::deposit_event(Event::ActivityCutoffFactorMilliSet { + netuid, + factor_milli, + }); + } + // Registration Toggle utils pub fn get_network_registration_allowed(netuid: NetUid) -> bool { NetworkRegistrationAllowed::::get(netuid) diff --git a/pallets/subtensor/src/utils/rate_limiting.rs b/pallets/subtensor/src/utils/rate_limiting.rs index e9559f2c6d..7b93d620ac 100644 --- a/pallets/subtensor/src/utils/rate_limiting.rs +++ b/pallets/subtensor/src/utils/rate_limiting.rs @@ -17,6 +17,7 @@ pub enum TransactionType { MechanismEmission, MaxUidsTrimming, AddStakeBurn, + TempoUpdate, } impl TransactionType { @@ -46,6 +47,7 @@ impl TransactionType { } Self::SetSNOwnerHotkey => DefaultSetSNOwnerHotkeyRateLimit::::get(), Self::AddStakeBurn => Tempo::::get(netuid) as u64, + Self::TempoUpdate => MIN_TEMPO as u64, _ => self.rate_limit::(), } @@ -144,6 +146,7 @@ impl From for u16 { TransactionType::MechanismEmission => 8, TransactionType::MaxUidsTrimming => 9, TransactionType::AddStakeBurn => 10, + TransactionType::TempoUpdate => 11, } } } @@ -162,6 +165,7 @@ impl From for TransactionType { 8 => TransactionType::MechanismEmission, 9 => TransactionType::MaxUidsTrimming, 10 => TransactionType::AddStakeBurn, + 11 => TransactionType::TempoUpdate, _ => TransactionType::Unknown, } } @@ -206,6 +210,8 @@ pub enum Hyperparameter { BurnIncreaseMult = 27, SubnetEmissionEnabled = 28, MinChildkeyTake = 29, + ActivityCutoffFactorMilli = 30, + TriggerEpoch = 31, } impl Pallet { diff --git a/pallets/subtensor/src/weights.rs b/pallets/subtensor/src/weights.rs index 6d536dadaa..e84c260c72 100644 --- a/pallets/subtensor/src/weights.rs +++ b/pallets/subtensor/src/weights.rs @@ -2,7 +2,7 @@ //! Autogenerated weights for `pallet_subtensor` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 49.1.0 -//! DATE: 2026-05-31, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-06-11, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `runnervm3jyl0`, CPU: `AMD EPYC 9V74 80-Core Processor` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` @@ -22,7 +22,7 @@ // --no-storage-info // --no-min-squares // --no-median-slopes -// --output=/tmp/tmp.iVvhDcFf4i +// --output=/tmp/tmp.2HksoV8klE // --template=/home/runner/work/subtensor/subtensor/.maintain/frame-weight-template.hbs #![cfg_attr(rustfmt, rustfmt_skip)] @@ -92,6 +92,10 @@ pub trait WeightInfo { fn set_pending_childkey_cooldown() -> Weight; fn lock_stake() -> Weight; fn move_lock() -> Weight; + fn associate_evm_key() -> Weight; + fn set_tempo() -> Weight; + fn set_activity_cutoff_factor() -> Weight; + fn trigger_epoch() -> Weight; } /// Weights for `pallet_subtensor` using the Substrate node and recommended hardware. @@ -121,28 +125,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaInProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetAlphaInProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:1) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:1) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::Positions` (r:1 w:1) - /// Proof: `Swap::Positions` (`max_values`: None, `max_size`: Some(140), added: 2615, mode: `MaxEncodedLen`) - /// Storage: `Swap::Ticks` (r:2 w:2) - /// Proof: `Swap::Ticks` (`max_values`: None, `max_size`: Some(78), added: 2553, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:5 w:5) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) - /// Storage: `Swap::FeeGlobalTao` (r:1 w:0) - /// Proof: `Swap::FeeGlobalTao` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::FeeGlobalAlpha` (r:1 w:0) - /// Proof: `Swap::FeeGlobalAlpha` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:1) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) - /// Storage: `Swap::LastPositionId` (r:1 w:1) - /// Proof: `Swap::LastPositionId` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) @@ -187,18 +173,16 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::Keys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::IsNetworkMember` (r:0 w:1) /// Proof: `SubtensorModule::IsNetworkMember` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::AlphaSqrtPrice` (r:0 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:0 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) + /// Storage: `Swap::SwapBalancer` (r:0 w:1) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) fn register() -> Weight { // Proof Size summary in bytes: - // Measured: `1716` - // Estimated: `13600` - // Minimum execution time: 374_002_000 picoseconds. - Weight::from_parts(380_312_000, 13600) - .saturating_add(T::DbWeight::get().reads(48_u64)) - .saturating_add(T::DbWeight::get().writes(40_u64)) + // Measured: `1837` + // Estimated: `6148` + // Minimum execution time: 340_604_000 picoseconds. + Weight::from_parts(344_520_000, 6148) + .saturating_add(T::DbWeight::get().reads(34_u64)) + .saturating_add(T::DbWeight::get().writes(29_u64)) } /// Storage: `SubtensorModule::CommitRevealWeightsEnabled` (r:1 w:0) /// Proof: `SubtensorModule::CommitRevealWeightsEnabled` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -238,8 +222,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `188792` // Estimated: `10327382` - // Minimum execution time: 16_598_698_000 picoseconds. - Weight::from_parts(16_897_861_000, 10327382) + // Minimum execution time: 16_142_608_000 picoseconds. + Weight::from_parts(16_400_997_000, 10327382) .saturating_add(T::DbWeight::get().reads(4112_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -251,20 +235,14 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaInProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetAlphaInProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:3 w:0) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:0) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:0) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:3 w:3) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::Owner` (r:1 w:0) @@ -277,8 +255,6 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetVolume` (r:1 w:1) @@ -309,18 +285,18 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::UnlockRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::MaturityRate` (r:1 w:0) /// Proof: `SubtensorModule::MaturityRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:0 w:1) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LockingColdkeys` (r:0 w:1) + /// Proof: `SubtensorModule::LockingColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (r:0 w:1) /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn add_stake() -> Weight { // Proof Size summary in bytes: - // Measured: `2633` + // Measured: `2226` // Estimated: `8727` - // Minimum execution time: 459_249_000 picoseconds. - Weight::from_parts(476_173_000, 8727) - .saturating_add(T::DbWeight::get().reads(38_u64)) - .saturating_add(T::DbWeight::get().writes(18_u64)) + // Minimum execution time: 665_334_000 picoseconds. + Weight::from_parts(685_664_000, 8727) + .saturating_add(T::DbWeight::get().reads(32_u64)) + .saturating_add(T::DbWeight::get().writes(16_u64)) } /// Storage: `SubtensorModule::IsNetworkMember` (r:2 w:0) /// Proof: `SubtensorModule::IsNetworkMember` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -332,8 +308,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `801` // Estimated: `6741` - // Minimum execution time: 32_538_000 picoseconds. - Weight::from_parts(33_289_000, 6741) + // Minimum execution time: 31_927_000 picoseconds. + Weight::from_parts(33_199_000, 6741) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -347,8 +323,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `774` // Estimated: `6714` - // Minimum execution time: 29_163_000 picoseconds. - Weight::from_parts(29_784_000, 6714) + // Minimum execution time: 28_011_000 picoseconds. + Weight::from_parts(29_073_000, 6714) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -376,28 +352,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaInProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetAlphaInProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:1) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:1) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::Positions` (r:1 w:1) - /// Proof: `Swap::Positions` (`max_values`: None, `max_size`: Some(140), added: 2615, mode: `MaxEncodedLen`) - /// Storage: `Swap::Ticks` (r:2 w:2) - /// Proof: `Swap::Ticks` (`max_values`: None, `max_size`: Some(78), added: 2553, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:5 w:5) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) - /// Storage: `Swap::FeeGlobalTao` (r:1 w:0) - /// Proof: `Swap::FeeGlobalTao` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::FeeGlobalAlpha` (r:1 w:0) - /// Proof: `Swap::FeeGlobalAlpha` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:1) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) - /// Storage: `Swap::LastPositionId` (r:1 w:1) - /// Proof: `Swap::LastPositionId` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) @@ -442,18 +400,16 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::Keys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::IsNetworkMember` (r:0 w:1) /// Proof: `SubtensorModule::IsNetworkMember` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::AlphaSqrtPrice` (r:0 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:0 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) + /// Storage: `Swap::SwapBalancer` (r:0 w:1) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) fn burned_register() -> Weight { // Proof Size summary in bytes: - // Measured: `1649` - // Estimated: `13600` - // Minimum execution time: 362_295_000 picoseconds. - Weight::from_parts(368_123_000, 13600) - .saturating_add(T::DbWeight::get().reads(48_u64)) - .saturating_add(T::DbWeight::get().writes(40_u64)) + // Measured: `1770` + // Estimated: `6148` + // Minimum execution time: 337_589_000 picoseconds. + Weight::from_parts(341_856_000, 6148) + .saturating_add(T::DbWeight::get().reads(34_u64)) + .saturating_add(T::DbWeight::get().writes(29_u64)) } /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -501,10 +457,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::IsNetworkMember` (`max_values`: None, `max_size`: None, mode: `Measured`) fn root_register() -> Weight { // Proof Size summary in bytes: - // Measured: `1445` - // Estimated: `4910` - // Minimum execution time: 102_751_000 picoseconds. - Weight::from_parts(104_294_000, 4910) + // Measured: `1482` + // Estimated: `4947` + // Minimum execution time: 100_028_000 picoseconds. + Weight::from_parts(102_953_000, 4947) .saturating_add(T::DbWeight::get().reads(19_u64)) .saturating_add(T::DbWeight::get().writes(16_u64)) } @@ -534,16 +490,12 @@ impl WeightInfo for SubstrateWeight { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:1) /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaInProvided` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetAlphaInProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::TotalNetworks` (r:1 w:1) /// Proof: `SubtensorModule::TotalNetworks` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Kappa` (r:1 w:1) @@ -624,12 +576,12 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::MaxAllowedUids` (`max_values`: None, `max_size`: None, mode: `Measured`) fn register_network() -> Weight { // Proof Size summary in bytes: - // Measured: `1459` - // Estimated: `9874` - // Minimum execution time: 277_470_000 picoseconds. - Weight::from_parts(282_297_000, 9874) - .saturating_add(T::DbWeight::get().reads(42_u64)) - .saturating_add(T::DbWeight::get().writes(49_u64)) + // Measured: `1532` + // Estimated: `9947` + // Minimum execution time: 266_053_000 picoseconds. + Weight::from_parts(272_392_000, 9947) + .saturating_add(T::DbWeight::get().reads(40_u64)) + .saturating_add(T::DbWeight::get().writes(47_u64)) } /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -655,8 +607,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `1071` // Estimated: `4536` - // Minimum execution time: 60_340_000 picoseconds. - Weight::from_parts(61_421_000, 4536) + // Minimum execution time: 57_885_000 picoseconds. + Weight::from_parts(58_817_000, 4536) .saturating_add(T::DbWeight::get().reads(10_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -700,8 +652,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `1589` // Estimated: `7529` - // Minimum execution time: 108_541_000 picoseconds. - Weight::from_parts(110_183_000, 7529) + // Minimum execution time: 105_065_000 picoseconds. + Weight::from_parts(107_229_000, 7529) .saturating_add(T::DbWeight::get().reads(18_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -711,8 +663,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 4_076_000 picoseconds. - Weight::from_parts(4_647_000, 0) + // Minimum execution time: 4_036_000 picoseconds. + Weight::from_parts(4_397_000, 0) .saturating_add(T::DbWeight::get().writes(1_u64)) } /// Storage: `SubtensorModule::Owner` (r:1 w:0) @@ -731,10 +683,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::TransactionKeyLastBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn set_childkey_take() -> Weight { // Proof Size summary in bytes: - // Measured: `999` - // Estimated: `4464` - // Minimum execution time: 52_278_000 picoseconds. - Weight::from_parts(53_209_000, 4464) + // Measured: `1033` + // Estimated: `4498` + // Minimum execution time: 51_045_000 picoseconds. + Weight::from_parts(51_967_000, 4498) .saturating_add(T::DbWeight::get().reads(7_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -750,8 +702,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `694` // Estimated: `4159` - // Minimum execution time: 43_995_000 picoseconds. - Weight::from_parts(45_167_000, 4159) + // Minimum execution time: 43_204_000 picoseconds. + Weight::from_parts(45_056_000, 4159) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -787,17 +739,19 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::MaturityRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Lock` (r:2 w:0) /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::DecayingLock` (r:1 w:0) + /// Proof: `SubtensorModule::DecayingLock` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `System::Account` (r:2 w:2) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::LastRateLimitedBlock` (r:0 w:1) /// Proof: `SubtensorModule::LastRateLimitedBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn swap_coldkey_announced() -> Weight { // Proof Size summary in bytes: - // Measured: `2175` - // Estimated: `13065` - // Minimum execution time: 286_653_000 picoseconds. - Weight::from_parts(294_536_000, 13065) - .saturating_add(T::DbWeight::get().reads(35_u64)) + // Measured: `2110` + // Estimated: `13000` + // Minimum execution time: 285_883_000 picoseconds. + Weight::from_parts(289_268_000, 13000) + .saturating_add(T::DbWeight::get().reads(36_u64)) .saturating_add(T::DbWeight::get().writes(15_u64)) } /// Storage: `System::Account` (r:2 w:2) @@ -834,6 +788,8 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::MaturityRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Lock` (r:2 w:0) /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::DecayingLock` (r:1 w:0) + /// Proof: `SubtensorModule::DecayingLock` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::ColdkeySwapAnnouncements` (r:0 w:1) /// Proof: `SubtensorModule::ColdkeySwapAnnouncements` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::ColdkeySwapDisputes` (r:0 w:1) @@ -842,11 +798,11 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::LastRateLimitedBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn swap_coldkey() -> Weight { // Proof Size summary in bytes: - // Measured: `2231` - // Estimated: `13121` - // Minimum execution time: 310_339_000 picoseconds. - Weight::from_parts(313_503_000, 13121) - .saturating_add(T::DbWeight::get().reads(35_u64)) + // Measured: `2166` + // Estimated: `13056` + // Minimum execution time: 308_517_000 picoseconds. + Weight::from_parts(314_044_000, 13056) + .saturating_add(T::DbWeight::get().reads(36_u64)) .saturating_add(T::DbWeight::get().writes(19_u64)) } /// Storage: `SubtensorModule::ColdkeySwapAnnouncements` (r:1 w:0) @@ -857,8 +813,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `665` // Estimated: `4130` - // Minimum execution time: 20_290_000 picoseconds. - Weight::from_parts(21_452_000, 4130) + // Minimum execution time: 20_170_000 picoseconds. + Weight::from_parts(20_820_000, 4130) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -870,8 +826,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `613` // Estimated: `4078` - // Minimum execution time: 16_995_000 picoseconds. - Weight::from_parts(17_505_000, 4078) + // Minimum execution time: 16_435_000 picoseconds. + Weight::from_parts(17_065_000, 4078) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -883,8 +839,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 6_830_000 picoseconds. - Weight::from_parts(7_271_000, 0) + // Minimum execution time: 6_750_000 picoseconds. + Weight::from_parts(7_190_000, 0) .saturating_add(T::DbWeight::get().writes(2_u64)) } /// Storage: `SubtensorModule::CommitRevealWeightsEnabled` (r:1 w:0) @@ -927,8 +883,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `2094` // Estimated: `8034` - // Minimum execution time: 429_484_000 picoseconds. - Weight::from_parts(443_415_000, 8034) + // Minimum execution time: 414_593_000 picoseconds. + Weight::from_parts(422_245_000, 8034) .saturating_add(T::DbWeight::get().reads(18_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -960,10 +916,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `AlphaAssets::TotalAlphaIssuance` (`max_values`: None, `max_size`: None, mode: `Measured`) fn recycle_alpha() -> Weight { // Proof Size summary in bytes: - // Measured: `1873` - // Estimated: `5338` - // Minimum execution time: 176_220_000 picoseconds. - Weight::from_parts(178_253_000, 5338) + // Measured: `1754` + // Estimated: `5219` + // Minimum execution time: 173_126_000 picoseconds. + Weight::from_parts(174_428_000, 5219) .saturating_add(T::DbWeight::get().reads(13_u64)) .saturating_add(T::DbWeight::get().writes(6_u64)) } @@ -993,10 +949,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `AlphaAssets::AlphaBurned` (`max_values`: None, `max_size`: None, mode: `Measured`) fn burn_alpha() -> Weight { // Proof Size summary in bytes: - // Measured: `1873` - // Estimated: `5338` - // Minimum execution time: 172_335_000 picoseconds. - Weight::from_parts(174_197_000, 5338) + // Measured: `1754` + // Estimated: `5219` + // Minimum execution time: 167_228_000 picoseconds. + Weight::from_parts(169_080_000, 5219) .saturating_add(T::DbWeight::get().reads(12_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -1016,8 +972,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `1118` // Estimated: `4583` - // Minimum execution time: 37_836_000 picoseconds. - Weight::from_parts(38_907_000, 4583) + // Minimum execution time: 37_305_000 picoseconds. + Weight::from_parts(38_226_000, 4583) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -1025,20 +981,14 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaInProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetAlphaInProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:3 w:0) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:0) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:0) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubtokenEnabled` (r:1 w:0) @@ -1055,8 +1005,6 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetVolume` (r:1 w:1) @@ -1087,18 +1035,18 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::UnlockRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::MaturityRate` (r:1 w:0) /// Proof: `SubtensorModule::MaturityRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:0 w:1) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LockingColdkeys` (r:0 w:1) + /// Proof: `SubtensorModule::LockingColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (r:0 w:1) /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn add_stake_limit() -> Weight { // Proof Size summary in bytes: - // Measured: `2633` + // Measured: `2226` // Estimated: `8727` - // Minimum execution time: 499_258_000 picoseconds. - Weight::from_parts(516_242_000, 8727) - .saturating_add(T::DbWeight::get().reads(38_u64)) - .saturating_add(T::DbWeight::get().writes(18_u64)) + // Minimum execution time: 862_916_000 picoseconds. + Weight::from_parts(876_506_000, 8727) + .saturating_add(T::DbWeight::get().reads(32_u64)) + .saturating_add(T::DbWeight::get().writes(16_u64)) } /// Storage: `SubtensorModule::Alpha` (r:2 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -1110,8 +1058,6 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:2 w:2) /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:1 w:0) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubtokenEnabled` (r:1 w:0) @@ -1122,18 +1068,20 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:0) /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:0) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (r:0 w:1) /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn move_stake() -> Weight { // Proof Size summary in bytes: - // Measured: `2060` - // Estimated: `8000` - // Minimum execution time: 222_999_000 picoseconds. - Weight::from_parts(227_526_000, 8000) + // Measured: `1979` + // Estimated: `7919` + // Minimum execution time: 218_613_000 picoseconds. + Weight::from_parts(219_955_000, 7919) .saturating_add(T::DbWeight::get().reads(19_u64)) .saturating_add(T::DbWeight::get().writes(7_u64)) } @@ -1151,34 +1099,24 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::NetworksAdded` (r:3 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:1 w:0) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetMechanism` (r:2 w:0) /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:3 w:0) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:0) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:0) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::Owner` (r:1 w:0) /// Proof: `SubtensorModule::Owner` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::StakingHotkeys` (r:1 w:0) /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Lock` (r:1 w:0) /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) @@ -1197,35 +1135,27 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn remove_stake() -> Weight { // Proof Size summary in bytes: - // Measured: `2564` - // Estimated: `10979` - // Minimum execution time: 435_183_000 picoseconds. - Weight::from_parts(444_777_000, 10979) - .saturating_add(T::DbWeight::get().reads(35_u64)) - .saturating_add(T::DbWeight::get().writes(15_u64)) + // Measured: `2142` + // Estimated: `10557` + // Minimum execution time: 559_437_000 picoseconds. + Weight::from_parts(573_518_000, 10557) + .saturating_add(T::DbWeight::get().reads(28_u64)) + .saturating_add(T::DbWeight::get().writes(13_u64)) } /// Storage: `SubtensorModule::SubnetMechanism` (r:2 w:0) /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:3 w:0) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:0) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:0) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::NetworksAdded` (r:3 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:1 w:0) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Alpha` (r:1 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::AlphaV2` (r:1 w:1) @@ -1242,8 +1172,6 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Lock` (r:1 w:0) /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) @@ -1262,12 +1190,12 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn remove_stake_limit() -> Weight { // Proof Size summary in bytes: - // Measured: `2598` - // Estimated: `11013` - // Minimum execution time: 475_352_000 picoseconds. - Weight::from_parts(478_116_000, 11013) - .saturating_add(T::DbWeight::get().reads(34_u64)) - .saturating_add(T::DbWeight::get().writes(15_u64)) + // Measured: `2176` + // Estimated: `10591` + // Minimum execution time: 739_182_000 picoseconds. + Weight::from_parts(755_287_000, 10591) + .saturating_add(T::DbWeight::get().reads(27_u64)) + .saturating_add(T::DbWeight::get().writes(13_u64)) } /// Storage: `SubtensorModule::Alpha` (r:2 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -1283,22 +1211,14 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetTAO` (r:2 w:2) /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:3 w:0) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:0) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:2 w:2) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:0) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:1 w:1) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::NetworksAdded` (r:2 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubtokenEnabled` (r:2 w:0) @@ -1309,8 +1229,6 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Lock` (r:3 w:1) /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaIn` (r:2 w:2) - /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:2 w:2) /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) @@ -1339,16 +1257,18 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::UnlockRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::MaturityRate` (r:1 w:0) /// Proof: `SubtensorModule::MaturityRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LockingColdkeys` (r:0 w:1) + /// Proof: `SubtensorModule::LockingColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (r:0 w:1) /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn swap_stake_limit() -> Weight { // Proof Size summary in bytes: - // Measured: `3108` - // Estimated: `11523` - // Minimum execution time: 688_567_000 picoseconds. - Weight::from_parts(707_234_000, 11523) - .saturating_add(T::DbWeight::get().reads(54_u64)) - .saturating_add(T::DbWeight::get().writes(26_u64)) + // Measured: `2662` + // Estimated: `11077` + // Minimum execution time: 949_273_000 picoseconds. + Weight::from_parts(964_857_000, 11077) + .saturating_add(T::DbWeight::get().reads(47_u64)) + .saturating_add(T::DbWeight::get().writes(24_u64)) } /// Storage: `SubtensorModule::Alpha` (r:2 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -1360,8 +1280,6 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:1 w:1) /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:1 w:0) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubtokenEnabled` (r:1 w:0) @@ -1376,18 +1294,20 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:0) /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:0) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (r:0 w:1) /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn transfer_stake() -> Weight { // Proof Size summary in bytes: - // Measured: `2054` - // Estimated: `7994` - // Minimum execution time: 254_636_000 picoseconds. - Weight::from_parts(258_541_000, 7994) + // Measured: `1988` + // Estimated: `7928` + // Minimum execution time: 250_861_000 picoseconds. + Weight::from_parts(253_195_000, 7928) .saturating_add(T::DbWeight::get().reads(18_u64)) .saturating_add(T::DbWeight::get().writes(6_u64)) } @@ -1401,8 +1321,6 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:2 w:2) /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:1 w:1) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::NetworksAdded` (r:2 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubtokenEnabled` (r:2 w:0) @@ -1413,26 +1331,18 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetTAO` (r:2 w:2) /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:3 w:0) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:0) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:2 w:2) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:0) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::StakingHotkeys` (r:1 w:0) /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Lock` (r:3 w:1) /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaIn` (r:2 w:2) - /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:2 w:2) /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) @@ -1461,16 +1371,18 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::UnlockRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::MaturityRate` (r:1 w:0) /// Proof: `SubtensorModule::MaturityRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LockingColdkeys` (r:0 w:1) + /// Proof: `SubtensorModule::LockingColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (r:0 w:1) /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn swap_stake() -> Weight { // Proof Size summary in bytes: - // Measured: `2951` - // Estimated: `11366` - // Minimum execution time: 633_996_000 picoseconds. - Weight::from_parts(655_699_000, 11366) - .saturating_add(T::DbWeight::get().reads(54_u64)) - .saturating_add(T::DbWeight::get().writes(26_u64)) + // Measured: `2505` + // Estimated: `10920` + // Minimum execution time: 728_698_000 picoseconds. + Weight::from_parts(746_774_000, 10920) + .saturating_add(T::DbWeight::get().reads(47_u64)) + .saturating_add(T::DbWeight::get().writes(24_u64)) } /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -1498,8 +1410,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `1122` // Estimated: `4587` - // Minimum execution time: 127_058_000 picoseconds. - Weight::from_parts(129_030_000, 4587) + // Minimum execution time: 123_562_000 picoseconds. + Weight::from_parts(125_566_000, 4587) .saturating_add(T::DbWeight::get().reads(11_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -1539,8 +1451,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `1426` // Estimated: `7366` - // Minimum execution time: 101_319_000 picoseconds. - Weight::from_parts(102_992_000, 7366) + // Minimum execution time: 97_624_000 picoseconds. + Weight::from_parts(100_468_000, 7366) .saturating_add(T::DbWeight::get().reads(16_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -1554,10 +1466,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::LastRateLimitedBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn decrease_take() -> Weight { // Proof Size summary in bytes: - // Measured: `793` - // Estimated: `4258` - // Minimum execution time: 25_969_000 picoseconds. - Weight::from_parts(27_160_000, 4258) + // Measured: `830` + // Estimated: `4295` + // Minimum execution time: 26_830_000 picoseconds. + Weight::from_parts(27_561_000, 4295) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -1573,10 +1485,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::TxDelegateTakeRateLimit` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) fn increase_take() -> Weight { // Proof Size summary in bytes: - // Measured: `886` - // Estimated: `4351` - // Minimum execution time: 33_360_000 picoseconds. - Weight::from_parts(34_381_000, 4351) + // Measured: `923` + // Estimated: `4388` + // Minimum execution time: 33_029_000 picoseconds. + Weight::from_parts(34_211_000, 4388) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -1604,16 +1516,12 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::BlockEmission` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:1) /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaInProvided` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetAlphaInProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::TotalNetworks` (r:1 w:1) /// Proof: `SubtensorModule::TotalNetworks` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Kappa` (r:1 w:1) @@ -1696,12 +1604,12 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::MaxAllowedUids` (`max_values`: None, `max_size`: None, mode: `Measured`) fn register_network_with_identity() -> Weight { // Proof Size summary in bytes: - // Measured: `1343` - // Estimated: `9758` - // Minimum execution time: 271_161_000 picoseconds. - Weight::from_parts(278_281_000, 9758) - .saturating_add(T::DbWeight::get().reads(41_u64)) - .saturating_add(T::DbWeight::get().writes(48_u64)) + // Measured: `1468` + // Estimated: `9883` + // Minimum execution time: 266_604_000 picoseconds. + Weight::from_parts(276_799_000, 9883) + .saturating_add(T::DbWeight::get().reads(39_u64)) + .saturating_add(T::DbWeight::get().writes(46_u64)) } /// Storage: `SubtensorModule::IsNetworkMember` (r:2 w:0) /// Proof: `SubtensorModule::IsNetworkMember` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -1713,8 +1621,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `772` // Estimated: `6712` - // Minimum execution time: 31_877_000 picoseconds. - Weight::from_parts(32_949_000, 6712) + // Minimum execution time: 31_506_000 picoseconds. + Weight::from_parts(32_528_000, 6712) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -1726,10 +1634,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::IdentitiesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) fn set_identity() -> Weight { // Proof Size summary in bytes: - // Measured: `852` - // Estimated: `6792` - // Minimum execution time: 28_833_000 picoseconds. - Weight::from_parts(29_874_000, 6792) + // Measured: `889` + // Estimated: `6829` + // Minimum execution time: 29_043_000 picoseconds. + Weight::from_parts(30_816_000, 6829) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -1741,8 +1649,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `595` // Estimated: `4060` - // Minimum execution time: 15_502_000 picoseconds. - Weight::from_parts(16_184_000, 4060) + // Minimum execution time: 15_503_000 picoseconds. + Weight::from_parts(16_204_000, 4060) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -1816,10 +1724,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::Keys` (`max_values`: None, `max_size`: None, mode: `Measured`) fn swap_hotkey() -> Weight { // Proof Size summary in bytes: - // Measured: `3026` - // Estimated: `28766` - // Minimum execution time: 1_201_334_000 picoseconds. - Weight::from_parts(1_208_365_000, 28766) + // Measured: `3131` + // Estimated: `28871` + // Minimum execution time: 1_193_554_000 picoseconds. + Weight::from_parts(1_201_736_000, 28871) .saturating_add(T::DbWeight::get().reads(171_u64)) .saturating_add(T::DbWeight::get().writes(95_u64)) } @@ -1831,10 +1739,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) fn try_associate_hotkey() -> Weight { // Proof Size summary in bytes: - // Measured: `745` - // Estimated: `4210` - // Minimum execution time: 22_373_000 picoseconds. - Weight::from_parts(23_134_000, 4210) + // Measured: `818` + // Estimated: `4283` + // Minimum execution time: 23_084_000 picoseconds. + Weight::from_parts(23_836_000, 4283) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -1846,10 +1754,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::SubtokenEnabled` (`max_values`: None, `max_size`: None, mode: `Measured`) fn unstake_all() -> Weight { // Proof Size summary in bytes: - // Measured: `740` - // Estimated: `9155` - // Minimum execution time: 25_017_000 picoseconds. - Weight::from_parts(25_658_000, 9155) + // Measured: `774` + // Estimated: `9189` + // Minimum execution time: 24_587_000 picoseconds. + Weight::from_parts(25_608_000, 9189) .saturating_add(T::DbWeight::get().reads(6_u64)) } /// Storage: `SubtensorModule::Owner` (r:1 w:0) @@ -1868,32 +1776,22 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:2 w:2) /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:1 w:0) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetMechanism` (r:2 w:0) /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetTAO` (r:2 w:2) /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:3 w:0) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:0) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:2 w:2) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:0) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::StakingHotkeys` (r:1 w:0) /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Lock` (r:2 w:0) /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaIn` (r:2 w:2) - /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:2 w:2) /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) @@ -1918,12 +1816,12 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn unstake_all_alpha() -> Weight { // Proof Size summary in bytes: - // Measured: `2642` + // Measured: `2235` // Estimated: `11306` - // Minimum execution time: 583_803_000 picoseconds. - Weight::from_parts(599_485_000, 11306) - .saturating_add(T::DbWeight::get().reads(50_u64)) - .saturating_add(T::DbWeight::get().writes(27_u64)) + // Minimum execution time: 695_268_000 picoseconds. + Weight::from_parts(708_618_000, 11306) + .saturating_add(T::DbWeight::get().reads(43_u64)) + .saturating_add(T::DbWeight::get().writes(25_u64)) } /// Storage: `SubtensorModule::Alpha` (r:1 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -1939,32 +1837,22 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:3 w:0) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:0) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:0) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::NetworksAdded` (r:3 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:1 w:0) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Owner` (r:1 w:0) /// Proof: `SubtensorModule::Owner` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::StakingHotkeys` (r:1 w:0) /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Lock` (r:1 w:0) /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) @@ -1983,12 +1871,12 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn remove_stake_full_limit() -> Weight { // Proof Size summary in bytes: - // Measured: `2598` - // Estimated: `11013` - // Minimum execution time: 497_956_000 picoseconds. - Weight::from_parts(503_033_000, 11013) - .saturating_add(T::DbWeight::get().reads(34_u64)) - .saturating_add(T::DbWeight::get().writes(15_u64)) + // Measured: `2176` + // Estimated: `10591` + // Minimum execution time: 763_548_000 picoseconds. + Weight::from_parts(778_691_000, 10591) + .saturating_add(T::DbWeight::get().reads(27_u64)) + .saturating_add(T::DbWeight::get().writes(13_u64)) } /// Storage: `Crowdloan::CurrentCrowdloanId` (r:1 w:0) /// Proof: `Crowdloan::CurrentCrowdloanId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -2022,16 +1910,12 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::BlockEmission` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:1) /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaInProvided` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetAlphaInProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::TotalNetworks` (r:1 w:1) /// Proof: `SubtensorModule::TotalNetworks` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Kappa` (r:1 w:1) @@ -2125,15 +2009,15 @@ impl WeightInfo for SubstrateWeight { /// The range of component `k` is `[2, 500]`. fn register_leased_network(k: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `1762 + k * (44 ±0)` - // Estimated: `10183 + k * (2579 ±0)` - // Minimum execution time: 488_121_000 picoseconds. - Weight::from_parts(306_068_429, 10183) - // Standard Error: 24_263 - .saturating_add(Weight::from_parts(48_393_644, 0).saturating_mul(k.into())) - .saturating_add(T::DbWeight::get().reads(51_u64)) + // Measured: `1835 + k * (44 ±0)` + // Estimated: `10256 + k * (2579 ±0)` + // Minimum execution time: 480_320_000 picoseconds. + Weight::from_parts(250_987_824, 10256) + // Standard Error: 56_710 + .saturating_add(Weight::from_parts(48_825_380, 0).saturating_mul(k.into())) + .saturating_add(T::DbWeight::get().reads(49_u64)) .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(k.into()))) - .saturating_add(T::DbWeight::get().writes(54_u64)) + .saturating_add(T::DbWeight::get().writes(52_u64)) .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(k.into()))) .saturating_add(Weight::from_parts(0, 2579).saturating_mul(k.into())) } @@ -2158,17 +2042,17 @@ impl WeightInfo for SubstrateWeight { /// The range of component `k` is `[2, 500]`. fn terminate_lease(k: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `1468 + k * (53 ±0)` - // Estimated: `6148 + k * (2514 ±0)` - // Minimum execution time: 91_385_000 picoseconds. - Weight::from_parts(96_646_996, 6148) - // Standard Error: 5_309 - .saturating_add(Weight::from_parts(1_570_386, 0).saturating_mul(k.into())) + // Measured: `1501 + k * (53 ±0)` + // Estimated: `6148 + k * (2529 ±0)` + // Minimum execution time: 89_272_000 picoseconds. + Weight::from_parts(79_136_631, 6148) + // Standard Error: 7_622 + .saturating_add(Weight::from_parts(1_641_271, 0).saturating_mul(k.into())) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(k.into()))) .saturating_add(T::DbWeight::get().writes(7_u64)) .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(k.into()))) - .saturating_add(Weight::from_parts(0, 2514).saturating_mul(k.into())) + .saturating_add(Weight::from_parts(0, 2529).saturating_mul(k.into())) } /// Storage: `SubtensorModule::SubnetOwner` (r:1 w:0) /// Proof: `SubtensorModule::SubnetOwner` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -2178,8 +2062,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `659` // Estimated: `9074` - // Minimum execution time: 24_486_000 picoseconds. - Weight::from_parts(25_798_000, 9074) + // Minimum execution time: 23_875_000 picoseconds. + Weight::from_parts(25_027_000, 9074) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -2207,8 +2091,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `1070` // Estimated: `4535` - // Minimum execution time: 72_186_000 picoseconds. - Weight::from_parts(73_359_000, 4535) + // Minimum execution time: 71_556_000 picoseconds. + Weight::from_parts(73_198_000, 4535) .saturating_add(T::DbWeight::get().reads(10_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -2224,8 +2108,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `809` // Estimated: `4274` - // Minimum execution time: 31_566_000 picoseconds. - Weight::from_parts(32_979_000, 4274) + // Minimum execution time: 31_547_000 picoseconds. + Weight::from_parts(32_238_000, 4274) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -2241,8 +2125,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `476` // Estimated: `3941` - // Minimum execution time: 15_613_000 picoseconds. - Weight::from_parts(16_064_000, 3941) + // Minimum execution time: 15_273_000 picoseconds. + Weight::from_parts(16_074_000, 3941) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -2270,10 +2154,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::RootClaimableThreshold` (`max_values`: None, `max_size`: None, mode: `Measured`) fn claim_root() -> Weight { // Proof Size summary in bytes: - // Measured: `1929` - // Estimated: `7869` - // Minimum execution time: 140_608_000 picoseconds. - Weight::from_parts(142_310_000, 7869) + // Measured: `1935` + // Estimated: `7875` + // Minimum execution time: 139_456_000 picoseconds. + Weight::from_parts(141_309_000, 7875) .saturating_add(T::DbWeight::get().reads(16_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -2283,8 +2167,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 1_883_000 picoseconds. - Weight::from_parts(2_083_000, 0) + // Minimum execution time: 2_023_000 picoseconds. + Weight::from_parts(2_303_000, 0) .saturating_add(T::DbWeight::get().writes(1_u64)) } /// Storage: `SubtensorModule::RootClaimableThreshold` (r:0 w:1) @@ -2293,8 +2177,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 4_136_000 picoseconds. - Weight::from_parts(4_747_000, 0) + // Minimum execution time: 4_567_000 picoseconds. + Weight::from_parts(5_117_000, 0) .saturating_add(T::DbWeight::get().writes(1_u64)) } /// Storage: `SubtensorModule::Owner` (r:1 w:0) @@ -2305,10 +2189,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::AutoParentDelegationEnabled` (`max_values`: None, `max_size`: None, mode: `Measured`) fn set_auto_parent_delegation_enabled() -> Weight { // Proof Size summary in bytes: - // Measured: `862` - // Estimated: `4327` - // Minimum execution time: 23_675_000 picoseconds. - Weight::from_parts(25_277_000, 4327) + // Measured: `899` + // Estimated: `4364` + // Minimum execution time: 24_225_000 picoseconds. + Weight::from_parts(25_578_000, 4364) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -2316,20 +2200,14 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaInProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetAlphaInProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:3 w:0) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:0) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:0) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubtokenEnabled` (r:1 w:0) @@ -2346,8 +2224,6 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetVolume` (r:1 w:1) @@ -2380,18 +2256,18 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::MaturityRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `AlphaAssets::AlphaBurned` (r:1 w:1) /// Proof: `AlphaAssets::AlphaBurned` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:0 w:1) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LockingColdkeys` (r:0 w:1) + /// Proof: `SubtensorModule::LockingColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (r:0 w:1) /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn add_stake_burn() -> Weight { // Proof Size summary in bytes: - // Measured: `2636` + // Measured: `2229` // Estimated: `8727` - // Minimum execution time: 630_852_000 picoseconds. - Weight::from_parts(646_565_000, 8727) - .saturating_add(T::DbWeight::get().reads(39_u64)) - .saturating_add(T::DbWeight::get().writes(19_u64)) + // Minimum execution time: 990_125_000 picoseconds. + Weight::from_parts(995_442_000, 8727) + .saturating_add(T::DbWeight::get().reads(33_u64)) + .saturating_add(T::DbWeight::get().writes(17_u64)) } /// Storage: `SubtensorModule::PendingChildKeyCooldown` (r:0 w:1) /// Proof: `SubtensorModule::PendingChildKeyCooldown` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) @@ -2399,8 +2275,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 1_963_000 picoseconds. - Weight::from_parts(2_083_000, 0) + // Minimum execution time: 2_013_000 picoseconds. + Weight::from_parts(2_173_000, 0) .saturating_add(T::DbWeight::get().writes(1_u64)) } /// Storage: `SubtensorModule::Owner` (r:1 w:0) @@ -2435,14 +2311,16 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::UnlockRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::MaturityRate` (r:1 w:0) /// Proof: `SubtensorModule::MaturityRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LockingColdkeys` (r:0 w:1) + /// Proof: `SubtensorModule::LockingColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) fn lock_stake() -> Weight { // Proof Size summary in bytes: - // Measured: `1644` - // Estimated: `7584` - // Minimum execution time: 111_775_000 picoseconds. - Weight::from_parts(114_028_000, 7584) + // Measured: `1715` + // Estimated: `7655` + // Minimum execution time: 114_670_000 picoseconds. + Weight::from_parts(116_542_000, 7655) .saturating_add(T::DbWeight::get().reads(17_u64)) - .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) } /// Storage: `SubtensorModule::Owner` (r:2 w:0) /// Proof: `SubtensorModule::Owner` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -2464,14 +2342,102 @@ impl WeightInfo for SubstrateWeight { /// Proof: `SubtensorModule::UnlockRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::MaturityRate` (r:1 w:0) /// Proof: `SubtensorModule::MaturityRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LockingColdkeys` (r:0 w:2) + /// Proof: `SubtensorModule::LockingColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) fn move_lock() -> Weight { // Proof Size summary in bytes: - // Measured: `1366` - // Estimated: `7306` - // Minimum execution time: 146_897_000 picoseconds. - Weight::from_parts(148_699_000, 7306) + // Measured: `1399` + // Estimated: `7339` + // Minimum execution time: 154_609_000 picoseconds. + Weight::from_parts(156_442_000, 7339) .saturating_add(T::DbWeight::get().reads(14_u64)) - .saturating_add(T::DbWeight::get().writes(4_u64)) + .saturating_add(T::DbWeight::get().writes(6_u64)) + } + /// Storage: `SubtensorModule::Owner` (r:1 w:0) + /// Proof: `SubtensorModule::Owner` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Uids` (r:1 w:0) + /// Proof: `SubtensorModule::Uids` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::AssociatedEvmAddress` (r:1 w:1) + /// Proof: `SubtensorModule::AssociatedEvmAddress` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn associate_evm_key() -> Weight { + // Proof Size summary in bytes: + // Measured: `950` + // Estimated: `4415` + // Minimum execution time: 697_391_000 picoseconds. + Weight::from_parts(712_594_000, 4415) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `SubtensorModule::SubnetOwner` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetOwner` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::CommitRevealWeightsEnabled` (r:1 w:0) + /// Proof: `SubtensorModule::CommitRevealWeightsEnabled` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Tempo` (r:1 w:1) + /// Proof: `SubtensorModule::Tempo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::PendingEpochAt` (r:1 w:0) + /// Proof: `SubtensorModule::PendingEpochAt` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LastEpochBlock` (r:1 w:1) + /// Proof: `SubtensorModule::LastEpochBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::AdminFreezeWindow` (r:1 w:0) + /// Proof: `SubtensorModule::AdminFreezeWindow` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TransactionKeyLastBlock` (r:1 w:1) + /// Proof: `SubtensorModule::TransactionKeyLastBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn set_tempo() -> Weight { + // Proof Size summary in bytes: + // Measured: `1015` + // Estimated: `4480` + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(35_000_000, 4480) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: `SubtensorModule::SubnetOwner` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetOwner` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Tempo` (r:1 w:0) + /// Proof: `SubtensorModule::Tempo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::PendingEpochAt` (r:1 w:0) + /// Proof: `SubtensorModule::PendingEpochAt` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LastEpochBlock` (r:1 w:0) + /// Proof: `SubtensorModule::LastEpochBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::AdminFreezeWindow` (r:1 w:0) + /// Proof: `SubtensorModule::AdminFreezeWindow` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::OwnerHyperparamRateLimit` (r:1 w:0) + /// Proof: `SubtensorModule::OwnerHyperparamRateLimit` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LastRateLimitedBlock` (r:1 w:1) + /// Proof: `SubtensorModule::LastRateLimitedBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ActivityCutoffFactorMilli` (r:0 w:1) + /// Proof: `SubtensorModule::ActivityCutoffFactorMilli` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn set_activity_cutoff_factor() -> Weight { + // Proof Size summary in bytes: + // Measured: `889` + // Estimated: `4354` + // Minimum execution time: 29_000_000 picoseconds. + Weight::from_parts(31_000_000, 4354) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `SubtensorModule::SubnetOwner` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetOwner` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::CommitRevealWeightsEnabled` (r:1 w:0) + /// Proof: `SubtensorModule::CommitRevealWeightsEnabled` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::PendingEpochAt` (r:1 w:1) + /// Proof: `SubtensorModule::PendingEpochAt` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::OwnerHyperparamRateLimit` (r:1 w:0) + /// Proof: `SubtensorModule::OwnerHyperparamRateLimit` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Tempo` (r:1 w:0) + /// Proof: `SubtensorModule::Tempo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LastRateLimitedBlock` (r:1 w:1) + /// Proof: `SubtensorModule::LastRateLimitedBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::AdminFreezeWindow` (r:1 w:0) + /// Proof: `SubtensorModule::AdminFreezeWindow` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn trigger_epoch() -> Weight { + // Proof Size summary in bytes: + // Measured: `853` + // Estimated: `4318` + // Minimum execution time: 25_000_000 picoseconds. + Weight::from_parts(28_000_000, 4318) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) } } @@ -2501,28 +2467,10 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaInProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetAlphaInProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:1) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:1) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::Positions` (r:1 w:1) - /// Proof: `Swap::Positions` (`max_values`: None, `max_size`: Some(140), added: 2615, mode: `MaxEncodedLen`) - /// Storage: `Swap::Ticks` (r:2 w:2) - /// Proof: `Swap::Ticks` (`max_values`: None, `max_size`: Some(78), added: 2553, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:5 w:5) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) - /// Storage: `Swap::FeeGlobalTao` (r:1 w:0) - /// Proof: `Swap::FeeGlobalTao` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::FeeGlobalAlpha` (r:1 w:0) - /// Proof: `Swap::FeeGlobalAlpha` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:1) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) - /// Storage: `Swap::LastPositionId` (r:1 w:1) - /// Proof: `Swap::LastPositionId` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) @@ -2567,18 +2515,16 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::Keys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::IsNetworkMember` (r:0 w:1) /// Proof: `SubtensorModule::IsNetworkMember` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::AlphaSqrtPrice` (r:0 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:0 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) + /// Storage: `Swap::SwapBalancer` (r:0 w:1) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) fn register() -> Weight { // Proof Size summary in bytes: - // Measured: `1716` - // Estimated: `13600` - // Minimum execution time: 374_002_000 picoseconds. - Weight::from_parts(380_312_000, 13600) - .saturating_add(RocksDbWeight::get().reads(48_u64)) - .saturating_add(RocksDbWeight::get().writes(40_u64)) + // Measured: `1837` + // Estimated: `6148` + // Minimum execution time: 340_604_000 picoseconds. + Weight::from_parts(344_520_000, 6148) + .saturating_add(RocksDbWeight::get().reads(34_u64)) + .saturating_add(RocksDbWeight::get().writes(29_u64)) } /// Storage: `SubtensorModule::CommitRevealWeightsEnabled` (r:1 w:0) /// Proof: `SubtensorModule::CommitRevealWeightsEnabled` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -2618,8 +2564,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `188792` // Estimated: `10327382` - // Minimum execution time: 16_598_698_000 picoseconds. - Weight::from_parts(16_897_861_000, 10327382) + // Minimum execution time: 16_142_608_000 picoseconds. + Weight::from_parts(16_400_997_000, 10327382) .saturating_add(RocksDbWeight::get().reads(4112_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -2631,20 +2577,14 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaInProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetAlphaInProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:3 w:0) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:0) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:0) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:3 w:3) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::Owner` (r:1 w:0) @@ -2657,8 +2597,6 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetVolume` (r:1 w:1) @@ -2689,18 +2627,18 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::UnlockRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::MaturityRate` (r:1 w:0) /// Proof: `SubtensorModule::MaturityRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:0 w:1) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LockingColdkeys` (r:0 w:1) + /// Proof: `SubtensorModule::LockingColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (r:0 w:1) /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn add_stake() -> Weight { // Proof Size summary in bytes: - // Measured: `2633` + // Measured: `2226` // Estimated: `8727` - // Minimum execution time: 459_249_000 picoseconds. - Weight::from_parts(476_173_000, 8727) - .saturating_add(RocksDbWeight::get().reads(38_u64)) - .saturating_add(RocksDbWeight::get().writes(18_u64)) + // Minimum execution time: 665_334_000 picoseconds. + Weight::from_parts(685_664_000, 8727) + .saturating_add(RocksDbWeight::get().reads(32_u64)) + .saturating_add(RocksDbWeight::get().writes(16_u64)) } /// Storage: `SubtensorModule::IsNetworkMember` (r:2 w:0) /// Proof: `SubtensorModule::IsNetworkMember` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -2712,8 +2650,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `801` // Estimated: `6741` - // Minimum execution time: 32_538_000 picoseconds. - Weight::from_parts(33_289_000, 6741) + // Minimum execution time: 31_927_000 picoseconds. + Weight::from_parts(33_199_000, 6741) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -2727,8 +2665,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `774` // Estimated: `6714` - // Minimum execution time: 29_163_000 picoseconds. - Weight::from_parts(29_784_000, 6714) + // Minimum execution time: 28_011_000 picoseconds. + Weight::from_parts(29_073_000, 6714) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -2756,28 +2694,10 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaInProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetAlphaInProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:1) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:1) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::Positions` (r:1 w:1) - /// Proof: `Swap::Positions` (`max_values`: None, `max_size`: Some(140), added: 2615, mode: `MaxEncodedLen`) - /// Storage: `Swap::Ticks` (r:2 w:2) - /// Proof: `Swap::Ticks` (`max_values`: None, `max_size`: Some(78), added: 2553, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:5 w:5) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) - /// Storage: `Swap::FeeGlobalTao` (r:1 w:0) - /// Proof: `Swap::FeeGlobalTao` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::FeeGlobalAlpha` (r:1 w:0) - /// Proof: `Swap::FeeGlobalAlpha` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:1) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) - /// Storage: `Swap::LastPositionId` (r:1 w:1) - /// Proof: `Swap::LastPositionId` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) @@ -2822,18 +2742,16 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::Keys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::IsNetworkMember` (r:0 w:1) /// Proof: `SubtensorModule::IsNetworkMember` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::AlphaSqrtPrice` (r:0 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:0 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) + /// Storage: `Swap::SwapBalancer` (r:0 w:1) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) fn burned_register() -> Weight { // Proof Size summary in bytes: - // Measured: `1649` - // Estimated: `13600` - // Minimum execution time: 362_295_000 picoseconds. - Weight::from_parts(368_123_000, 13600) - .saturating_add(RocksDbWeight::get().reads(48_u64)) - .saturating_add(RocksDbWeight::get().writes(40_u64)) + // Measured: `1770` + // Estimated: `6148` + // Minimum execution time: 337_589_000 picoseconds. + Weight::from_parts(341_856_000, 6148) + .saturating_add(RocksDbWeight::get().reads(34_u64)) + .saturating_add(RocksDbWeight::get().writes(29_u64)) } /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -2881,10 +2799,10 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::IsNetworkMember` (`max_values`: None, `max_size`: None, mode: `Measured`) fn root_register() -> Weight { // Proof Size summary in bytes: - // Measured: `1445` - // Estimated: `4910` - // Minimum execution time: 102_751_000 picoseconds. - Weight::from_parts(104_294_000, 4910) + // Measured: `1482` + // Estimated: `4947` + // Minimum execution time: 100_028_000 picoseconds. + Weight::from_parts(102_953_000, 4947) .saturating_add(RocksDbWeight::get().reads(19_u64)) .saturating_add(RocksDbWeight::get().writes(16_u64)) } @@ -2914,16 +2832,12 @@ impl WeightInfo for () { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:1) /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaInProvided` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetAlphaInProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::TotalNetworks` (r:1 w:1) /// Proof: `SubtensorModule::TotalNetworks` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Kappa` (r:1 w:1) @@ -3004,12 +2918,12 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::MaxAllowedUids` (`max_values`: None, `max_size`: None, mode: `Measured`) fn register_network() -> Weight { // Proof Size summary in bytes: - // Measured: `1459` - // Estimated: `9874` - // Minimum execution time: 277_470_000 picoseconds. - Weight::from_parts(282_297_000, 9874) - .saturating_add(RocksDbWeight::get().reads(42_u64)) - .saturating_add(RocksDbWeight::get().writes(49_u64)) + // Measured: `1532` + // Estimated: `9947` + // Minimum execution time: 266_053_000 picoseconds. + Weight::from_parts(272_392_000, 9947) + .saturating_add(RocksDbWeight::get().reads(40_u64)) + .saturating_add(RocksDbWeight::get().writes(47_u64)) } /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -3035,8 +2949,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `1071` // Estimated: `4536` - // Minimum execution time: 60_340_000 picoseconds. - Weight::from_parts(61_421_000, 4536) + // Minimum execution time: 57_885_000 picoseconds. + Weight::from_parts(58_817_000, 4536) .saturating_add(RocksDbWeight::get().reads(10_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -3080,8 +2994,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `1589` // Estimated: `7529` - // Minimum execution time: 108_541_000 picoseconds. - Weight::from_parts(110_183_000, 7529) + // Minimum execution time: 105_065_000 picoseconds. + Weight::from_parts(107_229_000, 7529) .saturating_add(RocksDbWeight::get().reads(18_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -3091,8 +3005,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 4_076_000 picoseconds. - Weight::from_parts(4_647_000, 0) + // Minimum execution time: 4_036_000 picoseconds. + Weight::from_parts(4_397_000, 0) .saturating_add(RocksDbWeight::get().writes(1_u64)) } /// Storage: `SubtensorModule::Owner` (r:1 w:0) @@ -3111,10 +3025,10 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::TransactionKeyLastBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn set_childkey_take() -> Weight { // Proof Size summary in bytes: - // Measured: `999` - // Estimated: `4464` - // Minimum execution time: 52_278_000 picoseconds. - Weight::from_parts(53_209_000, 4464) + // Measured: `1033` + // Estimated: `4498` + // Minimum execution time: 51_045_000 picoseconds. + Weight::from_parts(51_967_000, 4498) .saturating_add(RocksDbWeight::get().reads(7_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -3130,8 +3044,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `694` // Estimated: `4159` - // Minimum execution time: 43_995_000 picoseconds. - Weight::from_parts(45_167_000, 4159) + // Minimum execution time: 43_204_000 picoseconds. + Weight::from_parts(45_056_000, 4159) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -3167,17 +3081,19 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::MaturityRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Lock` (r:2 w:0) /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::DecayingLock` (r:1 w:0) + /// Proof: `SubtensorModule::DecayingLock` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `System::Account` (r:2 w:2) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::LastRateLimitedBlock` (r:0 w:1) /// Proof: `SubtensorModule::LastRateLimitedBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn swap_coldkey_announced() -> Weight { // Proof Size summary in bytes: - // Measured: `2175` - // Estimated: `13065` - // Minimum execution time: 286_653_000 picoseconds. - Weight::from_parts(294_536_000, 13065) - .saturating_add(RocksDbWeight::get().reads(35_u64)) + // Measured: `2110` + // Estimated: `13000` + // Minimum execution time: 285_883_000 picoseconds. + Weight::from_parts(289_268_000, 13000) + .saturating_add(RocksDbWeight::get().reads(36_u64)) .saturating_add(RocksDbWeight::get().writes(15_u64)) } /// Storage: `System::Account` (r:2 w:2) @@ -3214,6 +3130,8 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::MaturityRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Lock` (r:2 w:0) /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::DecayingLock` (r:1 w:0) + /// Proof: `SubtensorModule::DecayingLock` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::ColdkeySwapAnnouncements` (r:0 w:1) /// Proof: `SubtensorModule::ColdkeySwapAnnouncements` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::ColdkeySwapDisputes` (r:0 w:1) @@ -3222,11 +3140,11 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::LastRateLimitedBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn swap_coldkey() -> Weight { // Proof Size summary in bytes: - // Measured: `2231` - // Estimated: `13121` - // Minimum execution time: 310_339_000 picoseconds. - Weight::from_parts(313_503_000, 13121) - .saturating_add(RocksDbWeight::get().reads(35_u64)) + // Measured: `2166` + // Estimated: `13056` + // Minimum execution time: 308_517_000 picoseconds. + Weight::from_parts(314_044_000, 13056) + .saturating_add(RocksDbWeight::get().reads(36_u64)) .saturating_add(RocksDbWeight::get().writes(19_u64)) } /// Storage: `SubtensorModule::ColdkeySwapAnnouncements` (r:1 w:0) @@ -3237,8 +3155,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `665` // Estimated: `4130` - // Minimum execution time: 20_290_000 picoseconds. - Weight::from_parts(21_452_000, 4130) + // Minimum execution time: 20_170_000 picoseconds. + Weight::from_parts(20_820_000, 4130) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -3250,8 +3168,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `613` // Estimated: `4078` - // Minimum execution time: 16_995_000 picoseconds. - Weight::from_parts(17_505_000, 4078) + // Minimum execution time: 16_435_000 picoseconds. + Weight::from_parts(17_065_000, 4078) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -3263,8 +3181,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 6_830_000 picoseconds. - Weight::from_parts(7_271_000, 0) + // Minimum execution time: 6_750_000 picoseconds. + Weight::from_parts(7_190_000, 0) .saturating_add(RocksDbWeight::get().writes(2_u64)) } /// Storage: `SubtensorModule::CommitRevealWeightsEnabled` (r:1 w:0) @@ -3307,8 +3225,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `2094` // Estimated: `8034` - // Minimum execution time: 429_484_000 picoseconds. - Weight::from_parts(443_415_000, 8034) + // Minimum execution time: 414_593_000 picoseconds. + Weight::from_parts(422_245_000, 8034) .saturating_add(RocksDbWeight::get().reads(18_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -3340,10 +3258,10 @@ impl WeightInfo for () { /// Proof: `AlphaAssets::TotalAlphaIssuance` (`max_values`: None, `max_size`: None, mode: `Measured`) fn recycle_alpha() -> Weight { // Proof Size summary in bytes: - // Measured: `1873` - // Estimated: `5338` - // Minimum execution time: 176_220_000 picoseconds. - Weight::from_parts(178_253_000, 5338) + // Measured: `1754` + // Estimated: `5219` + // Minimum execution time: 173_126_000 picoseconds. + Weight::from_parts(174_428_000, 5219) .saturating_add(RocksDbWeight::get().reads(13_u64)) .saturating_add(RocksDbWeight::get().writes(6_u64)) } @@ -3373,10 +3291,10 @@ impl WeightInfo for () { /// Proof: `AlphaAssets::AlphaBurned` (`max_values`: None, `max_size`: None, mode: `Measured`) fn burn_alpha() -> Weight { // Proof Size summary in bytes: - // Measured: `1873` - // Estimated: `5338` - // Minimum execution time: 172_335_000 picoseconds. - Weight::from_parts(174_197_000, 5338) + // Measured: `1754` + // Estimated: `5219` + // Minimum execution time: 167_228_000 picoseconds. + Weight::from_parts(169_080_000, 5219) .saturating_add(RocksDbWeight::get().reads(12_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } @@ -3396,8 +3314,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `1118` // Estimated: `4583` - // Minimum execution time: 37_836_000 picoseconds. - Weight::from_parts(38_907_000, 4583) + // Minimum execution time: 37_305_000 picoseconds. + Weight::from_parts(38_226_000, 4583) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -3405,20 +3323,14 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaInProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetAlphaInProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:3 w:0) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:0) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:0) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubtokenEnabled` (r:1 w:0) @@ -3435,8 +3347,6 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetVolume` (r:1 w:1) @@ -3467,18 +3377,18 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::UnlockRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::MaturityRate` (r:1 w:0) /// Proof: `SubtensorModule::MaturityRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:0 w:1) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LockingColdkeys` (r:0 w:1) + /// Proof: `SubtensorModule::LockingColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (r:0 w:1) /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn add_stake_limit() -> Weight { // Proof Size summary in bytes: - // Measured: `2633` + // Measured: `2226` // Estimated: `8727` - // Minimum execution time: 499_258_000 picoseconds. - Weight::from_parts(516_242_000, 8727) - .saturating_add(RocksDbWeight::get().reads(38_u64)) - .saturating_add(RocksDbWeight::get().writes(18_u64)) + // Minimum execution time: 862_916_000 picoseconds. + Weight::from_parts(876_506_000, 8727) + .saturating_add(RocksDbWeight::get().reads(32_u64)) + .saturating_add(RocksDbWeight::get().writes(16_u64)) } /// Storage: `SubtensorModule::Alpha` (r:2 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -3490,8 +3400,6 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:2 w:2) /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:1 w:0) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubtokenEnabled` (r:1 w:0) @@ -3502,18 +3410,20 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:0) /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:0) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (r:0 w:1) /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn move_stake() -> Weight { // Proof Size summary in bytes: - // Measured: `2060` - // Estimated: `8000` - // Minimum execution time: 222_999_000 picoseconds. - Weight::from_parts(227_526_000, 8000) + // Measured: `1979` + // Estimated: `7919` + // Minimum execution time: 218_613_000 picoseconds. + Weight::from_parts(219_955_000, 7919) .saturating_add(RocksDbWeight::get().reads(19_u64)) .saturating_add(RocksDbWeight::get().writes(7_u64)) } @@ -3531,34 +3441,24 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::NetworksAdded` (r:3 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:1 w:0) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetMechanism` (r:2 w:0) /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:3 w:0) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:0) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:0) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::Owner` (r:1 w:0) /// Proof: `SubtensorModule::Owner` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::StakingHotkeys` (r:1 w:0) /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Lock` (r:1 w:0) /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) @@ -3577,35 +3477,27 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn remove_stake() -> Weight { // Proof Size summary in bytes: - // Measured: `2564` - // Estimated: `10979` - // Minimum execution time: 435_183_000 picoseconds. - Weight::from_parts(444_777_000, 10979) - .saturating_add(RocksDbWeight::get().reads(35_u64)) - .saturating_add(RocksDbWeight::get().writes(15_u64)) + // Measured: `2142` + // Estimated: `10557` + // Minimum execution time: 559_437_000 picoseconds. + Weight::from_parts(573_518_000, 10557) + .saturating_add(RocksDbWeight::get().reads(28_u64)) + .saturating_add(RocksDbWeight::get().writes(13_u64)) } /// Storage: `SubtensorModule::SubnetMechanism` (r:2 w:0) /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:3 w:0) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:0) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:0) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::NetworksAdded` (r:3 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:1 w:0) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Alpha` (r:1 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::AlphaV2` (r:1 w:1) @@ -3622,8 +3514,6 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Lock` (r:1 w:0) /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) @@ -3642,12 +3532,12 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn remove_stake_limit() -> Weight { // Proof Size summary in bytes: - // Measured: `2598` - // Estimated: `11013` - // Minimum execution time: 475_352_000 picoseconds. - Weight::from_parts(478_116_000, 11013) - .saturating_add(RocksDbWeight::get().reads(34_u64)) - .saturating_add(RocksDbWeight::get().writes(15_u64)) + // Measured: `2176` + // Estimated: `10591` + // Minimum execution time: 739_182_000 picoseconds. + Weight::from_parts(755_287_000, 10591) + .saturating_add(RocksDbWeight::get().reads(27_u64)) + .saturating_add(RocksDbWeight::get().writes(13_u64)) } /// Storage: `SubtensorModule::Alpha` (r:2 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -3663,22 +3553,14 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetTAO` (r:2 w:2) /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:3 w:0) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:0) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:2 w:2) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:0) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:1 w:1) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::NetworksAdded` (r:2 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubtokenEnabled` (r:2 w:0) @@ -3689,8 +3571,6 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Lock` (r:3 w:1) /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaIn` (r:2 w:2) - /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:2 w:2) /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) @@ -3719,16 +3599,18 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::UnlockRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::MaturityRate` (r:1 w:0) /// Proof: `SubtensorModule::MaturityRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LockingColdkeys` (r:0 w:1) + /// Proof: `SubtensorModule::LockingColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (r:0 w:1) /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn swap_stake_limit() -> Weight { // Proof Size summary in bytes: - // Measured: `3108` - // Estimated: `11523` - // Minimum execution time: 688_567_000 picoseconds. - Weight::from_parts(707_234_000, 11523) - .saturating_add(RocksDbWeight::get().reads(54_u64)) - .saturating_add(RocksDbWeight::get().writes(26_u64)) + // Measured: `2662` + // Estimated: `11077` + // Minimum execution time: 949_273_000 picoseconds. + Weight::from_parts(964_857_000, 11077) + .saturating_add(RocksDbWeight::get().reads(47_u64)) + .saturating_add(RocksDbWeight::get().writes(24_u64)) } /// Storage: `SubtensorModule::Alpha` (r:2 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -3740,8 +3622,6 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:1 w:1) /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:1 w:0) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubtokenEnabled` (r:1 w:0) @@ -3756,18 +3636,20 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:0) /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:0) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (r:0 w:1) /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn transfer_stake() -> Weight { // Proof Size summary in bytes: - // Measured: `2054` - // Estimated: `7994` - // Minimum execution time: 254_636_000 picoseconds. - Weight::from_parts(258_541_000, 7994) + // Measured: `1988` + // Estimated: `7928` + // Minimum execution time: 250_861_000 picoseconds. + Weight::from_parts(253_195_000, 7928) .saturating_add(RocksDbWeight::get().reads(18_u64)) .saturating_add(RocksDbWeight::get().writes(6_u64)) } @@ -3781,8 +3663,6 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:2 w:2) /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:1 w:1) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::NetworksAdded` (r:2 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubtokenEnabled` (r:2 w:0) @@ -3793,26 +3673,18 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetTAO` (r:2 w:2) /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:3 w:0) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:0) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:2 w:2) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:0) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::StakingHotkeys` (r:1 w:0) /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Lock` (r:3 w:1) /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaIn` (r:2 w:2) - /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:2 w:2) /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) @@ -3841,16 +3713,18 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::UnlockRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::MaturityRate` (r:1 w:0) /// Proof: `SubtensorModule::MaturityRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LockingColdkeys` (r:0 w:1) + /// Proof: `SubtensorModule::LockingColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (r:0 w:1) /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn swap_stake() -> Weight { // Proof Size summary in bytes: - // Measured: `2951` - // Estimated: `11366` - // Minimum execution time: 633_996_000 picoseconds. - Weight::from_parts(655_699_000, 11366) - .saturating_add(RocksDbWeight::get().reads(54_u64)) - .saturating_add(RocksDbWeight::get().writes(26_u64)) + // Measured: `2505` + // Estimated: `10920` + // Minimum execution time: 728_698_000 picoseconds. + Weight::from_parts(746_774_000, 10920) + .saturating_add(RocksDbWeight::get().reads(47_u64)) + .saturating_add(RocksDbWeight::get().writes(24_u64)) } /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -3878,8 +3752,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `1122` // Estimated: `4587` - // Minimum execution time: 127_058_000 picoseconds. - Weight::from_parts(129_030_000, 4587) + // Minimum execution time: 123_562_000 picoseconds. + Weight::from_parts(125_566_000, 4587) .saturating_add(RocksDbWeight::get().reads(11_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -3919,8 +3793,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `1426` // Estimated: `7366` - // Minimum execution time: 101_319_000 picoseconds. - Weight::from_parts(102_992_000, 7366) + // Minimum execution time: 97_624_000 picoseconds. + Weight::from_parts(100_468_000, 7366) .saturating_add(RocksDbWeight::get().reads(16_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -3934,10 +3808,10 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::LastRateLimitedBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn decrease_take() -> Weight { // Proof Size summary in bytes: - // Measured: `793` - // Estimated: `4258` - // Minimum execution time: 25_969_000 picoseconds. - Weight::from_parts(27_160_000, 4258) + // Measured: `830` + // Estimated: `4295` + // Minimum execution time: 26_830_000 picoseconds. + Weight::from_parts(27_561_000, 4295) .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -3953,10 +3827,10 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::TxDelegateTakeRateLimit` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) fn increase_take() -> Weight { // Proof Size summary in bytes: - // Measured: `886` - // Estimated: `4351` - // Minimum execution time: 33_360_000 picoseconds. - Weight::from_parts(34_381_000, 4351) + // Measured: `923` + // Estimated: `4388` + // Minimum execution time: 33_029_000 picoseconds. + Weight::from_parts(34_211_000, 4388) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -3984,16 +3858,12 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::BlockEmission` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:1) /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaInProvided` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetAlphaInProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::TotalNetworks` (r:1 w:1) /// Proof: `SubtensorModule::TotalNetworks` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Kappa` (r:1 w:1) @@ -4076,12 +3946,12 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::MaxAllowedUids` (`max_values`: None, `max_size`: None, mode: `Measured`) fn register_network_with_identity() -> Weight { // Proof Size summary in bytes: - // Measured: `1343` - // Estimated: `9758` - // Minimum execution time: 271_161_000 picoseconds. - Weight::from_parts(278_281_000, 9758) - .saturating_add(RocksDbWeight::get().reads(41_u64)) - .saturating_add(RocksDbWeight::get().writes(48_u64)) + // Measured: `1468` + // Estimated: `9883` + // Minimum execution time: 266_604_000 picoseconds. + Weight::from_parts(276_799_000, 9883) + .saturating_add(RocksDbWeight::get().reads(39_u64)) + .saturating_add(RocksDbWeight::get().writes(46_u64)) } /// Storage: `SubtensorModule::IsNetworkMember` (r:2 w:0) /// Proof: `SubtensorModule::IsNetworkMember` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -4093,8 +3963,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `772` // Estimated: `6712` - // Minimum execution time: 31_877_000 picoseconds. - Weight::from_parts(32_949_000, 6712) + // Minimum execution time: 31_506_000 picoseconds. + Weight::from_parts(32_528_000, 6712) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -4106,10 +3976,10 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::IdentitiesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) fn set_identity() -> Weight { // Proof Size summary in bytes: - // Measured: `852` - // Estimated: `6792` - // Minimum execution time: 28_833_000 picoseconds. - Weight::from_parts(29_874_000, 6792) + // Measured: `889` + // Estimated: `6829` + // Minimum execution time: 29_043_000 picoseconds. + Weight::from_parts(30_816_000, 6829) .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -4121,8 +3991,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `595` // Estimated: `4060` - // Minimum execution time: 15_502_000 picoseconds. - Weight::from_parts(16_184_000, 4060) + // Minimum execution time: 15_503_000 picoseconds. + Weight::from_parts(16_204_000, 4060) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -4196,10 +4066,10 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::Keys` (`max_values`: None, `max_size`: None, mode: `Measured`) fn swap_hotkey() -> Weight { // Proof Size summary in bytes: - // Measured: `3026` - // Estimated: `28766` - // Minimum execution time: 1_201_334_000 picoseconds. - Weight::from_parts(1_208_365_000, 28766) + // Measured: `3131` + // Estimated: `28871` + // Minimum execution time: 1_193_554_000 picoseconds. + Weight::from_parts(1_201_736_000, 28871) .saturating_add(RocksDbWeight::get().reads(171_u64)) .saturating_add(RocksDbWeight::get().writes(95_u64)) } @@ -4211,10 +4081,10 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) fn try_associate_hotkey() -> Weight { // Proof Size summary in bytes: - // Measured: `745` - // Estimated: `4210` - // Minimum execution time: 22_373_000 picoseconds. - Weight::from_parts(23_134_000, 4210) + // Measured: `818` + // Estimated: `4283` + // Minimum execution time: 23_084_000 picoseconds. + Weight::from_parts(23_836_000, 4283) .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -4226,10 +4096,10 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::SubtokenEnabled` (`max_values`: None, `max_size`: None, mode: `Measured`) fn unstake_all() -> Weight { // Proof Size summary in bytes: - // Measured: `740` - // Estimated: `9155` - // Minimum execution time: 25_017_000 picoseconds. - Weight::from_parts(25_658_000, 9155) + // Measured: `774` + // Estimated: `9189` + // Minimum execution time: 24_587_000 picoseconds. + Weight::from_parts(25_608_000, 9189) .saturating_add(RocksDbWeight::get().reads(6_u64)) } /// Storage: `SubtensorModule::Owner` (r:1 w:0) @@ -4248,32 +4118,22 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:2 w:2) /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:1 w:0) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetMechanism` (r:2 w:0) /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetTAO` (r:2 w:2) /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:3 w:0) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:0) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:2 w:2) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:0) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::StakingHotkeys` (r:1 w:0) /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Lock` (r:2 w:0) /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaIn` (r:2 w:2) - /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:2 w:2) /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) @@ -4298,12 +4158,12 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn unstake_all_alpha() -> Weight { // Proof Size summary in bytes: - // Measured: `2642` + // Measured: `2235` // Estimated: `11306` - // Minimum execution time: 583_803_000 picoseconds. - Weight::from_parts(599_485_000, 11306) - .saturating_add(RocksDbWeight::get().reads(50_u64)) - .saturating_add(RocksDbWeight::get().writes(27_u64)) + // Minimum execution time: 695_268_000 picoseconds. + Weight::from_parts(708_618_000, 11306) + .saturating_add(RocksDbWeight::get().reads(43_u64)) + .saturating_add(RocksDbWeight::get().writes(25_u64)) } /// Storage: `SubtensorModule::Alpha` (r:1 w:0) /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -4319,32 +4179,22 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:3 w:0) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:0) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:0) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::NetworksAdded` (r:3 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:1 w:0) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Owner` (r:1 w:0) /// Proof: `SubtensorModule::Owner` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::StakingHotkeys` (r:1 w:0) /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Lock` (r:1 w:0) /// Proof: `SubtensorModule::Lock` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) @@ -4363,12 +4213,12 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn remove_stake_full_limit() -> Weight { // Proof Size summary in bytes: - // Measured: `2598` - // Estimated: `11013` - // Minimum execution time: 497_956_000 picoseconds. - Weight::from_parts(503_033_000, 11013) - .saturating_add(RocksDbWeight::get().reads(34_u64)) - .saturating_add(RocksDbWeight::get().writes(15_u64)) + // Measured: `2176` + // Estimated: `10591` + // Minimum execution time: 763_548_000 picoseconds. + Weight::from_parts(778_691_000, 10591) + .saturating_add(RocksDbWeight::get().reads(27_u64)) + .saturating_add(RocksDbWeight::get().writes(13_u64)) } /// Storage: `Crowdloan::CurrentCrowdloanId` (r:1 w:0) /// Proof: `Crowdloan::CurrentCrowdloanId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -4402,16 +4252,12 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::BlockEmission` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetMechanism` (r:1 w:1) /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTaoProvided` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetTaoProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaInProvided` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetAlphaInProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::TotalNetworks` (r:1 w:1) /// Proof: `SubtensorModule::TotalNetworks` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::Kappa` (r:1 w:1) @@ -4505,15 +4351,15 @@ impl WeightInfo for () { /// The range of component `k` is `[2, 500]`. fn register_leased_network(k: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `1762 + k * (44 ±0)` - // Estimated: `10183 + k * (2579 ±0)` - // Minimum execution time: 488_121_000 picoseconds. - Weight::from_parts(306_068_429, 10183) - // Standard Error: 24_263 - .saturating_add(Weight::from_parts(48_393_644, 0).saturating_mul(k.into())) - .saturating_add(RocksDbWeight::get().reads(51_u64)) + // Measured: `1835 + k * (44 ±0)` + // Estimated: `10256 + k * (2579 ±0)` + // Minimum execution time: 480_320_000 picoseconds. + Weight::from_parts(250_987_824, 10256) + // Standard Error: 56_710 + .saturating_add(Weight::from_parts(48_825_380, 0).saturating_mul(k.into())) + .saturating_add(RocksDbWeight::get().reads(49_u64)) .saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(k.into()))) - .saturating_add(RocksDbWeight::get().writes(54_u64)) + .saturating_add(RocksDbWeight::get().writes(52_u64)) .saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(k.into()))) .saturating_add(Weight::from_parts(0, 2579).saturating_mul(k.into())) } @@ -4538,17 +4384,17 @@ impl WeightInfo for () { /// The range of component `k` is `[2, 500]`. fn terminate_lease(k: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `1468 + k * (53 ±0)` - // Estimated: `6148 + k * (2514 ±0)` - // Minimum execution time: 91_385_000 picoseconds. - Weight::from_parts(96_646_996, 6148) - // Standard Error: 5_309 - .saturating_add(Weight::from_parts(1_570_386, 0).saturating_mul(k.into())) + // Measured: `1501 + k * (53 ±0)` + // Estimated: `6148 + k * (2529 ±0)` + // Minimum execution time: 89_272_000 picoseconds. + Weight::from_parts(79_136_631, 6148) + // Standard Error: 7_622 + .saturating_add(Weight::from_parts(1_641_271, 0).saturating_mul(k.into())) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(k.into()))) .saturating_add(RocksDbWeight::get().writes(7_u64)) .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(k.into()))) - .saturating_add(Weight::from_parts(0, 2514).saturating_mul(k.into())) + .saturating_add(Weight::from_parts(0, 2529).saturating_mul(k.into())) } /// Storage: `SubtensorModule::SubnetOwner` (r:1 w:0) /// Proof: `SubtensorModule::SubnetOwner` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -4558,8 +4404,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `659` // Estimated: `9074` - // Minimum execution time: 24_486_000 picoseconds. - Weight::from_parts(25_798_000, 9074) + // Minimum execution time: 23_875_000 picoseconds. + Weight::from_parts(25_027_000, 9074) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -4587,8 +4433,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `1070` // Estimated: `4535` - // Minimum execution time: 72_186_000 picoseconds. - Weight::from_parts(73_359_000, 4535) + // Minimum execution time: 71_556_000 picoseconds. + Weight::from_parts(73_198_000, 4535) .saturating_add(RocksDbWeight::get().reads(10_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -4604,8 +4450,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `809` // Estimated: `4274` - // Minimum execution time: 31_566_000 picoseconds. - Weight::from_parts(32_979_000, 4274) + // Minimum execution time: 31_547_000 picoseconds. + Weight::from_parts(32_238_000, 4274) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -4621,8 +4467,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `476` // Estimated: `3941` - // Minimum execution time: 15_613_000 picoseconds. - Weight::from_parts(16_064_000, 3941) + // Minimum execution time: 15_273_000 picoseconds. + Weight::from_parts(16_074_000, 3941) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } @@ -4650,10 +4496,10 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::RootClaimableThreshold` (`max_values`: None, `max_size`: None, mode: `Measured`) fn claim_root() -> Weight { // Proof Size summary in bytes: - // Measured: `1929` - // Estimated: `7869` - // Minimum execution time: 140_608_000 picoseconds. - Weight::from_parts(142_310_000, 7869) + // Measured: `1935` + // Estimated: `7875` + // Minimum execution time: 139_456_000 picoseconds. + Weight::from_parts(141_309_000, 7875) .saturating_add(RocksDbWeight::get().reads(16_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } @@ -4663,8 +4509,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 1_883_000 picoseconds. - Weight::from_parts(2_083_000, 0) + // Minimum execution time: 2_023_000 picoseconds. + Weight::from_parts(2_303_000, 0) .saturating_add(RocksDbWeight::get().writes(1_u64)) } /// Storage: `SubtensorModule::RootClaimableThreshold` (r:0 w:1) @@ -4673,8 +4519,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 4_136_000 picoseconds. - Weight::from_parts(4_747_000, 0) + // Minimum execution time: 4_567_000 picoseconds. + Weight::from_parts(5_117_000, 0) .saturating_add(RocksDbWeight::get().writes(1_u64)) } /// Storage: `SubtensorModule::Owner` (r:1 w:0) @@ -4685,10 +4531,10 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::AutoParentDelegationEnabled` (`max_values`: None, `max_size`: None, mode: `Measured`) fn set_auto_parent_delegation_enabled() -> Weight { // Proof Size summary in bytes: - // Measured: `862` - // Estimated: `4327` - // Minimum execution time: 23_675_000 picoseconds. - Weight::from_parts(25_277_000, 4327) + // Measured: `899` + // Estimated: `4364` + // Minimum execution time: 24_225_000 picoseconds. + Weight::from_parts(25_578_000, 4364) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -4696,20 +4542,14 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::SubnetMechanism` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaInProvided` (r:1 w:0) - /// Proof: `SubtensorModule::SubnetAlphaInProvided` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::SwapV3Initialized` (r:1 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:1) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:1) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::TickIndexBitmapWords` (r:3 w:0) - /// Proof: `Swap::TickIndexBitmapWords` (`max_values`: None, `max_size`: Some(47), added: 2522, mode: `MaxEncodedLen`) + /// Storage: `Swap::PalSwapInitialized` (r:1 w:0) + /// Proof: `Swap::PalSwapInitialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) + /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) + /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Swap::SwapBalancer` (r:1 w:0) + /// Proof: `Swap::SwapBalancer` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `Swap::FeeRate` (r:1 w:0) /// Proof: `Swap::FeeRate` (`max_values`: None, `max_size`: Some(12), added: 2487, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:0) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubtokenEnabled` (r:1 w:0) @@ -4726,8 +4566,6 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::TotalStake` (r:1 w:1) /// Proof: `SubtensorModule::TotalStake` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::SubnetVolume` (r:1 w:1) @@ -4760,18 +4598,18 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::MaturityRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `AlphaAssets::AlphaBurned` (r:1 w:1) /// Proof: `AlphaAssets::AlphaBurned` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingOperationRateLimiter` (r:0 w:1) - /// Proof: `SubtensorModule::StakingOperationRateLimiter` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LockingColdkeys` (r:0 w:1) + /// Proof: `SubtensorModule::LockingColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (r:0 w:1) /// Proof: `SubtensorModule::LastColdkeyHotkeyStakeBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) fn add_stake_burn() -> Weight { // Proof Size summary in bytes: - // Measured: `2636` + // Measured: `2229` // Estimated: `8727` - // Minimum execution time: 630_852_000 picoseconds. - Weight::from_parts(646_565_000, 8727) - .saturating_add(RocksDbWeight::get().reads(39_u64)) - .saturating_add(RocksDbWeight::get().writes(19_u64)) + // Minimum execution time: 990_125_000 picoseconds. + Weight::from_parts(995_442_000, 8727) + .saturating_add(RocksDbWeight::get().reads(33_u64)) + .saturating_add(RocksDbWeight::get().writes(17_u64)) } /// Storage: `SubtensorModule::PendingChildKeyCooldown` (r:0 w:1) /// Proof: `SubtensorModule::PendingChildKeyCooldown` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) @@ -4779,8 +4617,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 1_963_000 picoseconds. - Weight::from_parts(2_083_000, 0) + // Minimum execution time: 2_013_000 picoseconds. + Weight::from_parts(2_173_000, 0) .saturating_add(RocksDbWeight::get().writes(1_u64)) } /// Storage: `SubtensorModule::Owner` (r:1 w:0) @@ -4815,14 +4653,16 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::UnlockRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::MaturityRate` (r:1 w:0) /// Proof: `SubtensorModule::MaturityRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LockingColdkeys` (r:0 w:1) + /// Proof: `SubtensorModule::LockingColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) fn lock_stake() -> Weight { // Proof Size summary in bytes: - // Measured: `1644` - // Estimated: `7584` - // Minimum execution time: 111_775_000 picoseconds. - Weight::from_parts(114_028_000, 7584) + // Measured: `1715` + // Estimated: `7655` + // Minimum execution time: 114_670_000 picoseconds. + Weight::from_parts(116_542_000, 7655) .saturating_add(RocksDbWeight::get().reads(17_u64)) - .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) } /// Storage: `SubtensorModule::Owner` (r:2 w:0) /// Proof: `SubtensorModule::Owner` (`max_values`: None, `max_size`: None, mode: `Measured`) @@ -4844,13 +4684,101 @@ impl WeightInfo for () { /// Proof: `SubtensorModule::UnlockRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `SubtensorModule::MaturityRate` (r:1 w:0) /// Proof: `SubtensorModule::MaturityRate` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LockingColdkeys` (r:0 w:2) + /// Proof: `SubtensorModule::LockingColdkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) fn move_lock() -> Weight { // Proof Size summary in bytes: - // Measured: `1366` - // Estimated: `7306` - // Minimum execution time: 146_897_000 picoseconds. - Weight::from_parts(148_699_000, 7306) + // Measured: `1399` + // Estimated: `7339` + // Minimum execution time: 154_609_000 picoseconds. + Weight::from_parts(156_442_000, 7339) .saturating_add(RocksDbWeight::get().reads(14_u64)) - .saturating_add(RocksDbWeight::get().writes(4_u64)) + .saturating_add(RocksDbWeight::get().writes(6_u64)) + } + /// Storage: `SubtensorModule::Owner` (r:1 w:0) + /// Proof: `SubtensorModule::Owner` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Uids` (r:1 w:0) + /// Proof: `SubtensorModule::Uids` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::AssociatedEvmAddress` (r:1 w:1) + /// Proof: `SubtensorModule::AssociatedEvmAddress` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn associate_evm_key() -> Weight { + // Proof Size summary in bytes: + // Measured: `950` + // Estimated: `4415` + // Minimum execution time: 697_391_000 picoseconds. + Weight::from_parts(712_594_000, 4415) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `SubtensorModule::SubnetOwner` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetOwner` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::CommitRevealWeightsEnabled` (r:1 w:0) + /// Proof: `SubtensorModule::CommitRevealWeightsEnabled` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Tempo` (r:1 w:1) + /// Proof: `SubtensorModule::Tempo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::PendingEpochAt` (r:1 w:0) + /// Proof: `SubtensorModule::PendingEpochAt` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LastEpochBlock` (r:1 w:1) + /// Proof: `SubtensorModule::LastEpochBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::AdminFreezeWindow` (r:1 w:0) + /// Proof: `SubtensorModule::AdminFreezeWindow` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::TransactionKeyLastBlock` (r:1 w:1) + /// Proof: `SubtensorModule::TransactionKeyLastBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn set_tempo() -> Weight { + // Proof Size summary in bytes: + // Measured: `1015` + // Estimated: `4480` + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(35_000_000, 4480) + .saturating_add(RocksDbWeight::get().reads(7_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: `SubtensorModule::SubnetOwner` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetOwner` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Tempo` (r:1 w:0) + /// Proof: `SubtensorModule::Tempo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::PendingEpochAt` (r:1 w:0) + /// Proof: `SubtensorModule::PendingEpochAt` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LastEpochBlock` (r:1 w:0) + /// Proof: `SubtensorModule::LastEpochBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::AdminFreezeWindow` (r:1 w:0) + /// Proof: `SubtensorModule::AdminFreezeWindow` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::OwnerHyperparamRateLimit` (r:1 w:0) + /// Proof: `SubtensorModule::OwnerHyperparamRateLimit` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LastRateLimitedBlock` (r:1 w:1) + /// Proof: `SubtensorModule::LastRateLimitedBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::ActivityCutoffFactorMilli` (r:0 w:1) + /// Proof: `SubtensorModule::ActivityCutoffFactorMilli` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn set_activity_cutoff_factor() -> Weight { + // Proof Size summary in bytes: + // Measured: `889` + // Estimated: `4354` + // Minimum execution time: 29_000_000 picoseconds. + Weight::from_parts(31_000_000, 4354) + .saturating_add(RocksDbWeight::get().reads(7_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `SubtensorModule::SubnetOwner` (r:1 w:0) + /// Proof: `SubtensorModule::SubnetOwner` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::CommitRevealWeightsEnabled` (r:1 w:0) + /// Proof: `SubtensorModule::CommitRevealWeightsEnabled` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::PendingEpochAt` (r:1 w:1) + /// Proof: `SubtensorModule::PendingEpochAt` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::OwnerHyperparamRateLimit` (r:1 w:0) + /// Proof: `SubtensorModule::OwnerHyperparamRateLimit` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::Tempo` (r:1 w:0) + /// Proof: `SubtensorModule::Tempo` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::LastRateLimitedBlock` (r:1 w:1) + /// Proof: `SubtensorModule::LastRateLimitedBlock` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `SubtensorModule::AdminFreezeWindow` (r:1 w:0) + /// Proof: `SubtensorModule::AdminFreezeWindow` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn trigger_epoch() -> Weight { + // Proof Size summary in bytes: + // Measured: `853` + // Estimated: `4318` + // Minimum execution time: 25_000_000 picoseconds. + Weight::from_parts(28_000_000, 4318) + .saturating_add(RocksDbWeight::get().reads(7_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) } } diff --git a/pallets/swap-interface/src/lib.rs b/pallets/swap-interface/src/lib.rs deleted file mode 100644 index a37e9e49ad..0000000000 --- a/pallets/swap-interface/src/lib.rs +++ /dev/null @@ -1,98 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std)] -use core::ops::Neg; - -use frame_support::pallet_prelude::*; -use substrate_fixed::types::U96F32; -use subtensor_macros::freeze_struct; -use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; - -pub use order::*; - -mod order; - -pub trait SwapEngine: DefaultPriceLimit { - fn swap( - netuid: NetUid, - order: O, - price_limit: TaoBalance, - drop_fees: bool, - should_rollback: bool, - ) -> Result, DispatchError>; -} - -pub trait SwapHandler { - fn swap( - netuid: NetUid, - order: O, - price_limit: TaoBalance, - drop_fees: bool, - should_rollback: bool, - ) -> Result, DispatchError> - where - Self: SwapEngine; - fn sim_swap( - netuid: NetUid, - order: O, - ) -> Result, DispatchError> - where - Self: SwapEngine; - - fn approx_fee_amount(netuid: NetUid, amount: T) -> T; - fn current_alpha_price(netuid: NetUid) -> U96F32; - fn get_protocol_tao(netuid: NetUid) -> TaoBalance; - fn max_price() -> C; - fn min_price() -> C; - fn adjust_protocol_liquidity(netuid: NetUid, tao_delta: TaoBalance, alpha_delta: AlphaBalance); - fn is_user_liquidity_enabled(netuid: NetUid) -> bool; - fn dissolve_all_liquidity_providers(netuid: NetUid) -> DispatchResultWithPostInfo; - fn toggle_user_liquidity(netuid: NetUid, enabled: bool); - fn clear_protocol_liquidity(netuid: NetUid) -> DispatchResult; - fn get_alpha_amount_for_tao(netuid: NetUid, tao_amount: TaoBalance) -> AlphaBalance; -} - -pub trait DefaultPriceLimit -where - PaidIn: Token, - PaidOut: Token, -{ - fn default_price_limit() -> C; -} - -/// Externally used swap result (for RPC) -#[freeze_struct("6a03533fc53ccfb8")] -#[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] -pub struct SwapResult -where - PaidIn: Token, - PaidOut: Token, -{ - pub amount_paid_in: PaidIn, - pub amount_paid_out: PaidOut, - pub fee_paid: PaidIn, - pub fee_to_block_author: PaidIn, -} - -impl SwapResult -where - PaidIn: Token, - PaidOut: Token, -{ - pub fn paid_in_reserve_delta(&self) -> i128 { - self.amount_paid_in.to_u64() as i128 - } - - pub fn paid_in_reserve_delta_i64(&self) -> i64 { - self.paid_in_reserve_delta() - .clamp(i64::MIN as i128, i64::MAX as i128) as i64 - } - - pub fn paid_out_reserve_delta(&self) -> i128 { - (self.amount_paid_out.to_u64() as i128).neg() - } - - pub fn paid_out_reserve_delta_i64(&self) -> i64 { - (self.amount_paid_out.to_u64() as i128) - .neg() - .clamp(i64::MIN as i128, i64::MAX as i128) as i64 - } -} diff --git a/pallets/swap/Cargo.toml b/pallets/swap/Cargo.toml index c50d1d4f78..7bc61caa50 100644 --- a/pallets/swap/Cargo.toml +++ b/pallets/swap/Cargo.toml @@ -11,6 +11,7 @@ frame-benchmarking = { workspace = true, optional = true } frame-support.workspace = true frame-system.workspace = true log.workspace = true +safe-bigmath.workspace = true safe-math.workspace = true scale-info = { workspace = true, features = ["derive"] } serde = { workspace = true, optional = true } @@ -28,6 +29,8 @@ subtensor-swap-interface.workspace = true [dev-dependencies] sp-tracing.workspace = true +rand = { version = "0.8", default-features = false } +rayon = "1.10" [lints] workspace = true @@ -42,6 +45,8 @@ std = [ "frame-system/std", "log/std", "pallet-subtensor-swap-runtime-api/std", + "rand/std", + "safe-bigmath/std", "safe-math/std", "scale-info/std", "serde/std", @@ -61,4 +66,5 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "subtensor-runtime-common/runtime-benchmarks", + "subtensor-swap-interface/runtime-benchmarks", ] diff --git a/pallets/swap/rpc/src/lib.rs b/pallets/swap/rpc/src/lib.rs index b4a8d6a7a0..fa072c29ae 100644 --- a/pallets/swap/rpc/src/lib.rs +++ b/pallets/swap/rpc/src/lib.rs @@ -13,12 +13,14 @@ use sp_blockchain::HeaderBackend; use sp_runtime::traits::Block as BlockT; use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance}; -pub use pallet_subtensor_swap_runtime_api::SwapRuntimeApi; +pub use pallet_subtensor_swap_runtime_api::{SubnetPrice, SwapRuntimeApi}; #[rpc(client, server)] pub trait SwapRpcApi { #[method(name = "swap_currentAlphaPrice")] fn current_alpha_price(&self, netuid: NetUid, at: Option) -> RpcResult; + #[method(name = "swap_currentAlphaPriceAll")] + fn current_alpha_price_all(&self, at: Option) -> RpcResult>; #[method(name = "swap_simSwapTaoForAlpha")] fn sim_swap_tao_for_alpha( &self, @@ -92,6 +94,18 @@ where }) } + fn current_alpha_price_all( + &self, + at: Option<::Hash>, + ) -> RpcResult> { + let api = self.client.runtime_api(); + let at = at.unwrap_or_else(|| self.client.info().best_hash); + + api.current_alpha_price_all(at).map_err(|e| { + Error::RuntimeError(format!("Unable to get all current alpha prices: {e:?}")).into() + }) + } + fn sim_swap_tao_for_alpha( &self, netuid: NetUid, diff --git a/pallets/swap/runtime-api/Cargo.toml b/pallets/swap/runtime-api/Cargo.toml index 042875fdd0..7a70dc74e3 100644 --- a/pallets/swap/runtime-api/Cargo.toml +++ b/pallets/swap/runtime-api/Cargo.toml @@ -8,6 +8,7 @@ edition.workspace = true codec = { workspace = true, features = ["derive"] } frame-support.workspace = true scale-info.workspace = true +serde.workspace = true sp-api.workspace = true sp-std.workspace = true subtensor-macros.workspace = true @@ -20,6 +21,7 @@ std = [ "codec/std", "frame-support/std", "scale-info/std", + "serde/std", "sp-api/std", "sp-std/std", "subtensor-runtime-common/std", diff --git a/pallets/swap/runtime-api/src/lib.rs b/pallets/swap/runtime-api/src/lib.rs index 0433793efb..0f9803f162 100644 --- a/pallets/swap/runtime-api/src/lib.rs +++ b/pallets/swap/runtime-api/src/lib.rs @@ -1,6 +1,7 @@ #![cfg_attr(not(feature = "std"), no_std)] use frame_support::pallet_prelude::*; +use serde::{Deserialize, Serialize}; use sp_std::vec::Vec; use subtensor_macros::freeze_struct; use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance}; @@ -16,8 +17,8 @@ pub struct SimSwapResult { pub alpha_slippage: AlphaBalance, } -#[freeze_struct("423384310ac5e2f7")] -#[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] +#[freeze_struct("d7bbb761fc2b2eac")] +#[derive(Decode, Deserialize, Encode, PartialEq, Eq, Clone, Debug, Serialize, TypeInfo)] pub struct SubnetPrice { pub netuid: NetUid, pub price: u64, diff --git a/pallets/swap/src/benchmarking.rs b/pallets/swap/src/benchmarking.rs index b486266741..eed3c5a25b 100644 --- a/pallets/swap/src/benchmarking.rs +++ b/pallets/swap/src/benchmarking.rs @@ -2,24 +2,11 @@ #![allow(clippy::unwrap_used)] #![allow(clippy::multiple_bound_locations)] -use core::marker::PhantomData; - use frame_benchmarking::v2::*; -use frame_support::assert_err; -use frame_support::traits::Get; use frame_system::RawOrigin; -use substrate_fixed::types::{I64F64, U64F64}; use subtensor_runtime_common::NetUid; -use crate::{ - Error, - pallet::{ - AlphaSqrtPrice, BenchmarkHelper, Call, Config, CurrentLiquidity, CurrentTick, - EnabledUserLiquidity, Pallet, Positions, SwapV3Initialized, - }, - position::{Position, PositionId}, - tick::TickIndex, -}; +use crate::pallet::{Call, Config, Pallet}; #[benchmarks(where T: Config)] mod benchmarks { @@ -34,164 +21,5 @@ mod benchmarks { _(RawOrigin::Root, netuid, rate); } - #[benchmark] - fn add_liquidity() { - let netuid = NetUid::from(1); - - if !SwapV3Initialized::::get(netuid) { - SwapV3Initialized::::insert(netuid, true); - AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1)); - CurrentTick::::insert(netuid, TickIndex::new(0).unwrap()); - CurrentLiquidity::::insert(netuid, T::MinimumLiquidity::get()); - } - - let caller: T::AccountId = whitelisted_caller(); - let hotkey: T::AccountId = account("hotkey", 0, 0); - let tick_low = TickIndex::new_unchecked(-1000); - let tick_high = TickIndex::new_unchecked(1000); - - #[block] - { - assert_err!( - Pallet::::add_liquidity( - RawOrigin::Signed(caller).into(), - hotkey, - netuid, - tick_low, - tick_high, - 1000, - ), - Error::::UserLiquidityDisabled - ); - } - } - - #[benchmark] - fn remove_liquidity() { - let netuid = NetUid::from(1); - - T::BenchmarkHelper::setup_subnet(netuid); - - if !SwapV3Initialized::::get(netuid) { - SwapV3Initialized::::insert(netuid, true); - AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1)); - CurrentTick::::insert(netuid, TickIndex::new(0).unwrap()); - CurrentLiquidity::::insert(netuid, T::MinimumLiquidity::get()); - } - - let caller: T::AccountId = whitelisted_caller(); - let hotkey: T::AccountId = account("hotkey", 0, 0); - T::BenchmarkHelper::register_hotkey(&hotkey, &caller); - let id = PositionId::from(1u128); - - Positions::::insert( - (netuid, caller.clone(), id), - Position { - id, - netuid, - tick_low: TickIndex::new(-10000).unwrap(), - tick_high: TickIndex::new(10000).unwrap(), - liquidity: 1000, - fees_tao: I64F64::from_num(0), - fees_alpha: I64F64::from_num(0), - _phantom: PhantomData, - }, - ); - - #[extrinsic_call] - _(RawOrigin::Signed(caller), hotkey, netuid.into(), id.into()); - } - - #[benchmark] - fn modify_position() { - let netuid = NetUid::from(1); - - T::BenchmarkHelper::setup_subnet(netuid); - EnabledUserLiquidity::::insert(netuid, true); - - if !SwapV3Initialized::::get(netuid) { - SwapV3Initialized::::insert(netuid, true); - AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1)); - CurrentTick::::insert(netuid, TickIndex::new(0).unwrap()); - CurrentLiquidity::::insert(netuid, T::MinimumLiquidity::get()); - } - - let caller: T::AccountId = whitelisted_caller(); - let hotkey: T::AccountId = account("hotkey", 0, 0); - T::BenchmarkHelper::register_hotkey(&hotkey, &caller); - let id = PositionId::from(1u128); - - Positions::::insert( - (netuid, caller.clone(), id), - Position { - id, - netuid, - tick_low: TickIndex::new(-10000).unwrap(), - tick_high: TickIndex::new(10000).unwrap(), - liquidity: 10000, - fees_tao: I64F64::from_num(0), - fees_alpha: I64F64::from_num(0), - _phantom: PhantomData, - }, - ); - - #[extrinsic_call] - _( - RawOrigin::Signed(caller), - hotkey, - netuid.into(), - id.into(), - -5000, - ); - } - - #[benchmark] - fn disable_lp() { - // Create a single user LP position so that do_dissolve_all_liquidity_providers - // executes its main path at least once. - let caller: T::AccountId = whitelisted_caller(); - let id = PositionId::from(1u128); - - for index in 1..=128 { - let netuid = NetUid::from(index); - - SwapV3Initialized::::insert(netuid, true); - AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1)); - CurrentTick::::insert(netuid, TickIndex::new(0).unwrap()); - CurrentLiquidity::::insert(netuid, T::MinimumLiquidity::get()); - - Positions::::insert( - (netuid, caller.clone(), id), - Position { - id, - netuid, - tick_low: TickIndex::new(-10000).unwrap(), - tick_high: TickIndex::new(10000).unwrap(), - liquidity: 1_000, - fees_tao: I64F64::from_num(0), - fees_alpha: I64F64::from_num(0), - _phantom: PhantomData, - }, - ); - - // Enable user liquidity on this subnet so the toggle path is exercised. - EnabledUserLiquidity::::insert(netuid, true); - } - - #[extrinsic_call] - disable_lp(RawOrigin::Root); - } - - #[benchmark] - fn toggle_user_liquidity() { - let netuid = NetUid::from(101); - T::BenchmarkHelper::setup_subnet(netuid); - - assert!(!EnabledUserLiquidity::::get(netuid)); - - #[extrinsic_call] - _(RawOrigin::Root, netuid.into(), true); - } - impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); } diff --git a/pallets/swap/src/lib.rs b/pallets/swap/src/lib.rs index 6257df852b..b51c3351dc 100644 --- a/pallets/swap/src/lib.rs +++ b/pallets/swap/src/lib.rs @@ -1,10 +1,6 @@ #![cfg_attr(not(feature = "std"), no_std)] -use substrate_fixed::types::U64F64; - pub mod pallet; -pub mod position; -pub mod tick; pub mod weights; pub use pallet::*; @@ -14,5 +10,3 @@ pub mod benchmarking; #[cfg(test)] pub(crate) mod mock; - -type SqrtPrice = U64F64; diff --git a/pallets/swap/src/mock.rs b/pallets/swap/src/mock.rs index 65dcc676a2..1b0ada87be 100644 --- a/pallets/swap/src/mock.rs +++ b/pallets/swap/src/mock.rs @@ -9,21 +9,17 @@ use frame_support::{ }; use frame_support::{construct_runtime, derive_impl}; use frame_system::{self as system}; -use scale_info::prelude::collections::HashMap; use sp_core::H256; use sp_runtime::{ BuildStorage, Vec, traits::{BlakeTwo256, IdentityLookup}, }; -use sp_std::cell::RefCell; -use substrate_fixed::types::U64F64; +use std::{cell::RefCell, collections::HashMap}; use subtensor_runtime_common::{ - AlphaBalance, BalanceOps, NetUid, SubnetInfo, TaoBalance, Token, TokenReserve, + AlphaBalance, BalanceOps, NetUid, SubnetInfo, TaoBalance, TokenReserve, }; use subtensor_swap_interface::Order; -use crate::pallet::{EnabledUserLiquidity, FeeGlobalAlpha, FeeGlobalTao}; - construct_runtime!( pub enum Test { System: frame_system = 0, @@ -69,7 +65,6 @@ impl system::Config for Test { parameter_types! { pub const SwapProtocolId: PalletId = PalletId(*b"ten/swap"); pub const MaxFeeRate: u16 = 10000; // 15.26% - pub const MaxPositions: u32 = 100; pub const MinimumLiquidity: u64 = 1_000; pub const MinimumReserves: NonZeroU64 = NonZeroU64::new(1).unwrap(); } @@ -147,23 +142,7 @@ impl TokenReserve for AlphaReserve { pub type GetAlphaForTao = subtensor_swap_interface::GetAlphaForTao; pub type GetTaoForAlpha = subtensor_swap_interface::GetTaoForAlpha; -pub(crate) trait GlobalFeeInfo: Token { - #[allow(dead_code)] - fn global_fee(&self, netuid: NetUid) -> U64F64; -} - -impl GlobalFeeInfo for TaoBalance { - fn global_fee(&self, netuid: NetUid) -> U64F64 { - FeeGlobalTao::::get(netuid) - } -} - -impl GlobalFeeInfo for AlphaBalance { - fn global_fee(&self, netuid: NetUid) -> U64F64 { - FeeGlobalAlpha::::get(netuid) - } -} - +#[allow(dead_code)] pub(crate) trait TestExt { fn approx_expected_swap_output( sqrt_current_price: f64, @@ -293,7 +272,6 @@ impl crate::pallet::Config for Test { type BalanceOps = MockBalanceOps; type ProtocolId = SwapProtocolId; type MaxFeeRate = MaxFeeRate; - type MaxPositions = MaxPositions; type MinimumLiquidity = MinimumLiquidity; type MinimumReserve = MinimumReserves; type WeightInfo = (); @@ -310,12 +288,6 @@ pub fn new_test_ext() -> sp_io::TestExternalities { let mut ext = sp_io::TestExternalities::new(storage); ext.execute_with(|| { System::set_block_number(1); - - for netuid in 0u16..=100 { - // enable V3 for this range of netuids - EnabledUserLiquidity::::set(NetUid::from(netuid), true); - } - EnabledUserLiquidity::::set(NetUid::from(WRAPPING_FEES_NETUID), true); }); ext } diff --git a/pallets/swap/src/pallet/balancer.rs b/pallets/swap/src/pallet/balancer.rs new file mode 100644 index 0000000000..2ffd04fdba --- /dev/null +++ b/pallets/swap/src/pallet/balancer.rs @@ -0,0 +1,1339 @@ +// Balancer swap +// +// Unlike uniswap v2 or v3, it allows adding liquidity disproportionally to price. This is +// achieved by introducing the weights w1 and w2 so that w1 + w2 = 1. In these formulas x +// means base currency (alpha) and y means quote currency (tao). The w1 weight in the code +// below is referred as weight_base, and w2 as weight_quote. Because of the w1 + w2 = 1 +// constraint, only weight_quote is stored, and weight_base is always calculated. +// +// The formulas used for pool operation are following: +// +// Price: p = (w1*y) / (w2*x) +// +// Reserve deltas / (or -1 * payouts) in swaps are computed by: +// +// if ∆x is given (sell) ∆y = y * ((x / (x+∆x))^(w1/w2) - 1) +// if ∆y is given (buy) ∆x = x * ((y / (y+∆y))^(w2/w1) - 1) +// +// When swaps are executing the orders with slippage control, we need to know what amount +// we can swap before the price reaches the limit value of p': +// +// If p' < p (sell): ∆x = x * ((p / p')^w2 - 1) +// If p' < p (buy): ∆y = y * ((p' / p)^w1 - 1) +// +// In order to initialize weights with existing reserve values and price: +// +// w1 = px / (px + y) +// w2 = y / (px + y) +// +// Weights are adjusted when some amounts are added to the reserves. This prevents price +// from changing. +// +// new_w1 = p * (x + ∆x) / (p * (x + ∆x) + y + ∆y) +// new_w2 = (y + ∆y) / (p * (x + ∆x) + y + ∆y) +// +// Weights are limited to stay within [0.1, 0.9] range to avoid precision issues in exponentiation. +// Practically, these limitations will not be achieved, but if they are, the swap will not allow injection +// that will push the weights out of this interval because we prefer chain and swap stability over success +// of a single injection. Currently, we only allow the protocol to inject disproportionally to price, and +// the amount of disproportion will not cause weigths to get far from 0.5. +// + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::pallet_prelude::*; +use safe_bigmath::*; +use safe_math::*; +use sp_arithmetic::Perquintill; +use sp_core::U256; +use sp_runtime::Saturating; +use sp_std::ops::Neg; +use substrate_fixed::types::U64F64; +use subtensor_macros::freeze_struct; + +/// Balancer implements all high complexity math for swap operations such as: +/// - Swapping x for y, which includes limit orders +/// - Adding and removing liquidity (including unbalanced) +/// +/// Notation used in this file: +/// - x: Base reserve (alplha reserve) +/// - y: Quote reserve (tao reserve) +/// - ∆x: Alpha paid in/out +/// - ∆y: Tao paid in/out +/// - w1: Base weight (a.k.a weight_base) +/// - w2: Quote weight (a.k.a weight_quote) +#[freeze_struct("33a4fb0774da77c7")] +#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct Balancer { + quote: Perquintill, +} + +/// Accuracy matches to 18 decimal digits used to represent weights +pub const ACCURACY: u64 = 1_000_000_000_000_000_000_u64; +/// Lower imit of weights is 0.01 +pub const MIN_WEIGHT: Perquintill = Perquintill::from_parts(ACCURACY / 100); +/// 1.0 in Perquintill +pub const ONE: Perquintill = Perquintill::from_parts(ACCURACY); + +#[derive(Debug)] +pub enum BalancerError { + /// The provided weight value is out of range + InvalidValue, +} + +impl Default for Balancer { + /// The default value of weights is 0.5 for pool initialization + fn default() -> Self { + Self { + quote: Perquintill::from_rational(1u128, 2u128), + } + } +} + +impl Balancer { + /// Creates a new instance of balancer with a given quote weight + pub fn new(quote: Perquintill) -> Result { + if Self::check_constraints(quote) { + Ok(Balancer { quote }) + } else { + Err(BalancerError::InvalidValue) + } + } + + /// Constraints limit balancer weights within certain range of values: + /// - Both weights are above minimum + /// - Sum of weights is equal to 1.0 + fn check_constraints(quote: Perquintill) -> bool { + let base = ONE.saturating_sub(quote); + (base >= MIN_WEIGHT) && (quote >= MIN_WEIGHT) + } + + /// We store quote weight as Perquintill + pub fn get_quote_weight(&self) -> Perquintill { + self.quote + } + + /// Base weight is calculated as 1.0 - quote_weight + pub fn get_base_weight(&self) -> Perquintill { + ONE.saturating_sub(self.quote) + } + + /// Sets quote currency weight in the balancer. + /// Because sum of weights is always 1.0, there is no need to + /// store base currency weight + pub fn set_quote_weight(&mut self, new_value: Perquintill) -> Result<(), BalancerError> { + if Self::check_constraints(new_value) { + self.quote = new_value; + Ok(()) + } else { + Err(BalancerError::InvalidValue) + } + } + + /// If base_quote is true, calculate (x / (x + ∆x))^(weight_base / weight_quote), + /// otherwise, calculate (x / (x + ∆x))^(weight_quote / weight_base) + /// + /// Here we use SafeInt from bigmath crate for high-precision exponentiation, + /// which exposes the function pow_ratio_scaled. + /// + /// Note: ∆x may be negative + fn exp_scaled(&self, x: u64, dx: i128, base_quote: bool) -> U64F64 { + let x_plus_dx = if dx >= 0 { + x.saturating_add(dx as u64) + } else { + x.saturating_sub(dx.neg() as u64) + }; + + if x_plus_dx == 0 { + return U64F64::saturating_from_num(0); + } + let w1: u128 = self.get_base_weight().deconstruct() as u128; + let w2: u128 = self.get_quote_weight().deconstruct() as u128; + + let precision = 1024; + let x_safe = SafeInt::from(x); + let w1_safe = SafeInt::from(w1); + let w2_safe = SafeInt::from(w2); + let perquintill_scale = SafeInt::from(ACCURACY as u128); + let denominator = SafeInt::from(x_plus_dx); + log::debug!("x = {:?}", x); + log::debug!("dx = {:?}", dx); + log::debug!("x_safe = {:?}", x_safe); + log::debug!("denominator = {:?}", denominator); + log::debug!("w1_safe = {:?}", w1_safe); + log::debug!("w2_safe = {:?}", w2_safe); + log::debug!("precision = {:?}", precision); + log::debug!("perquintill_scale = {:?}", perquintill_scale); + + let maybe_result_safe_int = if base_quote { + SafeInt::pow_ratio_scaled( + &x_safe, + &denominator, + &w1_safe, + &w2_safe, + precision, + &perquintill_scale, + ) + } else { + SafeInt::pow_ratio_scaled( + &x_safe, + &denominator, + &w2_safe, + &w1_safe, + precision, + &perquintill_scale, + ) + }; + + if let Some(result_safe_int) = maybe_result_safe_int + && let Some(result_u64) = result_safe_int.to_u64() + { + return U64F64::saturating_from_num(result_u64) + .safe_div(U64F64::saturating_from_num(ACCURACY)); + } + U64F64::saturating_from_num(0) + } + + /// Calculates exponent of (x / (x + ∆x)) ^ (w_base/w_quote) + /// This method is used in sell swaps + /// (∆x is given by user, ∆y is paid out by the pool) + pub fn exp_base_quote(&self, x: u64, dx: u64) -> U64F64 { + self.exp_scaled(x, dx as i128, true) + } + + /// Calculates exponent of (y / (y + ∆y)) ^ (w_quote/w_base) + /// This method is used in buy swaps + /// (∆y is given by user, ∆x is paid out by the pool) + pub fn exp_quote_base(&self, y: u64, dy: u64) -> U64F64 { + self.exp_scaled(y, dy as i128, false) + } + + /// Calculates price as (w1/w2) * (y/x), where + /// - w1 is base weight + /// - w2 is quote weight + /// - x is base reserve + /// - y is quote reserve + pub fn calculate_price(&self, x: u64, y: u64) -> U64F64 { + let w2_fixed = U64F64::saturating_from_num(self.get_quote_weight().deconstruct()); + let w1_fixed = U64F64::saturating_from_num(self.get_base_weight().deconstruct()); + let x_fixed = U64F64::saturating_from_num(x); + let y_fixed = U64F64::saturating_from_num(y); + w1_fixed + .safe_div(w2_fixed) + .saturating_mul(y_fixed.safe_div(x_fixed)) + } + + /// Multiply a u128 value by a Perquintill with u128 result rounded to the + /// nearest integer + fn mul_perquintill_round(p: Perquintill, value: u128) -> u128 { + let parts = p.deconstruct() as u128; + let acc = ACCURACY as u128; + + let num = U256::from(value).saturating_mul(U256::from(parts)); + let den = U256::from(acc); + + // Add 0.5 before integer division to achieve rounding to the nearest + // integer + let zero = U256::from(0); + let res = num + .saturating_add(den.checked_div(U256::from(2u8)).unwrap_or(zero)) + .checked_div(den) + .unwrap_or(zero); + res.min(U256::from(u128::MAX)) + .try_into() + .unwrap_or_default() + } + + /// When liquidity is added to balancer swap, it may be added with arbitrary proportion, + /// not necessarily in the proportion of price, like with uniswap v2 or v3. In order to + /// stay within balancer pool invariant, the weights need to be updated. Invariant: + /// + /// L = x ^ weight_base * y ^ weight_quote + /// + /// Note that weights must remain within the proper range (both be above MIN_WEIGHT), + /// so only reasonably small disproportions of updates are appropriate. + pub fn update_weights_for_added_liquidity( + &mut self, + tao_reserve: u64, + alpha_reserve: u64, + tao_delta: u64, + alpha_delta: u64, + ) -> Result<(), BalancerError> { + // Calculate new to-be reserves (do not update here) + let tao_reserve_u128 = u64::from(tao_reserve) as u128; + let alpha_reserve_u128 = u64::from(alpha_reserve) as u128; + let tao_delta_u128 = u64::from(tao_delta) as u128; + let alpha_delta_u128 = u64::from(alpha_delta) as u128; + let new_tao_reserve_u128 = tao_reserve_u128.saturating_add(tao_delta_u128); + let new_alpha_reserve_u128 = alpha_reserve_u128.saturating_add(alpha_delta_u128); + + // Calculate new weights + let quantity_1: u128 = Self::mul_perquintill_round( + self.get_base_weight(), + tao_reserve_u128.saturating_mul(new_alpha_reserve_u128), + ); + let quantity_2: u128 = Self::mul_perquintill_round( + self.get_quote_weight(), + alpha_reserve_u128.saturating_mul(new_tao_reserve_u128), + ); + let q_sum = quantity_1.saturating_add(quantity_2); + + // Calculate new reserve weights + let new_reserve_weight = if q_sum != 0 { + // Both TAO and Alpha are non-zero, normal case + Perquintill::from_rational(quantity_2, q_sum) + } else { + // Either TAO or Alpha reserve were and/or remain zero => Initialize weights to 0.5 + Perquintill::from_rational(1u128, 2u128) + }; + + self.set_quote_weight(new_reserve_weight) + } + + /// Calculates quote delta needed to reach the price up when byuing + /// This method is needed for limit orders. + /// + /// Formula is: + /// ∆y = y * ((price_new / price)^weight_base - 1) + /// price_new >= price + pub fn calculate_quote_delta_in( + &self, + current_price: U64F64, + target_price: U64F64, + reserve: u64, + ) -> u64 { + let base_numerator: u128 = target_price.to_bits(); + let base_denominator: u128 = current_price.to_bits(); + let w1_fixed: u128 = self.get_base_weight().deconstruct() as u128; + let scale: u128 = 10u128.pow(18); + + let maybe_exp_result = SafeInt::pow_ratio_scaled( + &SafeInt::from(base_numerator), + &SafeInt::from(base_denominator), + &SafeInt::from(w1_fixed), + &SafeInt::from(ACCURACY), + 1024, + &SafeInt::from(scale), + ); + + if let Some(exp_result_safe_int) = maybe_exp_result { + let reserve_fixed = U64F64::saturating_from_num(reserve); + let one = U64F64::saturating_from_num(1); + let scale_fixed = U64F64::saturating_from_num(scale); + let exp_result_fixed = if let Some(exp_result_u64) = exp_result_safe_int.to_u64() { + U64F64::saturating_from_num(exp_result_u64) + } else if u64::MAX < exp_result_safe_int { + U64F64::saturating_from_num(u64::MAX) + } else { + U64F64::saturating_from_num(0) + }; + reserve_fixed + .saturating_mul(exp_result_fixed.safe_div(scale_fixed).saturating_sub(one)) + .saturating_to_num::() + } else { + 0u64 + } + } + + /// Calculates base delta needed to reach the price down when selling + /// This method is needed for limit orders. + /// + /// Formula is: + /// ∆x = x * ((price / price_new)^weight_quote - 1) + /// price_new <= price + pub fn calculate_base_delta_in( + &self, + current_price: U64F64, + target_price: U64F64, + reserve: u64, + ) -> u64 { + let base_numerator: u128 = current_price.to_bits(); + let base_denominator: u128 = target_price.to_bits(); + let w2_fixed: u128 = self.get_quote_weight().deconstruct() as u128; + let scale: u128 = 10u128.pow(18); + + let maybe_exp_result = SafeInt::pow_ratio_scaled( + &SafeInt::from(base_numerator), + &SafeInt::from(base_denominator), + &SafeInt::from(w2_fixed), + &SafeInt::from(ACCURACY), + 1024, + &SafeInt::from(scale), + ); + + if let Some(exp_result_safe_int) = maybe_exp_result { + let one = U64F64::saturating_from_num(1); + let scale_fixed = U64F64::saturating_from_num(scale); + let reserve_fixed = U64F64::saturating_from_num(reserve); + let exp_result_fixed = if let Some(exp_result_u64) = exp_result_safe_int.to_u64() { + U64F64::saturating_from_num(exp_result_u64) + } else if u64::MAX < exp_result_safe_int { + U64F64::saturating_from_num(u64::MAX) + } else { + U64F64::saturating_from_num(0) + }; + reserve_fixed + .saturating_mul(exp_result_fixed.safe_div(scale_fixed).saturating_sub(one)) + .saturating_to_num::() + } else { + 0u64 + } + } + + /// Calculates amount of Alpha that needs to be sold to get a given amount of TAO + pub fn get_base_needed_for_quote( + &self, + tao_reserve: u64, + alpha_reserve: u64, + delta_tao: u64, + ) -> u64 { + let e = self.exp_scaled(tao_reserve, (delta_tao as i128).neg(), false); + let one = U64F64::from_num(1); + let alpha_reserve_fixed = U64F64::from_num(alpha_reserve); + // e > 1 in this case + alpha_reserve_fixed + .saturating_mul(e.saturating_sub(one)) + .saturating_to_num::() + } +} + +// cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests --nocapture +#[cfg(test)] +#[allow(clippy::expect_used, clippy::unwrap_used)] +#[cfg(feature = "std")] +mod tests { + use crate::pallet::Balancer; + use crate::pallet::balancer::*; + use approx::assert_abs_diff_eq; + use sp_arithmetic::Perquintill; + use std::panic::{AssertUnwindSafe, catch_unwind}; + + // Helper: convert Perquintill to f64 for comparison + fn perquintill_to_f64(p: Perquintill) -> f64 { + let parts = p.deconstruct() as f64; + parts / ACCURACY as f64 + } + + // Helper: convert U64F64 to f64 for comparison + fn f(v: U64F64) -> f64 { + v.to_num::() + } + + fn assert_no_panic(label: &str, f: F) -> R + where + F: FnOnce() -> R, + { + catch_unwind(AssertUnwindSafe(f)).unwrap_or_else(|_| panic!("{label} panicked")) + } + + #[test] + fn test_balancer_rejects_invalid_boundary_weights_without_panicking() { + [ + Perquintill::zero(), + Perquintill::from_parts(1), + MIN_WEIGHT.saturating_sub(Perquintill::from_parts(1)), + ONE.saturating_sub(MIN_WEIGHT) + .saturating_add(Perquintill::from_parts(1)), + ONE, + ] + .into_iter() + .for_each(|quote| { + assert_no_panic("Balancer::new invalid boundary weight", || { + assert!(Balancer::new(quote).is_err()); + }); + }); + + let mut balancer = Balancer::default(); + assert_no_panic("Balancer::set_quote_weight invalid boundary weight", || { + assert!(balancer.set_quote_weight(Perquintill::zero()).is_err()); + }); + assert_eq!( + balancer.get_quote_weight(), + Perquintill::from_rational(1u128, 2u128) + ); + } + + #[test] + fn test_balancer_extreme_exp_inputs_do_not_panic() { + let weights = [ + MIN_WEIGHT, + Perquintill::from_rational(1u128, 2u128), + ONE.saturating_sub(MIN_WEIGHT), + ]; + let inputs = [ + (0u64, 0u64), + (0u64, 1u64), + (1u64, 0u64), + (1u64, 1u64), + (1u64, u64::MAX), + (u64::MAX, 0u64), + (u64::MAX, 1u64), + (u64::MAX, u64::MAX), + ]; + + for quote in weights { + let balancer = Balancer::new(quote).unwrap(); + for (reserve, delta) in inputs { + assert_no_panic("exp_base_quote extreme input", || { + let _ = balancer.exp_base_quote(reserve, delta); + }); + assert_no_panic("exp_quote_base extreme input", || { + let _ = balancer.exp_quote_base(reserve, delta); + }); + assert_no_panic("exp_scaled negative extreme input", || { + let _ = balancer.exp_scaled(reserve, -(delta as i128), true); + let _ = balancer.exp_scaled(reserve, -(delta as i128), false); + }); + } + } + } + + #[test] + fn test_balancer_price_and_limit_delta_corner_cases_do_not_panic() { + let balancer = Balancer::new(MIN_WEIGHT).unwrap(); + let prices = [ + U64F64::from_num(0), + U64F64::from_num(1), + U64F64::from_num(u64::MAX), + ]; + let reserves = [0u64, 1u64, u64::MAX]; + + for x in reserves { + for y in reserves { + assert_no_panic("calculate_price corner reserves", || { + let _ = balancer.calculate_price(x, y); + }); + } + } + + for current_price in prices { + for target_price in prices { + for reserve in reserves { + assert_no_panic("calculate_quote_delta_in corner input", || { + let _ = + balancer.calculate_quote_delta_in(current_price, target_price, reserve); + }); + assert_no_panic("calculate_base_delta_in corner input", || { + let _ = + balancer.calculate_base_delta_in(current_price, target_price, reserve); + }); + } + } + } + } + + #[test] + fn test_balancer_liquidity_weight_update_extremes_do_not_panic() { + let inputs = [ + (0u64, 0u64, 0u64, 0u64), + (0u64, 0u64, u64::MAX, u64::MAX), + (0u64, u64::MAX, u64::MAX, 0u64), + (u64::MAX, 0u64, 0u64, u64::MAX), + (u64::MAX, u64::MAX, u64::MAX, u64::MAX), + (1u64, u64::MAX, u64::MAX, 1u64), + (u64::MAX, 1u64, 1u64, u64::MAX), + ]; + + for (tao_reserve, alpha_reserve, tao_delta, alpha_delta) in inputs { + let mut balancer = Balancer::default(); + assert_no_panic("update_weights_for_added_liquidity extreme input", || { + let _ = balancer.update_weights_for_added_liquidity( + tao_reserve, + alpha_reserve, + tao_delta, + alpha_delta, + ); + }); + } + } + + #[test] + fn test_balancer_base_needed_for_quote_extremes_do_not_panic() { + let balancer = Balancer::new(ONE.saturating_sub(MIN_WEIGHT)).unwrap(); + let inputs = [ + (0u64, 0u64, 0u64), + (0u64, 1u64, 1u64), + (1u64, 0u64, 1u64), + (1u64, 1u64, 0u64), + (1u64, 1u64, 1u64), + (1u64, 1u64, u64::MAX), + (u64::MAX, u64::MAX, 0u64), + (u64::MAX, u64::MAX, u64::MAX), + ]; + + for (tao_reserve, alpha_reserve, delta_tao) in inputs { + assert_no_panic("get_base_needed_for_quote extreme input", || { + let _ = balancer.get_base_needed_for_quote(tao_reserve, alpha_reserve, delta_tao); + }); + } + } + + #[test] + fn test_safe_bigmath_pow_ratio_internal_paths_do_not_panic() { + let base_num = SafeInt::from(999_999_937u64); + let base_den = SafeInt::from(1_000_000_003u64); + let scale = SafeInt::from(1_000_000u64); + let cases = [ + // Exact integer/root path with exponent values at the safe-bigmath threshold. + ( + SafeInt::from(1024u32), + SafeInt::one(), + "exact max numerator", + ), + ( + SafeInt::from(999u32), + SafeInt::from(1024u32), + "exact root denominator", + ), + // One step over the threshold forces the fixed-point ln/exp fallback path. + (SafeInt::from(1025u32), SafeInt::one(), "fallback numerator"), + ( + SafeInt::from(999u32), + SafeInt::from(1025u32), + "fallback denominator", + ), + // GCD reduction should route this back to the exact path. + ( + SafeInt::from(2048u32), + SafeInt::from(4096u32), + "gcd reduced", + ), + ]; + + for (exp_num, exp_den, label) in cases { + let result = assert_no_panic(label, || { + SafeInt::pow_ratio_scaled(&base_num, &base_den, &exp_num, &exp_den, 64, &scale) + }); + assert!(result.is_some(), "{label} should produce a result"); + } + } + + #[test] + fn test_balancer_near_equal_weights_with_tiny_delta_do_not_panic() { + let weights = [ + Perquintill::from_parts(500_000_000_500_000_000), + Perquintill::from_parts(499_999_999_500_000_000), + Perquintill::from_parts(500_000_000_000_500_000), + Perquintill::from_parts(499_999_999_999_500_000), + ]; + let reserve = 21_000_000_000_000_000u64; + let tiny_deltas = [1u64, 100u64, 100_000u64]; + + for quote in weights { + let balancer = Balancer::new(quote).unwrap(); + for delta in tiny_deltas { + assert_no_panic("near-equal exp_base_quote tiny delta", || { + let e = balancer.exp_base_quote(reserve, delta); + assert!(e <= U64F64::from_num(1)); + assert!(e > U64F64::from_num(0)); + }); + assert_no_panic("near-equal exp_quote_base tiny delta", || { + let e = balancer.exp_quote_base(reserve, delta); + assert!(e <= U64F64::from_num(1)); + assert!(e > U64F64::from_num(0)); + }); + } + } + } + + #[test] + fn test_balancer_log_normalization_reserve_shapes_do_not_panic() { + let balancer = Balancer::new(Perquintill::from_parts(500_000_000_500_000_000)).unwrap(); + let reserves = [ + (1u64 << 42) - 1, + 1u64 << 42, + (1u64 << 42) + 1, + ((1u64 << 42) + (1u64 << 41)) - 1, + (1u64 << 42) + (1u64 << 41), + ((1u64 << 42) + (1u64 << 41)) + 1, + ]; + + for reserve in reserves { + for delta in [1u64, reserve / 1_000, reserve / 2] { + assert_no_panic("log-normalization exp_base_quote", || { + let e = balancer.exp_base_quote(reserve, delta); + assert!(e <= U64F64::from_num(1)); + }); + assert_no_panic("log-normalization exp_quote_base", || { + let e = balancer.exp_quote_base(reserve, delta); + assert!(e <= U64F64::from_num(1)); + }); + } + } + } + + #[test] + fn test_perquintill_power() { + const PRECISION: u32 = 4096; + const PERQUINTILL: u128 = ACCURACY as u128; + + let x = SafeInt::from(21_000_000_000_000_000u64); + let delta = SafeInt::from(7_000_000_000_000_000u64); + let w1 = SafeInt::from(600_000_000_000_000_000u128); + let w2 = SafeInt::from(400_000_000_000_000_000u128); + let denominator = &x + δ + assert_eq!(w1.clone() + w2.clone(), SafeInt::from(PERQUINTILL)); + + let perquintill_result = SafeInt::pow_ratio_scaled( + &x, + &denominator, + &w1, + &w2, + PRECISION, + &SafeInt::from(PERQUINTILL), + ) + .expect("perquintill integer result"); + + assert_eq!( + perquintill_result, + SafeInt::from(649_519_052_838_328_985u128) + ); + let readable = safe_bigmath::SafeDec::<18>::from_raw(perquintill_result); + assert_eq!(format!("{}", readable), "0.649519052838328985"); + } + + /// Validate realistic values that can be calculated with f64 precision + #[test] + fn test_exp_base_quote_happy_path() { + // Outer test cases: w_quote + [ + Perquintill::from_rational(500_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_000_000_001_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(499_999_999_999_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_000_000_100_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_000_001_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_000_010_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_000_100_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_001_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_010_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(500_100_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(501_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(510_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(100_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(100_000_000_001_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(200_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(300_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(400_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(600_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(700_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(800_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(899_999_999_999_u128, 1_000_000_000_000_u128), + Perquintill::from_rational(900_000_000_000_u128, 1_000_000_000_000_u128), + Perquintill::from_rational( + 102_337_248_363_782_924_u128, + 1_000_000_000_000_000_000_u128, + ), + ] + .into_iter() + .for_each(|w_quote| { + // Inner test cases: y, x, ∆x + [ + (1_000_u64, 1_000_u64, 0_u64), + (1_000_u64, 1_000_u64, 1_u64), + (1_500_u64, 1_000_u64, 1_u64), + ( + 1_000_000_000_000_u64, + 100_000_000_000_000_u64, + 100_000_000_u64, + ), + ( + 1_000_000_000_000_u64, + 100_000_000_000_000_u64, + 100_000_000_u64, + ), + ( + 100_000_000_000_u64, + 100_000_000_000_000_u64, + 100_000_000_u64, + ), + (100_000_000_000_u64, 100_000_000_000_000_u64, 1_000_000_u64), + ( + 100_000_000_000_u64, + 100_000_000_000_000_u64, + 1_000_000_000_000_u64, + ), + ( + 1_000_000_000_u64, + 100_000_000_000_000_u64, + 1_000_000_000_000_u64, + ), + ( + 1_000_000_u64, + 100_000_000_000_000_u64, + 1_000_000_000_000_u64, + ), + (1_000_u64, 100_000_000_000_000_u64, 1_000_000_000_000_u64), + (1_000_u64, 100_000_000_000_000_u64, 1_000_000_000_u64), + (1_000_u64, 100_000_000_000_000_u64, 1_000_000_u64), + (1_000_u64, 100_000_000_000_000_u64, 1_000_u64), + (1_000_u64, 100_000_000_000_000_u64, 100_000_000_000_000_u64), + (10_u64, 100_000_000_000_000_u64, 100_000_000_000_000_u64), + // Extreme values of ∆x for small x + (1_000_000_000_u64, 4_000_000_000_u64, 1_000_000_000_000_u64), + (1_000_000_000_000_u64, 1_000_u64, 1_000_000_000_000_u64), + ( + 5_628_038_062_729_553_u64, + 400_775_553_u64, + 14_446_633_907_665_582_u64, + ), + ( + 5_600_000_000_000_000_u64, + 400_000_000_u64, + 14_000_000_000_000_000_u64, + ), + ] + .into_iter() + .for_each(|(y, x, dx)| { + let bal = Balancer::new(w_quote).unwrap(); + let e1 = bal.exp_base_quote(x, dx); + let e2 = bal.exp_quote_base(x, dx); + let one = U64F64::from_num(1); + let y_fixed = U64F64::from_num(y); + let dy1 = y_fixed * (one - e1); + let dy2 = y_fixed * (one - e2); + + let w1 = perquintill_to_f64(bal.get_base_weight()); + let w2 = perquintill_to_f64(bal.get_quote_weight()); + let e1_expected = (x as f64 / (x as f64 + dx as f64)).powf(w1 / w2); + let dy1_expected = y as f64 * (1. - e1_expected); + let e2_expected = (x as f64 / (x as f64 + dx as f64)).powf(w2 / w1); + let dy2_expected = y as f64 * (1. - e2_expected); + + // Start tolerance with 0.001 rao + let mut eps1 = 0.001; + let mut eps2 = 0.001; + + // If swapping more than 100k tao/alpha, relax tolerance to 1.0 rao + if dy1_expected > 100_000_000_000_000_f64 { + eps1 = 1.0; + } + if dy2_expected > 100_000_000_000_000_f64 { + eps2 = 1.0; + } + assert_abs_diff_eq!(f(dy1), dy1_expected, epsilon = eps1); + assert_abs_diff_eq!(f(dy2), dy2_expected, epsilon = eps2); + }) + }); + } + + /// This test exercises practical application edge cases of exp_base_quote + /// The practical formula where this function is used: + /// ∆y = y * (exp_base_quote(x, ∆x) - 1) + /// + /// The test validates that two different sets of parameters produce (sensibly) + /// different results + /// + #[test] + fn test_exp_base_quote_dy_precision() { + // Test cases: y, x1, ∆x1, w_quote1, x2, ∆x2, w_quote2 + // Realized dy1 should be greater than dy2 + [ + ( + 1_000_000_000_u64, + 21_000_000_000_000_000_u64, + 21_000_000_000_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + 21_000_000_000_000_000_u64, + 21_000_000_000_u64, + Perquintill::from_rational(1_000_000_000_001_u128, 2_000_000_000_000_u128), + ), + ( + 1_000_000_000_u64, + 21_000_000_000_000_000_u64, + 21_000_000_000_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_001_u128), + 21_000_000_000_000_000_u64, + 21_000_000_000_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + ), + ( + 1_000_000_000_u64, + 21_000_000_000_000_000_u64, + 2_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + 21_000_000_000_000_000_u64, + 1_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + ), + ( + 1_000_000_000_u64, + 21_000_000_000_000_000_u64, + 1_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + 21_000_000_000_000_000_u64, + 1_u64, + Perquintill::from_rational(1_010_000_000_000_u128, 2_000_000_000_000_u128), + ), + ( + 1_000_000_000_u64, + 21_000_000_000_000_000_u64, + 1_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_010_000_000_000_u128), + 21_000_000_000_000_000_u64, + 1_u64, + Perquintill::from_rational(1_000_000_000_000_u128, 2_000_000_000_000_u128), + ), + ] + .into_iter() + .for_each(|(y, x1, dx1, w_quote1, x2, dx2, w_quote2)| { + let bal1 = Balancer::new(w_quote1).unwrap(); + let bal2 = Balancer::new(w_quote2).unwrap(); + + let exp1 = bal1.exp_base_quote(x1, dx1); + let exp2 = bal2.exp_base_quote(x2, dx2); + + let one = U64F64::from_num(1); + let y_fixed = U64F64::from_num(y); + let dy1 = y_fixed * (one - exp1); + let dy2 = y_fixed * (one - exp2); + + assert!(dy1 > dy2); + + let zero = U64F64::from_num(0); + assert!(dy1 != zero); + assert!(dy2 != zero); + }) + } + + /// Test the broad range of w_quote values, usually should be ignored + #[ignore] + #[test] + fn test_exp_quote_broad_range() { + let y = 1_000_000_000_000_u64; + let x = 100_000_000_000_000_u64; + let dx = 10_000_000_u64; + + let mut prev = U64F64::from_num(1_000_000_000); + let mut last_progress = 0.; + let start = 100_000_000_000_u128; + let stop = 900_000_000_000_u128; + for num in (start..=stop).step_by(1000_usize) { + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + let e = bal.exp_base_quote(x, dx); + + let one = U64F64::from_num(1); + let dy = U64F64::from_num(y) * (one - e); + + let progress = (num as f64 - start as f64) / (stop as f64 - start as f64); + if progress - last_progress >= 0.0001 { + // Replace with println for real-time progress + log::debug!("progress = {:?}%", progress * 100.); + log::debug!("dy = {:?}", dy); + last_progress = progress; + } + + assert!(dy != U64F64::from_num(0)); + assert!(dy <= prev); + prev = dy; + } + } + + #[ignore] + #[test] + fn test_exp_quote_fuzzy() { + use rand::rngs::StdRng; + use rand::{Rng, SeedableRng}; + use rayon::prelude::*; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; + + const ITERATIONS: usize = 1_000_000_000; + let counter = Arc::new(AtomicUsize::new(0)); + + (0..ITERATIONS) + .into_par_iter() + .for_each(|i| { + // Each iteration gets its own deterministic RNG. + // Seed depends on i, so runs are reproducible. + let mut rng = StdRng::seed_from_u64(42 + i as u64); + let max_supply: u64 = 21_000_000_000_000_000; + let full_range = true; + + let x: u64 = rng.gen_range(1_000..=max_supply); // Alpha reserve + let y: u64 = if full_range { + // TAO reserve (allow huge prices) + rng.gen_range(1_000..=max_supply) + } else { + // TAO reserve (limit prices with 0-1000) + rng.gen_range(1_000..x.saturating_mul(1000).min(max_supply)) + }; + let dx: u64 = if full_range { + // Alhpa sold (allow huge values) + rng.gen_range(1_000..=21_000_000_000_000_000) + } else { + // Alhpa sold (do not sell more than 100% of what's in alpha reserve) + rng.gen_range(1_000..=x) + }; + let w_numerator: u64 = rng.gen_range(ACCURACY / 10..=ACCURACY / 10 * 9); + let w_quote = Perquintill::from_rational(w_numerator, ACCURACY); + + let bal = Balancer::new(w_quote).unwrap(); + let e = bal.exp_base_quote(x, dx); + + let one = U64F64::from_num(1); + let dy = U64F64::from_num(y) * (one - e); + + // Calculate expected in f64 and approx-assert + let w1 = perquintill_to_f64(bal.get_base_weight()); + let w2 = perquintill_to_f64(bal.get_quote_weight()); + let e_expected = (x as f64 / (x as f64 + dx as f64)).powf(w1 / w2); + let dy_expected = y as f64 * (1. - e_expected); + + let actual = dy.to_num::(); + let eps = (dy_expected / 1_000_000.).clamp(1.0, 1000.0); + + assert!( + (actual - dy_expected).abs() <= eps, + "dy mismatch:\n actual: {}\n expected: {}\n eps: {}\nParameters:\n x: {}\n y: {}\n dx: {}\n w_numerator: {}\n", + actual, dy_expected, eps, x, y, dx, w_numerator, + ); + + // Assert that we aren't giving out more than reserve y + assert!(dy <= y, "dy = {},\ny = {}", dy, y,); + + // Print progress + let done = counter.fetch_add(1, Ordering::Relaxed) + 1; + if done % 100_000_000 == 0 { + let progress = done as f64 / ITERATIONS as f64 * 100.0; + // Replace with println for real-time progress + log::debug!("progress = {progress:.4}%"); + } + }); + } + + #[test] + fn test_calculate_quote_delta_in() { + let num = 250_000_000_000_u128; // w1 = 0.75 + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + let current_price: U64F64 = U64F64::from_num(0.1); + let target_price: U64F64 = U64F64::from_num(0.2); + let tao_reserve: u64 = 1_000_000_000; + + let dy = bal.calculate_quote_delta_in(current_price, target_price, tao_reserve); + + // ∆y = y•[(p'/p)^w1 - 1] + let dy_expected = tao_reserve as f64 + * ((target_price.to_num::() / current_price.to_num::()).powf(0.75) - 1.0); + + assert_eq!(dy, dy_expected as u64,); + } + + #[test] + fn test_calculate_base_delta_in() { + let num = 250_000_000_000_u128; // w2 = 0.25 + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + let current_price: U64F64 = U64F64::from_num(0.2); + let target_price: U64F64 = U64F64::from_num(0.1); + let alpha_reserve: u64 = 1_000_000_000; + + let dx = bal.calculate_base_delta_in(current_price, target_price, alpha_reserve); + + // ∆x = x•[(p/p')^w2 - 1] + let dx_expected = alpha_reserve as f64 + * ((current_price.to_num::() / target_price.to_num::()).powf(0.25) - 1.0); + + assert_eq!(dx, dx_expected as u64,); + } + + #[test] + fn test_calculate_quote_delta_in_impossible() { + let num = 250_000_000_000_u128; // w1 = 0.75 + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + // Impossible price (lower) + let current_price: U64F64 = U64F64::from_num(0.1); + let target_price: U64F64 = U64F64::from_num(0.05); + let tao_reserve: u64 = 1_000_000_000; + + let dy = bal.calculate_quote_delta_in(current_price, target_price, tao_reserve); + let dy_expected = 0u64; + + assert_eq!(dy, dy_expected); + } + + #[test] + fn test_calculate_base_delta_in_impossible() { + let num = 250_000_000_000_u128; // w2 = 0.25 + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + // Impossible price (higher) + let current_price: U64F64 = U64F64::from_num(0.1); + let target_price: U64F64 = U64F64::from_num(0.2); + let alpha_reserve: u64 = 1_000_000_000; + + let dx = bal.calculate_base_delta_in(current_price, target_price, alpha_reserve); + let dx_expected = 0u64; + + assert_eq!(dx, dx_expected); + } + + #[test] + fn test_calculate_delta_in_reverse_swap() { + let num = 500_000_000_000_u128; + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + let current_price: U64F64 = U64F64::from_num(0.1); + let target_price: U64F64 = U64F64::from_num(0.2); + let tao_reserve: u64 = 1_000_000_000; + + // Here is the simple case of w1 = w2 = 0.5, so alpha = tao / price + let alpha_reserve: u64 = (tao_reserve as f64 / current_price.to_num::()) as u64; + + let dy = bal.calculate_quote_delta_in(current_price, target_price, tao_reserve); + let dx = alpha_reserve as f64 + * (1.0 + - (tao_reserve as f64 / (tao_reserve as f64 + dy as f64)) + .powf(num as f64 / (1_000_000_000_000 - num) as f64)); + + // Verify that buying with dy will in fact bring the price to target_price + let actual_price = bal.calculate_price(alpha_reserve - dx as u64, tao_reserve + dy); + assert_abs_diff_eq!( + actual_price.to_num::(), + target_price.to_num::(), + epsilon = target_price.to_num::() / 1_000_000_000. + ); + } + + #[test] + fn test_mul_round_zero_and_one() { + let v = 1_000_000u128; + + // p = 0 -> always 0 + assert_eq!(Balancer::mul_perquintill_round(Perquintill::zero(), v), 0); + + // p = 1 -> identity + assert_eq!(Balancer::mul_perquintill_round(Perquintill::one(), v), v); + } + + #[test] + fn test_mul_round_half_behaviour() { + // p = 1/2 + let p = Perquintill::from_rational(1u128, 2u128); + + // Check rounding around .5 boundaries + // value * 1/2, rounded to nearest + assert_eq!(Balancer::mul_perquintill_round(p, 0), 0); // 0.0 -> 0 + assert_eq!(Balancer::mul_perquintill_round(p, 1), 1); // 0.5 -> 1 (round up) + assert_eq!(Balancer::mul_perquintill_round(p, 2), 1); // 1.0 -> 1 + assert_eq!(Balancer::mul_perquintill_round(p, 3), 2); // 1.5 -> 2 + assert_eq!(Balancer::mul_perquintill_round(p, 4), 2); // 2.0 -> 2 + assert_eq!(Balancer::mul_perquintill_round(p, 5), 3); // 2.5 -> 3 + assert_eq!(Balancer::mul_perquintill_round(p, 1023), 512); // 511.5 -> 512 + assert_eq!(Balancer::mul_perquintill_round(p, 1025), 513); // 512.5 -> 513 + } + + #[test] + fn test_mul_round_third_behaviour() { + // p = 1/3 + let p = Perquintill::from_rational(1u128, 3u128); + + // value * 1/3, rounded to nearest + assert_eq!(Balancer::mul_perquintill_round(p, 3), 1); // 1.0 -> 1 + assert_eq!(Balancer::mul_perquintill_round(p, 4), 1); // 1.333... -> 1 + assert_eq!(Balancer::mul_perquintill_round(p, 5), 2); // 1.666... -> 2 + assert_eq!(Balancer::mul_perquintill_round(p, 6), 2); // 2.0 -> 2 + } + + #[test] + fn test_mul_round_large_values_simple_rational() { + // p = 7/10 (exact in perquintill: 0.7) + let p = Perquintill::from_rational(7u128, 10u128); + let v: u128 = 1_000_000_000_000_000_000; + + let res = Balancer::mul_perquintill_round(p, v); + + // Expected = round(0.7 * v) with pure integer math: + // round(v * 7 / 10) = (v*7 + 10/2) / 10 + let expected = (v.saturating_mul(7) + 10 / 2) / 10; + + assert_eq!(res, expected); + } + + #[test] + fn test_mul_round_max_value_with_one() { + let v = u128::MAX; + let p = ONE; + + // For p = 1, result must be exactly value, and must not overflow + let res = Balancer::mul_perquintill_round(p, v); + assert_eq!(res, v); + } + + #[test] + fn test_price_with_equal_weights_is_y_over_x() { + // quote = 0.5, base = 0.5 -> w1 / w2 = 1, so price = y/x + let quote = Perquintill::from_rational(1u128, 2u128); + let bal = Balancer::new(quote).unwrap(); + + let x = 2u64; + let y = 5u64; + + let price = bal.calculate_price(x, y); + let price_f = f(price); + + let expected_f = (y as f64) / (x as f64); + assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-12); + } + + #[test] + fn test_price_scales_with_weight_ratio_two_to_one() { + // Assume base = 1 - quote. + // quote = 1/3 -> base = 2/3, so w1 / w2 = 2. + // Then price = 2 * (y/x). + let quote = Perquintill::from_rational(1u128, 3u128); + let bal = Balancer::new(quote).unwrap(); + + let x = 4u64; + let y = 10u64; + + let price_f = f(bal.calculate_price(x, y)); + let expected_f = 2.0 * (y as f64 / x as f64); + + assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-10); + } + + #[test] + fn test_price_is_zero_when_y_is_zero() { + // If y = 0, y/x = 0 so price must be 0 regardless of weights (for x > 0). + let quote = Perquintill::from_rational(3u128, 10u128); // 0.3 + let bal = Balancer::new(quote).unwrap(); + + let x = 10u64; + let y = 0u64; + + let price_f = f(bal.calculate_price(x, y)); + assert_abs_diff_eq!(price_f, 0.0, epsilon = 0.0); + } + + #[test] + fn test_price_invariant_when_scaling_x_and_y_with_equal_weights() { + // For equal weights, price(x, y) == price(kx, ky). + let quote = Perquintill::from_rational(1u128, 2u128); // 0.5 + let bal = Balancer::new(quote).unwrap(); + + let x1 = 3u64; + let y1 = 7u64; + let k = 10u64; + let x2 = x1 * k; + let y2 = y1 * k; + + let p1 = f(bal.calculate_price(x1, y1)); + let p2 = f(bal.calculate_price(x2, y2)); + + assert_abs_diff_eq!(p1, p2, epsilon = 1e-12); + } + + #[test] + fn test_price_matches_formula_for_general_quote() { + // General check: price = (w1 / w2) * (y/x), + // where w1 = base_weight, w2 = quote_weight. + // Here we assume get_base_weight = 1 - quote. + let quote = Perquintill::from_rational(2u128, 5u128); // 0.4 + let bal = Balancer::new(quote).unwrap(); + + let x = 9u64; + let y = 25u64; + + let price_f = f(bal.calculate_price(x, y)); + + let base = Perquintill::one() - quote; + let w1 = base.deconstruct() as f64; + let w2 = quote.deconstruct() as f64; + + let expected_f = (w1 / w2) * (y as f64 / x as f64); + assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-9); + } + + #[test] + fn test_price_high_values_non_equal_weights() { + // Non-equal weights, high x and y (up to 21e15) + let quote = Perquintill::from_rational(3u128, 10u128); // 0.3 + let bal = Balancer::new(quote).unwrap(); + + let x: u64 = 21_000_000_000_000_000; + let y: u64 = 15_000_000_000_000_000; + + let price = bal.calculate_price(x, y); + let price_f = f(price); + + // Expected: (w1 / w2) * (y / x), using Balancer's actual weights + let w1 = bal.get_base_weight().deconstruct() as f64; + let w2 = bal.get_quote_weight().deconstruct() as f64; + let expected_f = (w1 / w2) * (y as f64 / x as f64); + + assert_abs_diff_eq!(price_f, expected_f, epsilon = 1e-9); + } + + // cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests::test_exp_scaled --exact --nocapture + #[test] + fn test_exp_scaled() { + [ + // base_weight_numerator, base_weight_denominator, reserve, d_reserve, base_quote + (5_u64, 10_u64, 100000_u64, 100_u64, true, 0.999000999000999), + (1_u64, 4_u64, 500000_u64, 5000_u64, true, 0.970590147927644), + (3_u64, 4_u64, 200000_u64, 2000_u64, false, 0.970590147927644), + ( + 9_u64, + 10_u64, + 13513642_u64, + 1673_u64, + false, + 0.998886481979889, + ), + ( + 773_u64, + 1000_u64, + 7_000_000_000_u64, + 10_000_u64, + true, + 0.999999580484586, + ), + ] + .into_iter() + .map(|v| { + ( + Perquintill::from_rational(v.0, v.1), + v.2, + v.3, + v.4, + U64F64::from_num(v.5), + ) + }) + .for_each(|(quote_weight, reserve, d_reserve, base_quote, expected)| { + let balancer = Balancer::new(quote_weight).unwrap(); + let result = balancer.exp_scaled(reserve, d_reserve as i128, base_quote); + assert_abs_diff_eq!( + result.to_num::(), + expected.to_num::(), + epsilon = 0.000000001 + ); + }); + } + + // cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests::test_base_needed_for_quote --exact --nocapture + #[test] + fn test_base_needed_for_quote() { + let num = 250_000_000_000_u128; // w1 = 0.75 + let w_quote = Perquintill::from_rational(num, 1_000_000_000_000_u128); + let bal = Balancer::new(w_quote).unwrap(); + + let tao_reserve: u64 = 1_000_000_000; + let alpha_reserve: u64 = 1_000_000_000; + let tao_delta: u64 = 1_123_432; // typical fee range + + let dx = bal.get_base_needed_for_quote(tao_reserve, alpha_reserve, tao_delta); + + // ∆x = x•[(y/(y+∆y))^(w2/w1) - 1] + let dx_expected = tao_reserve as f64 + * ((tao_reserve as f64 / ((tao_reserve - tao_delta) as f64)).powf(0.25 / 0.75) - 1.0); + + assert_eq!(dx, dx_expected as u64,); + } +} diff --git a/pallets/swap/src/pallet/hooks.rs b/pallets/swap/src/pallet/hooks.rs new file mode 100644 index 0000000000..90989d5f52 --- /dev/null +++ b/pallets/swap/src/pallet/hooks.rs @@ -0,0 +1,30 @@ +use frame_support::pallet_macros::pallet_section; + +#[pallet_section] +mod hooks { + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(block_number: BlockNumberFor) -> Weight { + Weight::from_parts(0, 0) + } + + fn on_finalize(_block_number: BlockNumberFor) {} + + fn on_runtime_upgrade() -> Weight { + // --- Migrate storage + let mut weight = Weight::from_parts(0, 0); + + weight = weight + // Cleanup uniswap v3 and migrate to balancer + .saturating_add( + migrations::migrate_swapv3_to_balancer::migrate_swapv3_to_balancer::(), + ); + weight + } + + #[cfg(feature = "try-runtime")] + fn try_state(_n: BlockNumberFor) -> Result<(), sp_runtime::TryRuntimeError> { + Ok(()) + } + } +} diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index 7be087c0f0..c3e0b2f1d3 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -1,216 +1,118 @@ -use core::ops::Neg; - -use frame_support::dispatch::DispatchResultWithPostInfo; use frame_support::storage::{TransactionOutcome, transactional}; -use frame_support::{ensure, pallet_prelude::DispatchError, traits::Get, weights::Weight}; +use frame_support::{ + ensure, + pallet_prelude::{DispatchError, Zero}, + traits::Get, +}; use safe_math::*; -use sp_arithmetic::{helpers_128bit, traits::Zero}; +use sp_arithmetic::Perquintill; use sp_runtime::{DispatchResult, traits::AccountIdConversion}; -use substrate_fixed::types::{I64F64, U64F64, U96F32}; -use subtensor_runtime_common::{ - AlphaBalance, BalanceOps, NetUid, SubnetInfo, TaoBalance, Token, TokenReserve, -}; - -use super::pallet::*; -use super::swap_step::{BasicSwapStep, SwapStep, SwapStepAction}; -use crate::{ - SqrtPrice, - position::{Position, PositionId}, - tick::{ActiveTickIndexManager, Tick, TickIndex}, -}; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::{AlphaBalance, NetUid, SubnetInfo, TaoBalance, Token, TokenReserve}; use subtensor_swap_interface::{ DefaultPriceLimit, Order as OrderT, SwapEngine, SwapHandler, SwapResult, }; -const MAX_SWAP_ITERATIONS: u16 = 1000; - -#[derive(Debug, PartialEq)] -pub struct UpdateLiquidityResult { - pub tao: TaoBalance, - pub alpha: AlphaBalance, - pub fee_tao: TaoBalance, - pub fee_alpha: AlphaBalance, - pub removed: bool, - pub tick_low: TickIndex, - pub tick_high: TickIndex, -} - -#[derive(Debug, PartialEq)] -pub struct RemoveLiquidityResult { - pub tao: TaoBalance, - pub alpha: AlphaBalance, - pub fee_tao: TaoBalance, - pub fee_alpha: AlphaBalance, - pub tick_low: TickIndex, - pub tick_high: TickIndex, - pub liquidity: u64, -} +use super::pallet::*; +use super::swap_step::{BasicSwapStep, SwapStep}; +use crate::{pallet::Balancer, pallet::balancer::BalancerError}; impl Pallet { - pub fn current_price(netuid: NetUid) -> U96F32 { + pub fn current_price(netuid: NetUid) -> U64F64 { match T::SubnetInfo::mechanism(netuid.into()) { 1 => { - if SwapV3Initialized::::get(netuid) { - let sqrt_price = AlphaSqrtPrice::::get(netuid); - U96F32::saturating_from_num(sqrt_price.saturating_mul(sqrt_price)) - } else { + let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); + if !alpha_reserve.is_zero() { let tao_reserve = T::TaoReserve::reserve(netuid.into()); - let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); - if !alpha_reserve.is_zero() { - U96F32::saturating_from_num(tao_reserve) - .saturating_div(U96F32::saturating_from_num(alpha_reserve)) - } else { - U96F32::saturating_from_num(0) - } + let balancer = SwapBalancer::::get(netuid); + balancer.calculate_price(alpha_reserve.into(), tao_reserve.into()) + } else { + U64F64::saturating_from_num(0) } } - _ => U96F32::saturating_from_num(1), + _ => U64F64::saturating_from_num(1), } } - // initializes V3 swap for a subnet if needed - pub fn maybe_initialize_v3(netuid: NetUid) -> Result<(), Error> { - if SwapV3Initialized::::get(netuid) { + // initializes pal-swap (balancer) for a subnet if needed + pub fn maybe_initialize_palswap( + netuid: NetUid, + maybe_price: Option, + ) -> Result<(), Error> { + if PalSwapInitialized::::get(netuid) { return Ok(()); } - // Initialize the v3: - // Reserves are re-purposed, nothing to set, just query values for liquidity and price - // calculation + // Query reserves let tao_reserve = T::TaoReserve::reserve(netuid.into()); let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); - // Set price - let price = U64F64::saturating_from_num(tao_reserve) - .safe_div(U64F64::saturating_from_num(alpha_reserve)); - - let epsilon = U64F64::saturating_from_num(0.000000000001); - - let current_sqrt_price = price.checked_sqrt(epsilon).unwrap_or(U64F64::from_num(0)); - AlphaSqrtPrice::::set(netuid, current_sqrt_price); - - // Set current tick - let current_tick = TickIndex::from_sqrt_price_bounded(current_sqrt_price); - CurrentTick::::set(netuid, current_tick); - - // Set initial (protocol owned) liquidity and positions - // Protocol liquidity makes one position from TickIndex::MIN to TickIndex::MAX - // We are using the sp_arithmetic sqrt here, which works for u128 - let liquidity = helpers_128bit::sqrt( - (tao_reserve.to_u64() as u128).saturating_mul(alpha_reserve.to_u64() as u128), - ) as u64; - let protocol_account_id = Self::protocol_account_id(); - - let (position, _, _) = Self::add_liquidity_not_insert( - netuid, - &protocol_account_id, - TickIndex::MIN, - TickIndex::MAX, - liquidity, - )?; + // Create balancer based on price + let balancer = Balancer::new(if let Some(price) = maybe_price { + // Price is given, calculate weights: + // w_quote = y / (px + y) + let px_high = (price.saturating_to_num::() as u128) + .saturating_mul(u64::from(alpha_reserve) as u128); + let px_low = U64F64::saturating_from_num(alpha_reserve) + .saturating_mul(price.frac()) + .saturating_to_num::(); + let px_plus_y = px_high + .saturating_add(px_low) + .saturating_add(u64::from(tao_reserve) as u128); + + // If price is given and both reserves are zero, the swap doesn't initialize + if px_plus_y == 0u128 { + return Err(Error::::ReservesOutOfBalance); + } + Perquintill::from_rational(u64::from(tao_reserve) as u128, px_plus_y) + } else { + // No price = insert 0.5 into SwapBalancer + Perquintill::from_rational(1_u64, 2_u64) + }) + .map_err(|err| match err { + BalancerError::InvalidValue => Error::::ReservesOutOfBalance, + })?; + SwapBalancer::::insert(netuid, balancer.clone()); - Positions::::insert(&(netuid, protocol_account_id, position.id), position); + PalSwapInitialized::::insert(netuid, true); Ok(()) } - pub(crate) fn get_proportional_alpha_tao_and_remainders( - sqrt_alpha_price: U64F64, - amount_tao: TaoBalance, - amount_alpha: AlphaBalance, - ) -> (TaoBalance, AlphaBalance, TaoBalance, AlphaBalance) { - let price = sqrt_alpha_price.saturating_mul(sqrt_alpha_price); - let tao_equivalent: u64 = U64F64::saturating_from_num(u64::from(amount_alpha)) - .saturating_mul(price) - .saturating_to_num(); - let amount_tao_u64 = u64::from(amount_tao); - - if tao_equivalent <= amount_tao_u64 { - // Too much or just enough TAO - ( - tao_equivalent.into(), - amount_alpha, - amount_tao.saturating_sub(TaoBalance::from(tao_equivalent)), - 0.into(), - ) - } else { - // Too much Alpha - let alpha_equivalent: u64 = U64F64::saturating_from_num(u64::from(amount_tao)) - .safe_div(price) - .saturating_to_num(); - ( - amount_tao, - alpha_equivalent.into(), - 0.into(), - u64::from(amount_alpha) - .saturating_sub(alpha_equivalent) - .into(), - ) - } - } - - /// Adjusts protocol liquidity with new values of TAO and Alpha reserve + /// Returns actually added Tao and Alpha, which may be zero in case + /// of a high disbalance pub(super) fn adjust_protocol_liquidity( netuid: NetUid, tao_delta: TaoBalance, alpha_delta: AlphaBalance, - ) { - // Update protocol position with new liquidity - let protocol_account_id = Self::protocol_account_id(); - let mut positions = - Positions::::iter_prefix_values((netuid, protocol_account_id.clone())) - .collect::>(); - - if let Some(position) = positions.get_mut(0) { - // Claim protocol fees and add them to liquidity - let (tao_fees, alpha_fees) = position.collect_fees(); - - // Add fee reservoirs and get proportional amounts - let current_sqrt_price = AlphaSqrtPrice::::get(netuid); - let tao_reservoir = ScrapReservoirTao::::get(netuid); - let alpha_reservoir = ScrapReservoirAlpha::::get(netuid); - let (corrected_tao_delta, corrected_alpha_delta, tao_scrap, alpha_scrap) = - Self::get_proportional_alpha_tao_and_remainders( - current_sqrt_price, - tao_delta - .saturating_add(TaoBalance::from(tao_fees)) - .saturating_add(tao_reservoir), - alpha_delta - .saturating_add(AlphaBalance::from(alpha_fees)) - .saturating_add(alpha_reservoir), - ); - - // Update scrap reservoirs - ScrapReservoirTao::::insert(netuid, tao_scrap); - ScrapReservoirAlpha::::insert(netuid, alpha_scrap); - - // Adjust liquidity - let maybe_token_amounts = position.to_token_amounts(current_sqrt_price); - if let Ok((tao, alpha)) = maybe_token_amounts { - // Get updated reserves, calculate liquidity - let new_tao_reserve = tao.saturating_add(corrected_tao_delta.to_u64()); - let new_alpha_reserve = alpha.saturating_add(corrected_alpha_delta.to_u64()); - let new_liquidity = helpers_128bit::sqrt( - (new_tao_reserve as u128).saturating_mul(new_alpha_reserve as u128), - ) as u64; - let liquidity_delta = new_liquidity.saturating_sub(position.liquidity); - - // Update current liquidity - CurrentLiquidity::::mutate(netuid, |current_liquidity| { - *current_liquidity = current_liquidity.saturating_add(liquidity_delta); - }); - - // Update protocol position - position.liquidity = new_liquidity; - Positions::::insert( - (netuid, protocol_account_id, position.id), - position.clone(), - ); - - // Update position ticks - Self::add_liquidity_at_index(netuid, position.tick_low, liquidity_delta, false); - Self::add_liquidity_at_index(netuid, position.tick_high, liquidity_delta, true); - } + ) -> (TaoBalance, AlphaBalance) { + // Get reserves + let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); + let tao_reserve = T::TaoReserve::reserve(netuid.into()); + let mut balancer = SwapBalancer::::get(netuid); + + // Update weights and log errors if they go out of range + if balancer + .update_weights_for_added_liquidity( + u64::from(tao_reserve), + u64::from(alpha_reserve), + u64::from(tao_delta), + u64::from(alpha_delta), + ) + .is_err() + { + log::warn!( + "Reserves are out of range for emission: netuid = {}, tao = {}, alpha = {}, tao_delta = {}, alpha_delta = {}", + netuid, + tao_reserve, + alpha_reserve, + tao_delta, + alpha_delta + ); + (TaoBalance::ZERO, AlphaBalance::ZERO) + } else { + SwapBalancer::::insert(netuid, balancer); + (tao_delta, alpha_delta) } } @@ -241,7 +143,7 @@ impl Pallet { pub(crate) fn do_swap( netuid: NetUid, order: Order, - limit_sqrt_price: SqrtPrice, + limit_price: U64F64, drop_fees: bool, simulate: bool, ) -> Result, DispatchError> @@ -252,7 +154,7 @@ impl Pallet { transactional::with_transaction(|| { let reserve = Order::ReserveOut::reserve(netuid.into()); - let result = Self::swap_inner::(netuid, order, limit_sqrt_price, drop_fees) + let result = Self::swap_inner::(netuid, order, limit_price, drop_fees) .map_err(Into::into); if simulate || result.is_err() { @@ -278,7 +180,7 @@ impl Pallet { fn swap_inner( netuid: NetUid, order: Order, - limit_sqrt_price: SqrtPrice, + limit_price: U64F64, drop_fees: bool, ) -> Result, Error> where @@ -290,74 +192,37 @@ impl Pallet { Error::::ReservesTooLow ); - Self::maybe_initialize_v3(netuid)?; + Self::maybe_initialize_palswap(netuid, None)?; // Because user specifies the limit price, check that it is in fact beoynd the current one ensure!( - order.is_beyond_price_limit(AlphaSqrtPrice::::get(netuid), limit_sqrt_price), + order.is_beyond_price_limit(Self::current_price(netuid), limit_price), Error::::PriceLimitExceeded ); - let mut amount_remaining = order.amount(); - let mut amount_paid_out = Order::PaidOut::ZERO; - let mut iteration_counter: u16 = 0; - let mut in_acc = Order::PaidIn::ZERO; - let mut fee_acc = Order::PaidIn::ZERO; - let mut fee_to_block_author_acc = Order::PaidIn::ZERO; - log::trace!("======== Start Swap ========"); - log::trace!("Amount Remaining: {amount_remaining}"); - - // Swap one tick at a time until we reach one of the stop conditions - while !amount_remaining.is_zero() { - log::trace!("\nIteration: {iteration_counter}"); - log::trace!( - "\tCurrent Liquidity: {}", - CurrentLiquidity::::get(netuid) - ); - - // Create and execute a swap step - let mut swap_step = BasicSwapStep::::new( - netuid, - amount_remaining, - limit_sqrt_price, - drop_fees, - ); - - let swap_result = swap_step.execute()?; - - in_acc = in_acc.saturating_add(swap_result.delta_in); - fee_acc = fee_acc.saturating_add(swap_result.fee_paid); - fee_to_block_author_acc = - fee_to_block_author_acc.saturating_add(swap_result.fee_to_block_author); - amount_remaining = amount_remaining.saturating_sub(swap_result.amount_to_take); - amount_paid_out = amount_paid_out.saturating_add(swap_result.delta_out); + let amount_to_swap = order.amount(); + log::trace!("Amount to swap: {amount_to_swap}"); - if swap_step.action() == SwapStepAction::Stop { - amount_remaining = Order::PaidIn::ZERO; - } - - // The swap step didn't exchange anything - if swap_result.amount_to_take.is_zero() { - amount_remaining = Order::PaidIn::ZERO; - } + // Create and execute a swap step + let mut swap_step = BasicSwapStep::::new( + netuid, + amount_to_swap, + limit_price, + drop_fees, + ); - iteration_counter = iteration_counter.saturating_add(1); + let swap_result = swap_step.execute()?; - ensure!( - iteration_counter <= MAX_SWAP_ITERATIONS, - Error::::TooManySwapSteps - ); - } - - log::trace!("\nAmount Paid Out: {amount_paid_out}"); + log::trace!("Delta out: {}", swap_result.delta_out); + log::trace!("Fees: {}", swap_result.fee_paid); log::trace!("======== End Swap ========"); Ok(SwapResult { - amount_paid_in: in_acc, - amount_paid_out, - fee_paid: fee_acc, - fee_to_block_author: fee_to_block_author_acc, + amount_paid_in: swap_result.delta_in, + amount_paid_out: swap_result.delta_out, + fee_paid: swap_result.fee_paid, + fee_to_block_author: swap_result.fee_to_block_author, }) } @@ -382,427 +247,6 @@ impl Pallet { } } - pub fn find_closest_lower_active_tick(netuid: NetUid, index: TickIndex) -> Option { - ActiveTickIndexManager::::find_closest_lower(netuid, index) - .and_then(|ti| Ticks::::get(netuid, ti)) - } - - pub fn find_closest_higher_active_tick(netuid: NetUid, index: TickIndex) -> Option { - ActiveTickIndexManager::::find_closest_higher(netuid, index) - .and_then(|ti| Ticks::::get(netuid, ti)) - } - - /// Here we subtract minimum safe liquidity from current liquidity to stay in the safe range - pub(crate) fn current_liquidity_safe(netuid: NetUid) -> U64F64 { - U64F64::saturating_from_num( - CurrentLiquidity::::get(netuid).saturating_sub(T::MinimumLiquidity::get()), - ) - } - - /// Adds liquidity to the specified price range. - /// - /// This function allows an account to provide liquidity to a given range of price ticks. The - /// amount of liquidity to be added can be determined using - /// [`get_tao_based_liquidity`] and [`get_alpha_based_liquidity`], which compute the required - /// liquidity based on TAO and Alpha balances for the current price tick. - /// - /// ### Behavior: - /// - If the `protocol` flag is **not set** (`false`), the function will attempt to - /// **withdraw balances** from the account using `state_ops.withdraw_balances()`. - /// - If the `protocol` flag is **set** (`true`), the liquidity is added without modifying balances. - /// - If swap V3 was not initialized before, updates the value in storage. - /// - /// ### Parameters: - /// - `coldkey_account_id`: A reference to the account coldkey that is providing liquidity. - /// - `hotkey_account_id`: A reference to the account hotkey that is providing liquidity. - /// - `tick_low`: The lower bound of the price tick range. - /// - `tick_high`: The upper bound of the price tick range. - /// - `liquidity`: The amount of liquidity to be added. - /// - /// ### Returns: - /// - `Ok((u64, u64))`: (tao, alpha) amounts at new position - /// - `Err(SwapError)`: If the operation fails due to insufficient balance, invalid tick range, - /// or other swap-related errors. - /// - /// ### Errors: - /// - [`SwapError::InsufficientBalance`] if the account does not have enough balance. - /// - [`SwapError::InvalidTickRange`] if `tick_low` is greater than or equal to `tick_high`. - /// - Other [`SwapError`] variants as applicable. - pub fn do_add_liquidity( - netuid: NetUid, - coldkey_account_id: &T::AccountId, - hotkey_account_id: &T::AccountId, - tick_low: TickIndex, - tick_high: TickIndex, - liquidity: u64, - ) -> Result<(PositionId, u64, u64), Error> { - ensure!( - EnabledUserLiquidity::::get(netuid), - Error::::UserLiquidityDisabled - ); - - let (position, tao, alpha) = Self::add_liquidity_not_insert( - netuid, - coldkey_account_id, - tick_low, - tick_high, - liquidity, - )?; - let position_id = position.id; - - ensure!( - T::BalanceOps::tao_balance(coldkey_account_id) >= TaoBalance::from(tao) - && T::BalanceOps::alpha_balance( - netuid.into(), - coldkey_account_id, - hotkey_account_id - ) >= AlphaBalance::from(alpha), - Error::::InsufficientBalance - ); - - // Small delta is not allowed - ensure!( - liquidity >= T::MinimumLiquidity::get(), - Error::::InvalidLiquidityValue - ); - - Positions::::insert(&(netuid, coldkey_account_id, position.id), position); - - Ok((position_id, tao, alpha)) - } - - // add liquidity without inserting position into storage (used privately for v3 intiialization). - // unlike Self::add_liquidity it also doesn't perform account's balance check. - // - // the public interface is [`Self::add_liquidity`] - fn add_liquidity_not_insert( - netuid: NetUid, - coldkey_account_id: &T::AccountId, - tick_low: TickIndex, - tick_high: TickIndex, - liquidity: u64, - ) -> Result<(Position, u64, u64), Error> { - ensure!( - Self::count_positions(netuid, coldkey_account_id) < T::MaxPositions::get() as usize, - Error::::MaxPositionsExceeded - ); - - // Ensure that tick_high is actually higher than tick_low - ensure!(tick_high > tick_low, Error::::InvalidTickRange); - - // Add liquidity at tick - Self::add_liquidity_at_index(netuid, tick_low, liquidity, false); - Self::add_liquidity_at_index(netuid, tick_high, liquidity, true); - - // Update current tick liquidity - let current_tick_index = TickIndex::current_bounded::(netuid); - Self::clamp_sqrt_price(netuid, current_tick_index); - - Self::update_liquidity_if_needed(netuid, tick_low, tick_high, liquidity as i128); - - // New position - let position_id = PositionId::new::(); - let position = Position::new(position_id, netuid, tick_low, tick_high, liquidity); - - let current_price_sqrt = AlphaSqrtPrice::::get(netuid); - let (tao, alpha) = position.to_token_amounts(current_price_sqrt)?; - - SwapV3Initialized::::set(netuid, true); - - Ok((position, tao, alpha)) - } - - /// Remove liquidity and credit balances back to (coldkey_account_id, hotkey_account_id) stake. - /// Removing is allowed even when user liquidity is enabled. - /// - /// Account ID and Position ID identify position in the storage map - pub fn do_remove_liquidity( - netuid: NetUid, - coldkey_account_id: &T::AccountId, - position_id: PositionId, - ) -> Result> { - let Some(mut position) = Positions::::get((netuid, coldkey_account_id, position_id)) - else { - return Err(Error::::LiquidityNotFound); - }; - - // Collect fees and get tao and alpha amounts - let (fee_tao, fee_alpha) = position.collect_fees(); - let current_price = AlphaSqrtPrice::::get(netuid); - let (tao, alpha) = position.to_token_amounts(current_price)?; - - // Update liquidity at position ticks - Self::remove_liquidity_at_index(netuid, position.tick_low, position.liquidity, false); - Self::remove_liquidity_at_index(netuid, position.tick_high, position.liquidity, true); - - // Update current tick liquidity - Self::update_liquidity_if_needed( - netuid, - position.tick_low, - position.tick_high, - (position.liquidity as i128).neg(), - ); - - // Remove user position - Positions::::remove((netuid, coldkey_account_id, position_id)); - - Ok(RemoveLiquidityResult { - tao: tao.into(), - alpha: alpha.into(), - fee_tao: fee_tao.into(), - fee_alpha: fee_alpha.into(), - tick_low: position.tick_low, - tick_high: position.tick_high, - liquidity: position.liquidity, - }) - } - - pub fn do_modify_position( - netuid: NetUid, - coldkey_account_id: &T::AccountId, - hotkey_account_id: &T::AccountId, - position_id: PositionId, - liquidity_delta: i64, - ) -> Result> { - ensure!( - EnabledUserLiquidity::::get(netuid), - Error::::UserLiquidityDisabled - ); - - // Find the position - let Some(mut position) = Positions::::get((netuid, coldkey_account_id, position_id)) - else { - return Err(Error::::LiquidityNotFound); - }; - - // Small delta is not allowed - ensure!( - liquidity_delta.unsigned_abs() >= T::MinimumLiquidity::get(), - Error::::InvalidLiquidityValue - ); - let mut delta_liquidity_abs = liquidity_delta.unsigned_abs(); - - // Determine the effective price for token calculations - let current_price_sqrt = AlphaSqrtPrice::::get(netuid); - let sqrt_pa: SqrtPrice = position - .tick_low - .try_to_sqrt_price() - .map_err(|_| Error::::InvalidTickRange)?; - let sqrt_pb: SqrtPrice = position - .tick_high - .try_to_sqrt_price() - .map_err(|_| Error::::InvalidTickRange)?; - let sqrt_price_box = if current_price_sqrt < sqrt_pa { - sqrt_pa - } else if current_price_sqrt > sqrt_pb { - sqrt_pb - } else { - // Update current liquidity if price is in range - let new_liquidity_curr = if liquidity_delta > 0 { - CurrentLiquidity::::get(netuid).saturating_add(delta_liquidity_abs) - } else { - CurrentLiquidity::::get(netuid).saturating_sub(delta_liquidity_abs) - }; - CurrentLiquidity::::set(netuid, new_liquidity_curr); - current_price_sqrt - }; - - // Calculate token amounts for the liquidity change - let mul = SqrtPrice::from_num(1) - .safe_div(sqrt_price_box) - .saturating_sub(SqrtPrice::from_num(1).safe_div(sqrt_pb)); - let alpha = SqrtPrice::saturating_from_num(delta_liquidity_abs).saturating_mul(mul); - let tao = SqrtPrice::saturating_from_num(delta_liquidity_abs) - .saturating_mul(sqrt_price_box.saturating_sub(sqrt_pa)); - - // Validate delta - if liquidity_delta > 0 { - // Check that user has enough balances - ensure!( - T::BalanceOps::tao_balance(coldkey_account_id) - >= TaoBalance::from(tao.saturating_to_num::()) - && T::BalanceOps::alpha_balance(netuid, coldkey_account_id, hotkey_account_id) - >= AlphaBalance::from(alpha.saturating_to_num::()), - Error::::InsufficientBalance - ); - } else { - // Check that position has enough liquidity - ensure!( - position.liquidity >= delta_liquidity_abs, - Error::::InsufficientLiquidity - ); - } - - // Collect fees - let (fee_tao, fee_alpha) = position.collect_fees(); - - // If delta brings the position liquidity below MinimumLiquidity, eliminate position and - // withdraw full amounts - let mut remove = false; - if (liquidity_delta < 0) - && (position.liquidity.saturating_sub(delta_liquidity_abs) < T::MinimumLiquidity::get()) - { - delta_liquidity_abs = position.liquidity; - remove = true; - } - - // Adjust liquidity at the ticks based on the delta sign - if liquidity_delta > 0 { - // Add liquidity at tick - Self::add_liquidity_at_index(netuid, position.tick_low, delta_liquidity_abs, false); - Self::add_liquidity_at_index(netuid, position.tick_high, delta_liquidity_abs, true); - - // Add liquidity to user position - position.liquidity = position.liquidity.saturating_add(delta_liquidity_abs); - } else { - // Remove liquidity at tick - Self::remove_liquidity_at_index(netuid, position.tick_low, delta_liquidity_abs, false); - Self::remove_liquidity_at_index(netuid, position.tick_high, delta_liquidity_abs, true); - - // Remove liquidity from user position - position.liquidity = position.liquidity.saturating_sub(delta_liquidity_abs); - } - - // Update or, in case if full liquidity is removed, remove the position - if remove { - Positions::::remove((netuid, coldkey_account_id, position_id)); - } else { - Positions::::insert(&(netuid, coldkey_account_id, position.id), position.clone()); - } - - Ok(UpdateLiquidityResult { - tao: tao.saturating_to_num::().into(), - alpha: alpha.saturating_to_num::().into(), - fee_tao: fee_tao.into(), - fee_alpha: fee_alpha.into(), - removed: remove, - tick_low: position.tick_low, - tick_high: position.tick_high, - }) - } - - /// Adds or updates liquidity at a specific tick index for a subnet - /// - /// # Arguments - /// * `netuid` - The subnet ID - /// * `tick_index` - The tick index to add liquidity to - /// * `liquidity` - The amount of liquidity to add - fn add_liquidity_at_index(netuid: NetUid, tick_index: TickIndex, liquidity: u64, upper: bool) { - // Convert liquidity to signed value, negating it for upper bounds - let net_liquidity_change = if upper { - (liquidity as i128).neg() - } else { - liquidity as i128 - }; - - Ticks::::mutate(netuid, tick_index, |maybe_tick| match maybe_tick { - Some(tick) => { - tick.liquidity_net = tick.liquidity_net.saturating_add(net_liquidity_change); - tick.liquidity_gross = tick.liquidity_gross.saturating_add(liquidity); - } - None => { - let current_tick = TickIndex::current_bounded::(netuid); - - let (fees_out_tao, fees_out_alpha) = if tick_index > current_tick { - ( - I64F64::saturating_from_num(FeeGlobalTao::::get(netuid)), - I64F64::saturating_from_num(FeeGlobalAlpha::::get(netuid)), - ) - } else { - ( - I64F64::saturating_from_num(0), - I64F64::saturating_from_num(0), - ) - }; - *maybe_tick = Some(Tick { - liquidity_net: net_liquidity_change, - liquidity_gross: liquidity, - fees_out_tao, - fees_out_alpha, - }); - } - }); - - // Update active ticks - ActiveTickIndexManager::::insert(netuid, tick_index); - } - - /// Remove liquidity at tick index. - fn remove_liquidity_at_index( - netuid: NetUid, - tick_index: TickIndex, - liquidity: u64, - upper: bool, - ) { - // Calculate net liquidity addition - let net_reduction = if upper { - (liquidity as i128).neg() - } else { - liquidity as i128 - }; - - Ticks::::mutate_exists(netuid, tick_index, |maybe_tick| { - if let Some(tick) = maybe_tick { - tick.liquidity_net = tick.liquidity_net.saturating_sub(net_reduction); - tick.liquidity_gross = tick.liquidity_gross.saturating_sub(liquidity); - - // If no liquidity is left at the tick, remove it - if tick.liquidity_gross == 0 { - *maybe_tick = None; - - // Update active ticks: Final liquidity is zero, remove this tick from active. - ActiveTickIndexManager::::remove(netuid, tick_index); - } - } - }); - } - - /// Updates the current liquidity for a subnet if the current tick index is within the specified - /// range - /// - /// This function handles both increasing and decreasing liquidity based on the sign of the - /// liquidity parameter. It uses i128 to safely handle values up to u64::MAX in both positive - /// and negative directions. - fn update_liquidity_if_needed( - netuid: NetUid, - tick_low: TickIndex, - tick_high: TickIndex, - liquidity: i128, - ) { - let current_tick_index = TickIndex::current_bounded::(netuid); - if (tick_low <= current_tick_index) && (current_tick_index < tick_high) { - CurrentLiquidity::::mutate(netuid, |current_liquidity| { - let is_neg = liquidity.is_negative(); - let liquidity = liquidity.abs().min(u64::MAX as i128) as u64; - if is_neg { - *current_liquidity = current_liquidity.saturating_sub(liquidity); - } else { - *current_liquidity = current_liquidity.saturating_add(liquidity); - } - }); - } - } - - /// Clamps the subnet's sqrt price when tick index is outside of valid bounds - fn clamp_sqrt_price(netuid: NetUid, tick_index: TickIndex) { - if tick_index >= TickIndex::MAX || tick_index <= TickIndex::MIN { - let corrected_price = tick_index.as_sqrt_price_bounded(); - AlphaSqrtPrice::::set(netuid, corrected_price); - } - } - - /// Returns the number of positions for an account in a specific subnet - /// - /// # Arguments - /// * `netuid` - The subnet ID - /// * `account_id` - The account ID - /// - /// # Returns - /// The number of positions that the account has in the specified subnet - pub(super) fn count_positions(netuid: NetUid, account_id: &T::AccountId) -> usize { - Positions::::iter_prefix_values((netuid, account_id.clone())).count() - } - /// Returns the protocol account ID /// /// # Returns @@ -812,94 +256,26 @@ impl Pallet { } pub(crate) fn min_price_inner() -> C { - TickIndex::min_sqrt_price() - .saturating_mul(TickIndex::min_sqrt_price()) - .saturating_mul(SqrtPrice::saturating_from_num(1_000_000_000)) - .saturating_to_num::() - .into() + u64::from(1_000_u64).into() } pub(crate) fn max_price_inner() -> C { - TickIndex::max_sqrt_price() - .saturating_mul(TickIndex::max_sqrt_price()) - .saturating_mul(SqrtPrice::saturating_from_num(1_000_000_000)) - .saturating_round() - .saturating_to_num::() - .into() - } - - /// Dissolve all LPs and clean state. - pub fn do_dissolve_all_liquidity_providers(_netuid: NetUid) -> DispatchResultWithPostInfo { - // Deprecated in balancer, also we do not have any active liquidity providers - // or any ways to provide liquidity. - Ok(Some(Weight::default()).into()) + u64::from(1_000_000_000_000_000_u64).into() } /// Clear **protocol-owned** liquidity and wipe all swap state for `netuid`. pub fn do_clear_protocol_liquidity(netuid: NetUid) -> DispatchResult { - let protocol_account = Self::protocol_account_id(); - - // 1) Force-close only protocol positions, burning proceeds. - let mut burned_tao = TaoBalance::ZERO; - let mut burned_alpha = AlphaBalance::ZERO; + // 1) Force-close protocol liquidity, burning proceeds. + let burned_tao = T::TaoReserve::reserve(netuid.into()); + let burned_alpha = T::AlphaReserve::reserve(netuid.into()); - // Collect protocol position IDs first to avoid mutating while iterating. - let protocol_pos_ids: sp_std::vec::Vec = Positions::::iter_prefix((netuid,)) - .filter_map(|((owner, pos_id), _)| { - if owner == protocol_account { - Some(pos_id) - } else { - None - } - }) - .collect(); + T::TaoReserve::decrease_provided(netuid.into(), burned_tao); + T::AlphaReserve::decrease_provided(netuid.into(), burned_alpha); - for pos_id in protocol_pos_ids { - match Self::do_remove_liquidity(netuid, &protocol_account, pos_id) { - Ok(rm) => { - let alpha_total_from_pool: AlphaBalance = rm.alpha.saturating_add(rm.fee_alpha); - let tao_total_from_pool: TaoBalance = rm.tao.saturating_add(rm.fee_tao); + PalSwapInitialized::::remove(netuid); - if tao_total_from_pool > TaoBalance::ZERO { - burned_tao = burned_tao.saturating_add(tao_total_from_pool); - } - if alpha_total_from_pool > AlphaBalance::ZERO { - burned_alpha = burned_alpha.saturating_add(alpha_total_from_pool); - } - - log::debug!( - "clear_protocol_liquidity: burned protocol pos: netuid={netuid:?}, pos_id={pos_id:?}, τ={tao_total_from_pool:?}, α_total={alpha_total_from_pool:?}" - ); - } - Err(e) => { - log::debug!( - "clear_protocol_liquidity: force-close failed: netuid={netuid:?}, pos_id={pos_id:?}, err={e:?}" - ); - continue; - } - } - } - - // 2) Clear active tick index entries, then all swap state (idempotent even if empty/non‑V3). - let active_ticks: sp_std::vec::Vec = - Ticks::::iter_prefix(netuid).map(|(ti, _)| ti).collect(); - for ti in active_ticks { - ActiveTickIndexManager::::remove(netuid, ti); - } - - let _ = Positions::::clear_prefix((netuid,), u32::MAX, None); - let _ = Ticks::::clear_prefix(netuid, u32::MAX, None); - - FeeGlobalTao::::remove(netuid); - FeeGlobalAlpha::::remove(netuid); - CurrentLiquidity::::remove(netuid); - CurrentTick::::remove(netuid); - AlphaSqrtPrice::::remove(netuid); - SwapV3Initialized::::remove(netuid); - - let _ = TickIndexBitmapWords::::clear_prefix((netuid,), u32::MAX, None); FeeRate::::remove(netuid); - EnabledUserLiquidity::::remove(netuid); + SwapBalancer::::remove(netuid); log::debug!( "clear_protocol_liquidity: netuid={netuid:?}, protocol_burned: τ={burned_tao:?}, α={burned_alpha:?}; state cleared" @@ -935,15 +311,13 @@ where drop_fees: bool, should_rollback: bool, ) -> Result, DispatchError> { - let limit_sqrt_price = SqrtPrice::saturating_from_num(price_limit.to_u64()) - .safe_div(SqrtPrice::saturating_from_num(1_000_000_000)) - .checked_sqrt(SqrtPrice::saturating_from_num(0.0000000001)) - .ok_or(Error::::PriceLimitExceeded)?; + let limit_price = U64F64::saturating_from_num(price_limit.to_u64()) + .safe_div(U64F64::saturating_from_num(1_000_000_000_u64)); Self::do_swap::( NetUid::from(netuid), order, - limit_sqrt_price, + limit_price, drop_fees, should_rollback, ) @@ -1000,28 +374,10 @@ impl SwapHandler for Pallet { Self::calculate_fee_amount(netuid, amount, false) } - fn current_alpha_price(netuid: NetUid) -> U96F32 { + fn current_alpha_price(netuid: NetUid) -> U64F64 { Self::current_price(netuid.into()) } - fn get_protocol_tao(netuid: NetUid) -> TaoBalance { - let protocol_account_id = Self::protocol_account_id(); - let mut positions = - Positions::::iter_prefix_values((netuid, protocol_account_id.clone())) - .collect::>(); - - if let Some(position) = positions.get_mut(0) { - let current_sqrt_price = AlphaSqrtPrice::::get(netuid); - // Adjust liquidity - let maybe_token_amounts = position.to_token_amounts(current_sqrt_price); - if let Ok((tao, _)) = maybe_token_amounts { - return tao.into(); - } - } - - TaoBalance::ZERO - } - fn min_price() -> C { Self::min_price_inner() } @@ -1030,23 +386,22 @@ impl SwapHandler for Pallet { Self::max_price_inner() } - fn adjust_protocol_liquidity(netuid: NetUid, tao_delta: TaoBalance, alpha_delta: AlphaBalance) { - Self::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta); + fn adjust_protocol_liquidity( + netuid: NetUid, + tao_delta: TaoBalance, + alpha_delta: AlphaBalance, + ) -> (TaoBalance, AlphaBalance) { + Self::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta) } - fn is_user_liquidity_enabled(netuid: NetUid) -> bool { - EnabledUserLiquidity::::get(netuid) - } - fn dissolve_all_liquidity_providers(netuid: NetUid) -> DispatchResultWithPostInfo { - Self::do_dissolve_all_liquidity_providers(netuid) - } - fn toggle_user_liquidity(netuid: NetUid, enabled: bool) { - EnabledUserLiquidity::::insert(netuid, enabled) - } fn clear_protocol_liquidity(netuid: NetUid) -> DispatchResult { Self::do_clear_protocol_liquidity(netuid) } + fn init_swap(netuid: NetUid, maybe_price: Option) { + Self::maybe_initialize_palswap(netuid, maybe_price).unwrap_or_default(); + } + /// Get the amount of Alpha that needs to be sold to get a given amount of Tao fn get_alpha_amount_for_tao(netuid: NetUid, tao_amount: TaoBalance) -> AlphaBalance { match T::SubnetInfo::mechanism(netuid.into()) { @@ -1055,7 +410,7 @@ impl SwapHandler for Pallet { // hence we can neglect slippage and return slightly lower amount. let alpha_price = Self::current_price(netuid.into()); AlphaBalance::from( - U96F32::from(u64::from(tao_amount)) + U64F64::from(u64::from(tao_amount)) .safe_div(alpha_price) .saturating_to_num::(), ) diff --git a/pallets/swap/src/pallet/migrations/migrate_swapv3_to_balancer.rs b/pallets/swap/src/pallet/migrations/migrate_swapv3_to_balancer.rs index 63392da9ea..2f06d88a00 100644 --- a/pallets/swap/src/pallet/migrations/migrate_swapv3_to_balancer.rs +++ b/pallets/swap/src/pallet/migrations/migrate_swapv3_to_balancer.rs @@ -44,7 +44,17 @@ pub fn migrate_swapv3_to_balancer() -> Weight { // ------------------------------ for (netuid, price_sqrt) in deprecated_swap_maps::AlphaSqrtPrice::::iter() { let price = price_sqrt.saturating_mul(price_sqrt); - crate::Pallet::::maybe_initialize_palswap(netuid, Some(price)).unwrap_or_default(); + if let Err(error) = crate::Pallet::::maybe_initialize_palswap(netuid, Some(price)) { + log::warn!( + "Migration '{}' failed to initialize balancer with V3 price for netuid {}: {:?}. Falling back to default balancer.", + String::from_utf8_lossy(&migration_name), + netuid, + error, + ); + SwapBalancer::::insert(netuid, Balancer::default()); + PalSwapInitialized::::insert(netuid, true); + weight = weight.saturating_add(T::DbWeight::get().writes(2)); + } } // ------------------------------ diff --git a/pallets/swap/src/pallet/migrations/mod.rs b/pallets/swap/src/pallet/migrations/mod.rs new file mode 100644 index 0000000000..d34626f05e --- /dev/null +++ b/pallets/swap/src/pallet/migrations/mod.rs @@ -0,0 +1,25 @@ +use super::*; +use frame_support::pallet_prelude::Weight; +use sp_io::KillStorageResult; +use sp_io::hashing::twox_128; +use sp_io::storage::clear_prefix; +use sp_std::vec::Vec; + +pub mod migrate_swapv3_to_balancer; + +pub(crate) fn remove_prefix(module: &str, old_map: &str, weight: &mut Weight) { + let mut prefix = Vec::new(); + prefix.extend_from_slice(&twox_128(module.as_bytes())); + prefix.extend_from_slice(&twox_128(old_map.as_bytes())); + + let removal_results = clear_prefix(&prefix, Some(u32::MAX)); + let removed_entries_count = match removal_results { + KillStorageResult::AllRemoved(removed) => removed as u64, + KillStorageResult::SomeRemaining(removed) => { + log::info!("Failed To Remove Some Items During migration"); + removed as u64 + } + }; + + *weight = (*weight).saturating_add(T::DbWeight::get().writes(removed_entries_count)); +} diff --git a/pallets/swap/src/pallet/mod.rs b/pallets/swap/src/pallet/mod.rs index 763d2150b2..1d2fd07c59 100644 --- a/pallets/swap/src/pallet/mod.rs +++ b/pallets/swap/src/pallet/mod.rs @@ -2,31 +2,31 @@ use core::num::NonZeroU64; use frame_support::{PalletId, pallet_prelude::*, traits::Get}; use frame_system::pallet_prelude::*; -use sp_arithmetic::Perbill; -use substrate_fixed::types::U64F64; use subtensor_runtime_common::{ AlphaBalance, BalanceOps, NetUid, SubnetInfo, TaoBalance, TokenReserve, }; -use crate::{ - position::{Position, PositionId}, - tick::{LayerLevel, Tick, TickIndex}, - weights::WeightInfo, -}; - +use crate::{pallet::balancer::Balancer, weights::WeightInfo}; pub use pallet::*; +use subtensor_macros::freeze_struct; +mod balancer; +mod hooks; mod impls; +pub mod migrations; mod swap_step; #[cfg(test)] mod tests; +// Define a maximum length for the migration key +type MigrationKeyMaxLen = ConstU32<128>; + #[allow(clippy::module_inception)] #[frame_support::pallet] #[allow(clippy::expect_used)] mod pallet { use super::*; - use frame_system::{ensure_root, ensure_signed}; + use frame_system::ensure_root; #[pallet::pallet] pub struct Pallet(_); @@ -56,10 +56,6 @@ mod pallet { #[pallet::constant] type MaxFeeRate: Get; - /// The maximum number of positions a user can have - #[pallet::constant] - type MaxPositions: Get; - /// Minimum liquidity that is safe for rounding and integer math. #[pallet::constant] type MinimumLiquidity: Get; @@ -95,85 +91,32 @@ mod pallet { 33 // ~0.05 % } - /// Fee split between pool and block builder. - /// Pool receives the portion returned by this function - #[pallet::type_value] - pub fn DefaultFeeSplit() -> Perbill { - Perbill::zero() - } - /// The fee rate applied to swaps per subnet, normalized value between 0 and u16::MAX #[pallet::storage] pub type FeeRate = StorageMap<_, Twox64Concat, NetUid, u16, ValueQuery, DefaultFeeRate>; - // Global accrued fees in tao per subnet - #[pallet::storage] - pub type FeeGlobalTao = StorageMap<_, Twox64Concat, NetUid, U64F64, ValueQuery>; - - // Global accrued fees in alpha per subnet - #[pallet::storage] - pub type FeeGlobalAlpha = StorageMap<_, Twox64Concat, NetUid, U64F64, ValueQuery>; - - /// Storage for all ticks, using subnet ID as the primary key and tick index as the secondary key - #[pallet::storage] - pub type Ticks = StorageDoubleMap<_, Twox64Concat, NetUid, Twox64Concat, TickIndex, Tick>; - - /// Storage to determine whether swap V3 was initialized for a specific subnet. - #[pallet::storage] - pub type SwapV3Initialized = StorageMap<_, Twox64Concat, NetUid, bool, ValueQuery>; + //////////////////////////////////////////////////// + // Balancer (PalSwap) maps and variables - /// Storage for the square root price of Alpha token for each subnet. - #[pallet::storage] - pub type AlphaSqrtPrice = StorageMap<_, Twox64Concat, NetUid, U64F64, ValueQuery>; - - /// Storage for the current price tick. - #[pallet::storage] - pub type CurrentTick = StorageMap<_, Twox64Concat, NetUid, TickIndex, ValueQuery>; - - /// Storage for the current liquidity amount for each subnet. - #[pallet::storage] - pub type CurrentLiquidity = StorageMap<_, Twox64Concat, NetUid, u64, ValueQuery>; + /// Default reserve weight + #[pallet::type_value] + pub fn DefaultBalancer() -> Balancer { + Balancer::default() + } - /// Indicates whether a subnet has been switched to V3 swap from V2. - /// If `true`, the subnet is permanently on V3 swap mode allowing add/remove liquidity - /// operations. Once set to `true` for a subnet, it cannot be changed back to `false`. + /// u64-normalized reserve weight #[pallet::storage] - pub type EnabledUserLiquidity = StorageMap<_, Twox64Concat, NetUid, bool, ValueQuery>; + pub type SwapBalancer = + StorageMap<_, Twox64Concat, NetUid, Balancer, ValueQuery, DefaultBalancer>; - /// Storage for user positions, using subnet ID and account ID as keys - /// The value is a bounded vector of Position structs with details about the liquidity positions + /// Storage to determine whether balancer swap was initialized for a specific subnet. #[pallet::storage] - pub type Positions = StorageNMap< - _, - ( - NMapKey, // Subnet ID - NMapKey, // Account ID - NMapKey, // Position ID - ), - Position, - OptionQuery, - >; - - /// Position ID counter. - #[pallet::storage] - pub type LastPositionId = StorageValue<_, u128, ValueQuery>; + pub type PalSwapInitialized = StorageMap<_, Twox64Concat, NetUid, bool, ValueQuery>; - /// Tick index bitmap words storage - #[pallet::storage] - pub type TickIndexBitmapWords = StorageNMap< - _, - ( - NMapKey, // Subnet ID - NMapKey, // Layer level - NMapKey, // word index - ), - u128, - ValueQuery, - >; - - /// TAO reservoir for scraps of protocol claimed fees. + /// --- Storage for migration run status #[pallet::storage] - pub type ScrapReservoirTao = StorageMap<_, Twox64Concat, NetUid, TaoBalance, ValueQuery>; + pub type HasMigrationRun = + StorageMap<_, Identity, BoundedVec, bool, ValueQuery>; /// Alpha reservoir for scraps of protocol claimed fees. #[pallet::storage] @@ -184,85 +127,6 @@ mod pallet { pub enum Event { /// Event emitted when the fee rate has been updated for a subnet FeeRateSet { netuid: NetUid, rate: u16 }, - - /// Event emitted when user liquidity operations are enabled for a subnet. - /// First enable even indicates a switch from V2 to V3 swap. - UserLiquidityToggled { netuid: NetUid, enable: bool }, - - /// Event emitted when a liquidity position is added to a subnet's liquidity pool. - LiquidityAdded { - /// The coldkey account that owns the position - coldkey: T::AccountId, - /// The hotkey account where Alpha comes from - hotkey: T::AccountId, - /// The subnet identifier - netuid: NetUid, - /// Unique identifier for the liquidity position - position_id: PositionId, - /// The amount of liquidity added to the position - liquidity: u64, - /// The amount of TAO tokens committed to the position - tao: TaoBalance, - /// The amount of Alpha tokens committed to the position - alpha: AlphaBalance, - /// the lower tick - tick_low: TickIndex, - /// the upper tick - tick_high: TickIndex, - }, - - /// Event emitted when a liquidity position is removed from a subnet's liquidity pool. - LiquidityRemoved { - /// The coldkey account that owns the position - coldkey: T::AccountId, - /// The hotkey account where Alpha goes to - hotkey: T::AccountId, - /// The subnet identifier - netuid: NetUid, - /// Unique identifier for the liquidity position - position_id: PositionId, - /// The amount of liquidity removed from the position - liquidity: u64, - /// The amount of TAO tokens returned to the user - tao: TaoBalance, - /// The amount of Alpha tokens returned to the user - alpha: AlphaBalance, - /// The amount of TAO fees earned from the position - fee_tao: TaoBalance, - /// The amount of Alpha fees earned from the position - fee_alpha: AlphaBalance, - /// the lower tick - tick_low: TickIndex, - /// the upper tick - tick_high: TickIndex, - }, - - /// Event emitted when a liquidity position is modified in a subnet's liquidity pool. - /// Modifying causes the fees to be claimed. - LiquidityModified { - /// The coldkey account that owns the position - coldkey: T::AccountId, - /// The hotkey account where Alpha comes from or goes to - hotkey: T::AccountId, - /// The subnet identifier - netuid: NetUid, - /// Unique identifier for the liquidity position - position_id: PositionId, - /// The amount of liquidity added to or removed from the position - liquidity: i64, - /// The amount of TAO tokens returned to the user - tao: i64, - /// The amount of Alpha tokens returned to the user - alpha: i64, - /// The amount of TAO fees earned from the position - fee_tao: TaoBalance, - /// The amount of Alpha fees earned from the position - fee_alpha: AlphaBalance, - /// the lower tick - tick_low: TickIndex, - /// the upper tick - tick_high: TickIndex, - }, } #[pallet::error] @@ -283,18 +147,9 @@ mod pallet { /// The caller does not have enough balance for the operation. InsufficientBalance, - /// Attempted to remove liquidity that does not exist. - LiquidityNotFound, - /// The provided tick range is invalid. InvalidTickRange, - /// Maximum user positions exceeded - MaxPositionsExceeded, - - /// Too many swap steps - TooManySwapSteps, - /// Provided liquidity parameter is invalid (likely too small) InvalidLiquidityValue, @@ -304,11 +159,14 @@ mod pallet { /// The subnet does not exist. MechanismDoesNotExist, - /// User liquidity operations are disabled for this subnet - UserLiquidityDisabled, - /// The subnet does not have subtoken enabled SubtokenDisabled, + + /// Swap reserves are too imbalanced + ReservesOutOfBalance, + + /// The extrinsic is deprecated + Deprecated, } #[pallet::call] @@ -339,149 +197,47 @@ mod pallet { Ok(()) } - /// Enable user liquidity operations for a specific subnet. This switches the - /// subnet from V2 to V3 swap mode. Thereafter, adding new user liquidity can be disabled - /// by toggling this flag to false, but the swap mode will remain V3 because of existing - /// user liquidity until all users withdraw their liquidity. - /// - /// Only sudo or subnet owner can enable user liquidity. - /// Only sudo can disable user liquidity. + /// DEPRECATED #[pallet::call_index(4)] - #[pallet::weight(::WeightInfo::toggle_user_liquidity())] + #[pallet::weight(Weight::from_parts(15_000_000, 0))] pub fn toggle_user_liquidity( - origin: OriginFor, - netuid: NetUid, - enable: bool, + _origin: OriginFor, + _netuid: NetUid, + _enable: bool, ) -> DispatchResult { - if ensure_root(origin.clone()).is_err() { - let account_id: T::AccountId = ensure_signed(origin)?; - // Only enabling is allowed to subnet owner - ensure!( - T::SubnetInfo::is_owner(&account_id, netuid.into()) && enable, - DispatchError::BadOrigin - ); - } - - ensure!( - T::SubnetInfo::exists(netuid.into()), - Error::::MechanismDoesNotExist - ); - - // EnabledUserLiquidity::::insert(netuid, enable); - - // Self::deposit_event(Event::UserLiquidityToggled { netuid, enable }); - - Ok(()) + Err(Error::::Deprecated.into()) } - /// Add liquidity to a specific price range for a subnet. - /// - /// Parameters: - /// - origin: The origin of the transaction - /// - netuid: Subnet ID - /// - tick_low: Lower bound of the price range - /// - tick_high: Upper bound of the price range - /// - liquidity: Amount of liquidity to add - /// - /// Emits `Event::LiquidityAdded` on success + /// DEPRECATED #[pallet::call_index(1)] - #[pallet::weight(::WeightInfo::add_liquidity())] + #[pallet::weight(Weight::from_parts(15_000_000, 0))] pub fn add_liquidity( - origin: OriginFor, + _origin: OriginFor, _hotkey: T::AccountId, _netuid: NetUid, _tick_low: TickIndex, _tick_high: TickIndex, _liquidity: u64, ) -> DispatchResult { - ensure_signed(origin)?; - - // Extrinsic should have no effect. This fix may have to be reverted later, - // so leaving the code in for now. - - // // Ensure that the subnet exists. - // ensure!( - // T::SubnetInfo::exists(netuid.into()), - // Error::::MechanismDoesNotExist - // ); - - // ensure!( - // T::SubnetInfo::is_subtoken_enabled(netuid.into()), - // Error::::SubtokenDisabled - // ); - - // let (position_id, tao, alpha) = Self::do_add_liquidity( - // netuid.into(), - // &coldkey, - // &hotkey, - // tick_low, - // tick_high, - // liquidity, - // )?; - // let alpha = AlphaBalance::from(alpha); - // let tao = TaoBalance::from(tao); - - // // Remove TAO and Alpha balances or fail transaction if they can't be removed exactly - // let tao_provided = T::BalanceOps::decrease_balance(&coldkey, tao)?; - // ensure!(tao_provided == tao, Error::::InsufficientBalance); - - // let alpha_provided = - // T::BalanceOps::decrease_stake(&coldkey, &hotkey, netuid.into(), alpha)?; - // ensure!(alpha_provided == alpha, Error::::InsufficientBalance); - - // // Add provided liquidity to user-provided reserves - // T::TaoReserve::increase_provided(netuid.into(), tao_provided); - // T::AlphaReserve::increase_provided(netuid.into(), alpha_provided); - - // // Emit an event - // Self::deposit_event(Event::LiquidityAdded { - // coldkey, - // hotkey, - // netuid, - // position_id, - // liquidity, - // tao, - // alpha, - // tick_low, - // tick_high, - // }); - - // Ok(()) - - Err(Error::::UserLiquidityDisabled.into()) + Err(Error::::Deprecated.into()) } - /// Remove liquidity from a specific position. - /// - /// Parameters: - /// - origin: The origin of the transaction - /// - netuid: Subnet ID - /// - position_id: ID of the position to remove - /// - /// Emits `Event::LiquidityRemoved` on success + /// DEPRECATED #[pallet::call_index(2)] - #[pallet::weight(::WeightInfo::remove_liquidity())] + #[pallet::weight(Weight::from_parts(15_000_000, 0))] pub fn remove_liquidity( _origin: OriginFor, _hotkey: T::AccountId, _netuid: NetUid, _position_id: PositionId, ) -> DispatchResult { - // Deprecated by balancer. We don't have any active liquidity providers either. - Ok(()) + Err(Error::::Deprecated.into()) } - /// Modify a liquidity position. - /// - /// Parameters: - /// - origin: The origin of the transaction - /// - netuid: Subnet ID - /// - position_id: ID of the position to remove - /// - liquidity_delta: Liquidity to add (if positive) or remove (if negative) - /// - /// Emits `Event::LiquidityRemoved` on success + /// DEPRECATED #[pallet::call_index(3)] - #[pallet::weight(::WeightInfo::modify_position())] + #[pallet::weight(Weight::from_parts(15_000_000, 0))] + #[deprecated(note = "Deprecated, user liquidity is permanently disabled")] pub fn modify_position( _origin: OriginFor, _hotkey: T::AccountId, @@ -489,35 +245,52 @@ mod pallet { _position_id: PositionId, _liquidity_delta: i64, ) -> DispatchResult { - // Deprecated by balancer. We don't have any active liquidity providers either. - Ok(()) + Err(Error::::Deprecated.into()) } - /// Disable user liquidity in all subnets. - /// - /// Emits `Event::UserLiquidityToggled` on success + /// DEPRECATED #[pallet::call_index(5)] - #[pallet::weight(::WeightInfo::disable_lp())] - pub fn disable_lp(origin: OriginFor) -> DispatchResult { - ensure_root(origin)?; - - for netuid in 1..=128 { - let netuid = NetUid::from(netuid as u16); - if EnabledUserLiquidity::::get(netuid) { - EnabledUserLiquidity::::insert(netuid, false); - Self::deposit_event(Event::UserLiquidityToggled { - netuid, - enable: false, - }); - } - - // Remove provided liquidity unconditionally because the network may have - // user liquidity previously disabled - // Ignore result to avoid early stopping - let _ = Self::do_dissolve_all_liquidity_providers(netuid); - } - - Ok(()) + #[pallet::weight(Weight::from_parts(15_000_000, 0))] + #[deprecated(note = "Deprecated, user liquidity is permanently disabled")] + pub fn disable_lp(_origin: OriginFor) -> DispatchResult { + Err(Error::::Deprecated.into()) } } } + +/// Struct representing a tick index, DEPRECATED +#[freeze_struct("7c280c2b3bbbb33e")] +#[derive( + Debug, + Default, + Clone, + Copy, + Decode, + Encode, + DecodeWithMemTracking, + TypeInfo, + MaxEncodedLen, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, +)] +pub struct TickIndex(i32); + +/// Struct representing a liquidity position ID, DEPRECATED +#[freeze_struct("e695cd6455c3f0cb")] +#[derive( + Clone, + Copy, + Decode, + DecodeWithMemTracking, + Default, + Encode, + Eq, + MaxEncodedLen, + PartialEq, + RuntimeDebug, + TypeInfo, +)] +pub struct PositionId(u128); diff --git a/pallets/swap/src/pallet/swap_step.rs b/pallets/swap/src/pallet/swap_step.rs index bdcad40074..7f10bff65a 100644 --- a/pallets/swap/src/pallet/swap_step.rs +++ b/pallets/swap/src/pallet/swap_step.rs @@ -1,15 +1,11 @@ use core::marker::PhantomData; -use frame_support::{ensure, pallet_prelude::Zero, traits::Get}; +use frame_support::ensure; use safe_math::*; -use substrate_fixed::types::{I64F64, U64F64}; -use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; +use substrate_fixed::types::U64F64; +use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token, TokenReserve}; use super::pallet::*; -use crate::{ - SqrtPrice, - tick::{ActiveTickIndexManager, TickIndex}, -}; /// A struct representing a single swap step with all its parameters and state pub(crate) struct BasicSwapStep @@ -21,22 +17,16 @@ where // Input parameters netuid: NetUid, drop_fees: bool, + requested_delta_in: PaidIn, + limit_price: U64F64, - // Computed values - current_liquidity: U64F64, - possible_delta_in: PaidIn, - - // Ticks and prices (current, limit, edge, target) - target_sqrt_price: SqrtPrice, - limit_sqrt_price: SqrtPrice, - current_sqrt_price: SqrtPrice, - edge_sqrt_price: SqrtPrice, - edge_tick: TickIndex, + // Intermediate calculations + target_price: U64F64, + current_price: U64F64, // Result values - action: SwapStepAction, delta_in: PaidIn, - final_price: SqrtPrice, + final_price: U64F64, fee: PaidIn, _phantom: PhantomData<(T, PaidIn, PaidOut)>, @@ -53,36 +43,25 @@ where pub(crate) fn new( netuid: NetUid, amount_remaining: PaidIn, - limit_sqrt_price: SqrtPrice, + limit_price: U64F64, drop_fees: bool, ) -> Self { - // Calculate prices and ticks - let current_tick = CurrentTick::::get(netuid); - let current_sqrt_price = AlphaSqrtPrice::::get(netuid); - let edge_tick = Self::tick_edge(netuid, current_tick); - let edge_sqrt_price = edge_tick.as_sqrt_price_bounded(); - let fee = Pallet::::calculate_fee_amount(netuid, amount_remaining, drop_fees); - let possible_delta_in = amount_remaining.saturating_sub(fee); + let requested_delta_in = amount_remaining.saturating_sub(fee); - // Target price and quantities - let current_liquidity = U64F64::saturating_from_num(CurrentLiquidity::::get(netuid)); - let target_sqrt_price = - Self::sqrt_price_target(current_liquidity, current_sqrt_price, possible_delta_in); + // Target and current prices + let target_price = Self::price_target(netuid, requested_delta_in); + let current_price = Pallet::::current_price(netuid); Self { netuid, drop_fees, - target_sqrt_price, - limit_sqrt_price, - current_sqrt_price, - edge_sqrt_price, - edge_tick, - possible_delta_in, - current_liquidity, - action: SwapStepAction::Stop, + requested_delta_in, + limit_price, + target_price, + current_price, delta_in: PaidIn::ZERO, - final_price: target_sqrt_price, + final_price: target_price, fee, _phantom: PhantomData, } @@ -99,64 +78,25 @@ where let mut recalculate_fee = false; // Calculate the stopping price: The price at which we either reach the limit price, - // exchange the full amount, or reach the edge price. - if Self::price_is_closer(&self.target_sqrt_price, &self.limit_sqrt_price) - && Self::price_is_closer(&self.target_sqrt_price, &self.edge_sqrt_price) - { - // Case 1. target_quantity is the lowest - // The trade completely happens within one tick, no tick crossing happens. - self.action = SwapStepAction::Stop; - self.final_price = self.target_sqrt_price; - self.delta_in = self.possible_delta_in; - } else if Self::price_is_closer(&self.limit_sqrt_price, &self.target_sqrt_price) - && Self::price_is_closer(&self.limit_sqrt_price, &self.edge_sqrt_price) - { - // Case 2. lim_quantity is the lowest - // The trade also completely happens within one tick, no tick crossing happens. - self.action = SwapStepAction::Stop; - self.final_price = self.limit_sqrt_price; - self.delta_in = Self::delta_in( - self.current_liquidity, - self.current_sqrt_price, - self.limit_sqrt_price, - ); - recalculate_fee = true; + // or exchange the full amount. + if Self::price_is_closer(&self.target_price, &self.limit_price) { + // Case 1. target_quantity is the lowest, execute in full + self.final_price = self.target_price; + self.delta_in = self.requested_delta_in; } else { - // Case 3. edge_quantity is the lowest - // Tick crossing is likely - self.action = SwapStepAction::Crossing; - self.delta_in = Self::delta_in( - self.current_liquidity, - self.current_sqrt_price, - self.edge_sqrt_price, - ); - self.final_price = self.edge_sqrt_price; + // Case 2. lim_quantity is the lowest + self.final_price = self.limit_price; + self.delta_in = Self::delta_in(self.netuid, self.current_price, self.limit_price); recalculate_fee = true; } - log::trace!("\tAction : {:?}", self.action); - log::trace!( - "\tCurrent Price : {}", - self.current_sqrt_price - .saturating_mul(self.current_sqrt_price) - ); - log::trace!( - "\tTarget Price : {}", - self.target_sqrt_price - .saturating_mul(self.target_sqrt_price) - ); - log::trace!( - "\tLimit Price : {}", - self.limit_sqrt_price.saturating_mul(self.limit_sqrt_price) - ); - log::trace!( - "\tEdge Price : {}", - self.edge_sqrt_price.saturating_mul(self.edge_sqrt_price) - ); + log::trace!("\tCurrent Price : {}", self.current_price); + log::trace!("\tTarget Price : {}", self.target_price); + log::trace!("\tLimit Price : {}", self.limit_price); log::trace!("\tDelta In : {}", self.delta_in); // Because on step creation we calculate fee off the total amount, we might need to - // recalculate it in case if we hit the limit price or the edge price. + // recalculate it in case if we hit the limit price. if recalculate_fee { let u16_max = U64F64::saturating_from_num(u16::MAX); let fee_rate = if self.drop_fees { @@ -170,345 +110,115 @@ where .saturating_to_num::() .into(); } - - // Now correct the action if we stopped exactly at the edge no matter what was the case - // above. Because order type buy moves the price up and tick semi-open interval doesn't - // include its right point, we cross on buys and stop on sells. - let natural_reason_stop_price = - if Self::price_is_closer(&self.limit_sqrt_price, &self.target_sqrt_price) { - self.limit_sqrt_price - } else { - self.target_sqrt_price - }; - if natural_reason_stop_price == self.edge_sqrt_price { - self.action = Self::action_on_edge_sqrt_price(); - } } /// Process a single step of a swap fn process_swap(&self) -> Result, Error> { + // Convert amounts, actual swap happens here let delta_out = Self::convert_deltas(self.netuid, self.delta_in); log::trace!("\tDelta Out : {delta_out}"); - let mut fee_to_block_author = 0.into(); - if self.delta_in > 0.into() { - ensure!(delta_out > 0.into(), Error::::ReservesTooLow); - - // Split fees according to DefaultFeeSplit between liquidity pool and - // validators. In case we want just to forward 100% of fees to the block - // author, it can be done this way: - // ``` - // fee_to_block_author = self.fee; - // ``` - let fee_split = DefaultFeeSplit::get(); - let lp_fee: PaidIn = fee_split.mul_floor(self.fee.to_u64()).into(); - - // Hold the reserve portion of fees - if !lp_fee.is_zero() { - Self::add_fees( - self.netuid, - Pallet::::current_liquidity_safe(self.netuid), - lp_fee, - ); - } + if !self.delta_in.is_zero() { + ensure!(!delta_out.is_zero(), Error::::ReservesTooLow); - fee_to_block_author = self.fee.saturating_sub(lp_fee); + // 100% of swap fees to to block builder + fee_to_block_author = self.fee; } - if self.action == SwapStepAction::Crossing { - let mut tick = Ticks::::get(self.netuid, self.edge_tick).unwrap_or_default(); - tick.fees_out_tao = I64F64::saturating_from_num(FeeGlobalTao::::get(self.netuid)) - .saturating_sub(tick.fees_out_tao); - tick.fees_out_alpha = - I64F64::saturating_from_num(FeeGlobalAlpha::::get(self.netuid)) - .saturating_sub(tick.fees_out_alpha); - Self::update_liquidity_at_crossing(self.netuid)?; - Ticks::::insert(self.netuid, self.edge_tick, tick); - } - - // Update current price - AlphaSqrtPrice::::set(self.netuid, self.final_price); - - // Update current tick - let new_current_tick = TickIndex::from_sqrt_price_bounded(self.final_price); - CurrentTick::::set(self.netuid, new_current_tick); - Ok(SwapStepResult { - amount_to_take: self.delta_in.saturating_add(self.fee), fee_paid: self.fee, delta_in: self.delta_in, delta_out, fee_to_block_author, }) } - - pub(crate) fn action(&self) -> SwapStepAction { - self.action - } } impl SwapStep for BasicSwapStep { - fn delta_in( - liquidity_curr: U64F64, - sqrt_price_curr: SqrtPrice, - sqrt_price_target: SqrtPrice, - ) -> TaoBalance { - liquidity_curr - .saturating_mul(sqrt_price_target.saturating_sub(sqrt_price_curr)) - .saturating_to_num::() - .into() + fn delta_in(netuid: NetUid, price_curr: U64F64, price_target: U64F64) -> TaoBalance { + let tao_reserve = T::TaoReserve::reserve(netuid.into()); + let balancer = SwapBalancer::::get(netuid); + TaoBalance::from(balancer.calculate_quote_delta_in( + price_curr, + price_target, + tao_reserve.into(), + )) } - fn tick_edge(netuid: NetUid, current_tick: TickIndex) -> TickIndex { - ActiveTickIndexManager::::find_closest_higher( - netuid, - current_tick.next().unwrap_or(TickIndex::MAX), + fn price_target(netuid: NetUid, delta_in: TaoBalance) -> U64F64 { + let tao_reserve = T::TaoReserve::reserve(netuid.into()); + let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); + let balancer = SwapBalancer::::get(netuid); + let dy = delta_in; + let dx = Self::convert_deltas(netuid, dy); + balancer.calculate_price( + u64::from(alpha_reserve.saturating_sub(dx)), + u64::from(tao_reserve.saturating_add(dy)), ) - .unwrap_or(TickIndex::MAX) } - fn sqrt_price_target( - liquidity_curr: U64F64, - sqrt_price_curr: SqrtPrice, - delta_in: TaoBalance, - ) -> SqrtPrice { - let delta_fixed = U64F64::saturating_from_num(delta_in); - - // No liquidity means that price should go to the limit - if liquidity_curr == 0 { - return SqrtPrice::saturating_from_num( - Pallet::::max_price_inner::().to_u64(), - ); - } - - delta_fixed - .safe_div(liquidity_curr) - .saturating_add(sqrt_price_curr) - } - - fn price_is_closer(sq_price1: &SqrtPrice, sq_price2: &SqrtPrice) -> bool { - sq_price1 <= sq_price2 - } - - fn action_on_edge_sqrt_price() -> SwapStepAction { - SwapStepAction::Crossing - } - - fn add_fees(netuid: NetUid, current_liquidity: U64F64, fee: TaoBalance) { - if current_liquidity == 0 { - return; - } - - let fee_fixed = U64F64::saturating_from_num(fee.to_u64()); - - FeeGlobalTao::::mutate(netuid, |value| { - *value = value.saturating_add(fee_fixed.safe_div(current_liquidity)) - }); + fn price_is_closer(price1: &U64F64, price2: &U64F64) -> bool { + price1 <= price2 } fn convert_deltas(netuid: NetUid, delta_in: TaoBalance) -> AlphaBalance { - // Skip conversion if delta_in is zero - if delta_in.is_zero() { - return AlphaBalance::ZERO; - } - - let liquidity_curr = SqrtPrice::saturating_from_num(CurrentLiquidity::::get(netuid)); - let sqrt_price_curr = AlphaSqrtPrice::::get(netuid); - let delta_fixed = SqrtPrice::saturating_from_num(delta_in.to_u64()); - - // Calculate result based on order type with proper fixed-point math - // Using safe math operations throughout to prevent overflows - let result = { - // (liquidity_curr * sqrt_price_curr + delta_fixed) * sqrt_price_curr; - let a = liquidity_curr - .saturating_mul(sqrt_price_curr) - .saturating_add(delta_fixed) - .saturating_mul(sqrt_price_curr); - // liquidity_curr / a; - let b = liquidity_curr.safe_div(a); - // b * delta_fixed; - b.saturating_mul(delta_fixed) - }; - - result.saturating_to_num::().into() - } - - fn update_liquidity_at_crossing(netuid: NetUid) -> Result<(), Error> { - let mut liquidity_curr = CurrentLiquidity::::get(netuid); - let current_tick_index = TickIndex::current_bounded::(netuid); - - // Find the appropriate tick based on order type - let tick = { - // Self::find_closest_higher_active_tick(netuid, current_tick_index), - let upper_tick = ActiveTickIndexManager::::find_closest_higher( - netuid, - current_tick_index.next().unwrap_or(TickIndex::MAX), - ) - .unwrap_or(TickIndex::MAX); - Ticks::::get(netuid, upper_tick) - } - .ok_or(Error::::InsufficientLiquidity)?; - - let liquidity_update_abs_u64 = tick.liquidity_net_as_u64(); - - // Update liquidity based on the sign of liquidity_net and the order type - liquidity_curr = if tick.liquidity_net >= 0 { - liquidity_curr.saturating_add(liquidity_update_abs_u64) - } else { - liquidity_curr.saturating_sub(liquidity_update_abs_u64) - }; - - CurrentLiquidity::::set(netuid, liquidity_curr); - - Ok(()) + let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); + let tao_reserve = T::TaoReserve::reserve(netuid.into()); + let balancer = SwapBalancer::::get(netuid); + let e = balancer.exp_quote_base(tao_reserve.into(), delta_in.into()); + let one = U64F64::from_num(1); + let alpha_reserve_fixed = U64F64::from_num(alpha_reserve); + AlphaBalance::from( + alpha_reserve_fixed + .saturating_mul(one.saturating_sub(e)) + .saturating_to_num::(), + ) } } impl SwapStep for BasicSwapStep { - fn delta_in( - liquidity_curr: U64F64, - sqrt_price_curr: SqrtPrice, - sqrt_price_target: SqrtPrice, - ) -> AlphaBalance { - let one = U64F64::saturating_from_num(1); - - liquidity_curr - .saturating_mul( - one.safe_div(sqrt_price_target.into()) - .saturating_sub(one.safe_div(sqrt_price_curr)), - ) - .saturating_to_num::() - .into() + fn delta_in(netuid: NetUid, price_curr: U64F64, price_target: U64F64) -> AlphaBalance { + let alpha_reserve = T::AlphaReserve::reserve(netuid); + let balancer = SwapBalancer::::get(netuid); + AlphaBalance::from(balancer.calculate_base_delta_in( + price_curr, + price_target, + alpha_reserve.into(), + )) } - fn tick_edge(netuid: NetUid, current_tick: TickIndex) -> TickIndex { - let current_price: SqrtPrice = AlphaSqrtPrice::::get(netuid); - let current_tick_price = current_tick.as_sqrt_price_bounded(); - let is_active = ActiveTickIndexManager::::tick_is_active(netuid, current_tick); - - if is_active && current_price > current_tick_price { - return ActiveTickIndexManager::::find_closest_lower(netuid, current_tick) - .unwrap_or(TickIndex::MIN); - } - - ActiveTickIndexManager::::find_closest_lower( - netuid, - current_tick.prev().unwrap_or(TickIndex::MIN), - ) - .unwrap_or(TickIndex::MIN) - } - - fn sqrt_price_target( - liquidity_curr: U64F64, - sqrt_price_curr: SqrtPrice, - delta_in: AlphaBalance, - ) -> SqrtPrice { - let delta_fixed = U64F64::saturating_from_num(delta_in); - let one = U64F64::saturating_from_num(1); - - // No liquidity means that price should go to the limit - if liquidity_curr == 0 { - return SqrtPrice::saturating_from_num( - Pallet::::min_price_inner::().to_u64(), - ); - } - - one.safe_div( - delta_fixed - .safe_div(liquidity_curr) - .saturating_add(one.safe_div(sqrt_price_curr)), + fn price_target(netuid: NetUid, delta_in: AlphaBalance) -> U64F64 { + let tao_reserve = T::TaoReserve::reserve(netuid.into()); + let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); + let balancer = SwapBalancer::::get(netuid); + let dx = delta_in; + let dy = Self::convert_deltas(netuid, dx); + balancer.calculate_price( + u64::from(alpha_reserve.saturating_add(dx)), + u64::from(tao_reserve.saturating_sub(dy)), ) } - fn price_is_closer(sq_price1: &SqrtPrice, sq_price2: &SqrtPrice) -> bool { - sq_price1 >= sq_price2 - } - - fn action_on_edge_sqrt_price() -> SwapStepAction { - SwapStepAction::Stop - } - - fn add_fees(netuid: NetUid, current_liquidity: U64F64, fee: AlphaBalance) { - if current_liquidity == 0 { - return; - } - - let fee_fixed = U64F64::saturating_from_num(fee.to_u64()); - - FeeGlobalAlpha::::mutate(netuid, |value| { - *value = value.saturating_add(fee_fixed.safe_div(current_liquidity)) - }); + fn price_is_closer(price1: &U64F64, price2: &U64F64) -> bool { + price1 >= price2 } fn convert_deltas(netuid: NetUid, delta_in: AlphaBalance) -> TaoBalance { - // Skip conversion if delta_in is zero - if delta_in.is_zero() { - return TaoBalance::ZERO; - } - - let liquidity_curr = SqrtPrice::saturating_from_num(CurrentLiquidity::::get(netuid)); - let sqrt_price_curr = AlphaSqrtPrice::::get(netuid); - let delta_fixed = SqrtPrice::saturating_from_num(delta_in.to_u64()); - - // Calculate result based on order type with proper fixed-point math - // Using safe math operations throughout to prevent overflows - let result = { - // liquidity_curr / (liquidity_curr / sqrt_price_curr + delta_fixed); - let denom = liquidity_curr - .safe_div(sqrt_price_curr) - .saturating_add(delta_fixed); - let a = liquidity_curr.safe_div(denom); - // a * sqrt_price_curr; - let b = a.saturating_mul(sqrt_price_curr); - - // delta_fixed * b; - delta_fixed.saturating_mul(b) - }; - - result.saturating_to_num::().into() - } - - fn update_liquidity_at_crossing(netuid: NetUid) -> Result<(), Error> { - let mut liquidity_curr = CurrentLiquidity::::get(netuid); - let current_tick_index = TickIndex::current_bounded::(netuid); - - // Find the appropriate tick based on order type - let tick = { - // Self::find_closest_lower_active_tick(netuid, current_tick_index) - let current_price = AlphaSqrtPrice::::get(netuid); - let current_tick_price = current_tick_index.as_sqrt_price_bounded(); - let is_active = ActiveTickIndexManager::::tick_is_active(netuid, current_tick_index); - - let lower_tick = if is_active && current_price > current_tick_price { - ActiveTickIndexManager::::find_closest_lower(netuid, current_tick_index) - .unwrap_or(TickIndex::MIN) - } else { - ActiveTickIndexManager::::find_closest_lower( - netuid, - current_tick_index.prev().unwrap_or(TickIndex::MIN), - ) - .unwrap_or(TickIndex::MIN) - }; - Ticks::::get(netuid, lower_tick) - } - .ok_or(Error::::InsufficientLiquidity)?; - - let liquidity_update_abs_u64 = tick.liquidity_net_as_u64(); - - // Update liquidity based on the sign of liquidity_net and the order type - liquidity_curr = if tick.liquidity_net >= 0 { - liquidity_curr.saturating_sub(liquidity_update_abs_u64) - } else { - liquidity_curr.saturating_add(liquidity_update_abs_u64) - }; - - CurrentLiquidity::::set(netuid, liquidity_curr); - - Ok(()) + let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); + let tao_reserve = T::TaoReserve::reserve(netuid.into()); + let balancer = SwapBalancer::::get(netuid); + let e = balancer.exp_base_quote(alpha_reserve.into(), delta_in.into()); + let one = U64F64::from_num(1); + let tao_reserve_fixed = U64F64::from_num(u64::from(tao_reserve)); + TaoBalance::from( + tao_reserve_fixed + .saturating_mul(one.saturating_sub(e)) + .saturating_to_num::(), + ) } } @@ -519,49 +229,21 @@ where PaidOut: Token, { /// Get the input amount needed to reach the target price - fn delta_in( - liquidity_curr: U64F64, - sqrt_price_curr: SqrtPrice, - sqrt_price_target: SqrtPrice, - ) -> PaidIn; + fn delta_in(netuid: NetUid, price_curr: U64F64, price_target: U64F64) -> PaidIn; - /// Get the tick at the current tick edge. - /// - /// If anything is wrong with tick math and it returns Err, we just abort the deal, i.e. return - /// the edge that is impossible to execute - fn tick_edge(netuid: NetUid, current_tick: TickIndex) -> TickIndex; - - /// Get the target square root price based on the input amount - /// - /// This is the price that would be reached if - /// - There are no liquidity positions other than protocol liquidity - /// - Full delta_in amount is executed - fn sqrt_price_target( - liquidity_curr: U64F64, - sqrt_price_curr: SqrtPrice, - delta_in: PaidIn, - ) -> SqrtPrice; + /// Get the target price based on the input amount + fn price_target(netuid: NetUid, delta_in: PaidIn) -> U64F64; - /// Returns True if sq_price1 is closer to the current price than sq_price2 - /// in terms of order direction. - /// For buying: sq_price1 <= sq_price2 - /// For selling: sq_price1 >= sq_price2 - fn price_is_closer(sq_price1: &SqrtPrice, sq_price2: &SqrtPrice) -> bool; - - /// Get swap step action on the edge sqrt price. - fn action_on_edge_sqrt_price() -> SwapStepAction; - - /// Add fees to the global fee counters - fn add_fees(netuid: NetUid, current_liquidity: U64F64, fee: PaidIn); + /// Returns True if price1 is closer to the current price than price2 + /// For buying: price1 <= price2 + /// For selling: price1 >= price2 + fn price_is_closer(price1: &U64F64, price2: &U64F64) -> bool; /// Convert input amount (delta_in) to output amount (delta_out) /// - /// This is the core method of uniswap V3 that tells how much output token is given for an + /// This is the core method of the swap that tells how much output token is given for an /// amount of input token within one price tick. fn convert_deltas(netuid: NetUid, delta_in: PaidIn) -> PaidOut; - - /// Update liquidity when crossing a tick - fn update_liquidity_at_crossing(netuid: NetUid) -> Result<(), Error>; } #[derive(Debug, PartialEq)] @@ -570,15 +252,8 @@ where PaidIn: Token, PaidOut: Token, { - pub(crate) amount_to_take: PaidIn, pub(crate) fee_paid: PaidIn, pub(crate) delta_in: PaidIn, pub(crate) delta_out: PaidOut, pub(crate) fee_to_block_author: PaidIn, } - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum SwapStepAction { - Crossing, - Stop, -} diff --git a/pallets/swap/src/pallet/tests.rs b/pallets/swap/src/pallet/tests.rs index 5d75c7da27..b1071294d3 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -6,76 +6,30 @@ )] use approx::assert_abs_diff_eq; -use frame_support::{assert_err, assert_noop, assert_ok}; -use sp_arithmetic::helpers_128bit; +use frame_support::{assert_noop, assert_ok}; +use sp_arithmetic::Perquintill; use sp_runtime::DispatchError; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::U64F64; use subtensor_runtime_common::{NetUid, Token}; use subtensor_swap_interface::Order as OrderT; use super::*; +use crate::mock::*; use crate::pallet::swap_step::*; -use crate::{SqrtPrice, mock::*}; - -// this function is used to convert price (NON-SQRT price!) to TickIndex. it's only utility for -// testing, all the implementation logic is based on sqrt prices -fn price_to_tick(price: f64) -> TickIndex { - let price_sqrt: SqrtPrice = SqrtPrice::from_num(price.sqrt()); - // Handle potential errors in the conversion - match TickIndex::try_from_sqrt_price(price_sqrt) { - Ok(mut tick) => { - // Ensure the tick is within bounds - if tick > TickIndex::MAX { - tick = TickIndex::MAX; - } else if tick < TickIndex::MIN { - tick = TickIndex::MIN; - } - tick - } - // Default to a reasonable value when conversion fails - Err(_) => { - if price > 1.0 { - TickIndex::MAX - } else { - TickIndex::MIN - } - } - } -} -fn get_ticked_prices_around_current_price() -> (f64, f64) { - // Get current price, ticks around it, and prices on the tick edges for test cases - let netuid = NetUid::from(1); - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - let current_tick = CurrentTick::::get(netuid); - - // Low and high prices that match to a lower and higher tick that doesn't contain the current price - let current_price_low_sqrt = current_tick.as_sqrt_price_bounded(); - let current_price_high_sqrt = current_tick.next().unwrap().as_sqrt_price_bounded(); - let current_price_low = U96F32::from_num(current_price_low_sqrt * current_price_low_sqrt); - let current_price_high = U96F32::from_num(current_price_high_sqrt * current_price_high_sqrt); - - ( - current_price_low.to_num::(), - current_price_high.to_num::() + 0.000000001, - ) +// Run all tests: +// cargo test --package pallet-subtensor-swap --lib -- pallet::tests --nocapture + +#[allow(dead_code)] +fn get_min_price() -> U64F64 { + U64F64::from_num(Pallet::::min_price_inner::()) + / U64F64::from_num(1_000_000_000) } -// this function is used to convert tick index NON-SQRT (!) price. it's only utility for -// testing, all the implementation logic is based on sqrt prices -fn tick_to_price(tick: TickIndex) -> f64 { - // Handle errors gracefully - match tick.try_to_sqrt_price() { - Ok(price_sqrt) => (price_sqrt * price_sqrt).to_num::(), - Err(_) => { - // Return a sensible default based on whether the tick is above or below the valid range - if tick > TickIndex::MAX { - tick_to_price(TickIndex::MAX) // Use the max valid tick price - } else { - tick_to_price(TickIndex::MIN) // Use the min valid tick price - } - } - } +#[allow(dead_code)] +fn get_max_price() -> U64F64 { + U64F64::from_num(Pallet::::max_price_inner::()) + / U64F64::from_num(1_000_000_000) } mod dispatchables { @@ -105,607 +59,363 @@ mod dispatchables { ); }); } -} - -#[test] -fn test_swap_initialization() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - // Get reserves from the mock provider - let tao = TaoReserve::reserve(netuid.into()); - let alpha = AlphaReserve::reserve(netuid.into()); - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - assert!(SwapV3Initialized::::get(netuid)); - - // Verify current price is set - let sqrt_price = AlphaSqrtPrice::::get(netuid); - let expected_sqrt_price = U64F64::from_num(0.5_f64); - assert_abs_diff_eq!( - sqrt_price.to_num::(), - expected_sqrt_price.to_num::(), - epsilon = 0.000000001 - ); - // Verify that current tick is set - let current_tick = CurrentTick::::get(netuid); - let expected_current_tick = TickIndex::from_sqrt_price_bounded(expected_sqrt_price); - assert_eq!(current_tick, expected_current_tick); - - // Calculate expected liquidity - let expected_liquidity = - helpers_128bit::sqrt((tao.to_u64() as u128).saturating_mul(alpha.to_u64() as u128)) - as u64; - - // Get the protocol account - let protocol_account_id = Pallet::::protocol_account_id(); - - // Verify position created for protocol account - let positions = Positions::::iter_prefix_values((netuid, protocol_account_id)) - .collect::>(); - assert_eq!(positions.len(), 1); - - let position = &positions[0]; - assert_eq!(position.liquidity, expected_liquidity); - assert_eq!(position.tick_low, TickIndex::MIN); - assert_eq!(position.tick_high, TickIndex::MAX); - assert_eq!(position.fees_tao, 0); - assert_eq!(position.fees_alpha, 0); - - // Verify ticks were created - let tick_low = Ticks::::get(netuid, TickIndex::MIN).unwrap(); - let tick_high = Ticks::::get(netuid, TickIndex::MAX).unwrap(); - - // Check liquidity values - assert_eq!(tick_low.liquidity_net, expected_liquidity as i128); - assert_eq!(tick_low.liquidity_gross, expected_liquidity); - assert_eq!(tick_high.liquidity_net, -(expected_liquidity as i128)); - assert_eq!(tick_high.liquidity_gross, expected_liquidity); - - // Verify current liquidity is set - assert_eq!(CurrentLiquidity::::get(netuid), expected_liquidity); - }); -} + fn perquintill_to_f64(p: Perquintill) -> f64 { + let parts = p.deconstruct() as f64; + parts / 1_000_000_000_000_000_000_f64 + } -// Test adding liquidity on top of the existing protocol liquidity -#[test] -fn test_add_liquidity_basic() { - new_test_ext().execute_with(|| { - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let max_tick = price_to_tick(max_price); - assert_eq!(max_tick, TickIndex::MAX); - - assert_ok!(Pallet::::maybe_initialize_v3(NetUid::from(1))); - let current_price = Pallet::::current_price(NetUid::from(1)).to_num::(); - let (current_price_low, current_price_high) = get_ticked_prices_around_current_price(); - - // As a user add liquidity with all possible corner cases - // - Initial price is 0.25 - // - liquidity is expressed in RAO units - // Test case is (price_low, price_high, liquidity, tao, alpha) + /// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::dispatchables::test_adjust_protocol_liquidity_happy --exact --nocapture + #[test] + fn test_adjust_protocol_liquidity_happy() { + // test case: tao_delta, alpha_delta [ - // Repeat the protocol liquidity at maximum range: Expect all the same values - ( - min_price, - max_price, - 2_000_000_000_u64, - 1_000_000_000_u64, - 4_000_000_000_u64, - ), - // Repeat the protocol liquidity at current to max range: Expect the same alpha - ( - current_price_high, - max_price, - 2_000_000_000_u64, - 0, - 4_000_000_000, - ), - // Repeat the protocol liquidity at min to current range: Expect all the same tao - ( - min_price, - current_price_low, - 2_000_000_000_u64, - 1_000_000_000, - 0, - ), - // Half to double price - just some sane wothdraw amounts - (0.125, 0.5, 2_000_000_000_u64, 293_000_000, 1_171_000_000), - // Both below price - tao is non-zero, alpha is zero - (0.12, 0.13, 2_000_000_000_u64, 28_270_000, 0), - // Both above price - tao is zero, alpha is non-zero - (0.3, 0.4, 2_000_000_000_u64, 0, 489_200_000), + (0_u64, 0_u64), + (0_u64, 1_u64), + (1_u64, 0_u64), + (1_u64, 1_u64), + (0_u64, 10_u64), + (10_u64, 0_u64), + (10_u64, 10_u64), + (0_u64, 100_u64), + (100_u64, 0_u64), + (100_u64, 100_u64), + (0_u64, 1_000_u64), + (1_000_u64, 0_u64), + (1_000_u64, 1_000_u64), + (1_000_000_u64, 0_u64), + (0_u64, 1_000_000_u64), + (1_000_000_u64, 1_000_000_u64), + (1_000_000_000_u64, 0_u64), + (0_u64, 1_000_000_000_u64), + (1_000_000_000_u64, 1_000_000_000_u64), + (1_000_000_000_000_u64, 0_u64), + (0_u64, 1_000_000_000_000_u64), + (1_000_000_000_000_u64, 1_000_000_000_000_u64), + (1_u64, 2_u64), + (2_u64, 1_u64), + (10_u64, 20_u64), + (20_u64, 10_u64), + (100_u64, 200_u64), + (200_u64, 100_u64), + (1_000_u64, 2_000_u64), + (2_000_u64, 1_000_u64), + (1_000_000_u64, 2_000_000_u64), + (2_000_000_u64, 1_000_000_u64), + (1_000_000_000_u64, 2_000_000_000_u64), + (2_000_000_000_u64, 1_000_000_000_u64), + (1_000_000_000_000_u64, 2_000_000_000_000_u64), + (2_000_000_000_000_u64, 1_000_000_000_000_u64), + (1_234_567_u64, 2_432_765_u64), + (1_234_567_u64, 2_432_765_890_u64), ] .into_iter() - .enumerate() - .map(|(n, v)| (NetUid::from(n as u16 + 1), v.0, v.1, v.2, v.3, v.4)) - .for_each( - |(netuid, price_low, price_high, liquidity, expected_tao, expected_alpha)| { - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Calculate ticks (assuming tick math is tested separately) - let tick_low = price_to_tick(price_low); - let tick_high = price_to_tick(price_high); - - // Get tick infos and liquidity before adding (to account for protocol liquidity) - let tick_low_info_before = Ticks::::get(netuid, tick_low).unwrap_or_default(); - let tick_high_info_before = - Ticks::::get(netuid, tick_high).unwrap_or_default(); - let liquidity_before = CurrentLiquidity::::get(netuid); - - // Add liquidity - let (position_id, tao, alpha) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, - ) - .unwrap(); - - assert_abs_diff_eq!(tao, expected_tao, epsilon = tao / 1000); - assert_abs_diff_eq!(alpha, expected_alpha, epsilon = alpha / 1000); - - // Check that low and high ticks appear in the state and are properly updated - let tick_low_info = Ticks::::get(netuid, tick_low).unwrap(); - let tick_high_info = Ticks::::get(netuid, tick_high).unwrap(); - let expected_liquidity_net_low = liquidity as i128; - let expected_liquidity_gross_low = liquidity; - let expected_liquidity_net_high = -(liquidity as i128); - let expected_liquidity_gross_high = liquidity; - - assert_eq!( - tick_low_info.liquidity_net - tick_low_info_before.liquidity_net, - expected_liquidity_net_low, - ); - assert_eq!( - tick_low_info.liquidity_gross - tick_low_info_before.liquidity_gross, - expected_liquidity_gross_low, - ); - assert_eq!( - tick_high_info.liquidity_net - tick_high_info_before.liquidity_net, - expected_liquidity_net_high, - ); - assert_eq!( - tick_high_info.liquidity_gross - tick_high_info_before.liquidity_gross, - expected_liquidity_gross_high, + .for_each(|(tao_delta, alpha_delta)| { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let tao_delta = TaoBalance::from(tao_delta); + let alpha_delta = AlphaBalance::from(alpha_delta); + + // Initialize reserves and price + let tao = TaoBalance::from(1_000_000_000_000_u64); + let alpha = AlphaBalance::from(4_000_000_000_000_u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + let price_before = Swap::current_price(netuid); + + // Adjust reserves + Swap::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta); + TaoReserve::set_mock_reserve(netuid, tao + tao_delta); + AlphaReserve::set_mock_reserve(netuid, alpha + alpha_delta); + + // Check that price didn't change + let price_after = Swap::current_price(netuid); + assert_abs_diff_eq!( + price_before.to_num::(), + price_after.to_num::(), + epsilon = price_before.to_num::() / 1_000_000_000_000. ); - // Liquidity position at correct ticks - assert_eq!( - Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID), - 1 + // Check that reserve weight was properly updated + let new_tao = u64::from(tao + tao_delta) as f64; + let new_alpha = u64::from(alpha + alpha_delta) as f64; + let expected_quote_weight = + new_tao / (new_alpha * price_before.to_num::() + new_tao); + let expected_quote_weight_delta = expected_quote_weight - 0.5; + let res_weights = SwapBalancer::::get(netuid); + let actual_quote_weight_delta = + perquintill_to_f64(res_weights.get_quote_weight()) - 0.5; + let eps = expected_quote_weight / 1_000_000_000_000.; + assert_abs_diff_eq!( + expected_quote_weight_delta, + actual_quote_weight_delta, + epsilon = eps ); + }); + }); + } - let position = - Positions::::get((netuid, OK_COLDKEY_ACCOUNT_ID, position_id)).unwrap(); - assert_eq!(position.liquidity, liquidity); - assert_eq!(position.tick_low, tick_low); - assert_eq!(position.tick_high, tick_high); - assert_eq!(position.fees_alpha, 0); - assert_eq!(position.fees_tao, 0); - - // Current liquidity is updated only when price range includes the current price - let expected_liquidity = - if (price_high > current_price) && (price_low <= current_price) { - liquidity_before + liquidity - } else { - liquidity_before - }; - - assert_eq!(CurrentLiquidity::::get(netuid), expected_liquidity) - }, - ); - }); -} - -#[test] -fn test_add_liquidity_max_limit_enforced() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let liquidity = 2_000_000_000_u64; - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - let limit = MaxPositions::get() as usize; - - for _ in 0..limit { - Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - TickIndex::MIN, - TickIndex::MAX, - liquidity, - ) - .unwrap(); - } - - let test_result = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - TickIndex::MIN, - TickIndex::MAX, - liquidity, - ); - - assert_err!(test_result, Error::::MaxPositionsExceeded); - }); -} - -#[test] -fn test_add_liquidity_out_of_bounds() { - new_test_ext().execute_with(|| { + /// This test case verifies that small gradual injections (like emissions in every block) + /// in the worst case + /// - Do not cause price to change + /// - Result in the same weight change as one large injection + /// + /// This is a long test that only tests validity of weights math. Run again if changing + /// Balancer::update_weights_for_added_liquidity + /// + /// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::dispatchables::test_adjust_protocol_liquidity_deltas --exact --nocapture + #[ignore] + #[test] + fn test_adjust_protocol_liquidity_deltas() { + // The number of times (blocks) over which gradual injections will be made + // One year price drift due to precision is under 1e-6 + const ITERATIONS: u64 = 2_700_000; + const PRICE_PRECISION: f64 = 0.000_001; + const PREC_LARGE_DELTA: f64 = 0.001; + const WEIGHT_PRECISION: f64 = 0.000_000_000_000_000_001; + + let initial_tao_reserve = TaoBalance::from(1_000_000_000_000_000_u64); + let initial_alpha_reserve = AlphaBalance::from(10_000_000_000_000_000_u64); + + // test case: tao_delta, alpha_delta, price_precision [ - // For our tests, we'll construct TickIndex values that are intentionally - // outside the valid range for testing purposes only - ( - TickIndex::new_unchecked(TickIndex::MIN.get() - 1), - TickIndex::MAX, - 1_000_000_000_u64, - ), - ( - TickIndex::MIN, - TickIndex::new_unchecked(TickIndex::MAX.get() + 1), - 1_000_000_000_u64, - ), - ( - TickIndex::new_unchecked(TickIndex::MIN.get() - 1), - TickIndex::new_unchecked(TickIndex::MAX.get() + 1), - 1_000_000_000_u64, - ), - ( - TickIndex::new_unchecked(TickIndex::MIN.get() - 100), - TickIndex::new_unchecked(TickIndex::MAX.get() + 100), - 1_000_000_000_u64, - ), - // Inverted ticks: high < low - ( - TickIndex::new_unchecked(-900), - TickIndex::new_unchecked(-1000), - 1_000_000_000_u64, - ), - // Equal ticks: high == low - ( - TickIndex::new_unchecked(-10_000), - TickIndex::new_unchecked(-10_000), - 1_000_000_000_u64, - ), + (0_u64, 0_u64, PRICE_PRECISION), + (0_u64, 1_u64, PRICE_PRECISION), + (1_u64, 0_u64, PRICE_PRECISION), + (1_u64, 1_u64, PRICE_PRECISION), + (0_u64, 10_u64, PRICE_PRECISION), + (10_u64, 0_u64, PRICE_PRECISION), + (10_u64, 10_u64, PRICE_PRECISION), + (0_u64, 100_u64, PRICE_PRECISION), + (100_u64, 0_u64, PRICE_PRECISION), + (100_u64, 100_u64, PRICE_PRECISION), + (0_u64, 987_u64, PRICE_PRECISION), + (987_u64, 0_u64, PRICE_PRECISION), + (876_u64, 987_u64, PRICE_PRECISION), + (0_u64, 1_000_u64, PRICE_PRECISION), + (1_000_u64, 0_u64, PRICE_PRECISION), + (1_000_u64, 1_000_u64, PRICE_PRECISION), + (0_u64, 1_234_u64, PRICE_PRECISION), + (1_234_u64, 0_u64, PRICE_PRECISION), + (1_234_u64, 4_321_u64, PRICE_PRECISION), + (1_234_000_u64, 4_321_000_u64, PREC_LARGE_DELTA), + (1_234_u64, 4_321_000_u64, PREC_LARGE_DELTA), ] .into_iter() - .enumerate() - .map(|(n, v)| (NetUid::from(n as u16 + 1), v.0, v.1, v.2)) - .for_each(|(netuid, tick_low, tick_high, liquidity)| { - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Add liquidity - assert_err!( - Swap::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity - ), - Error::::InvalidTickRange, - ); - }); - }); -} + .for_each(|(tao_delta, alpha_delta, price_precision)| { + new_test_ext().execute_with(|| { + let netuid1 = NetUid::from(1); + + let tao_delta = TaoBalance::from(tao_delta); + let alpha_delta = AlphaBalance::from(alpha_delta); + + // Initialize realistically large reserves + let mut tao = initial_tao_reserve; + let mut alpha = initial_alpha_reserve; + TaoReserve::set_mock_reserve(netuid1, tao); + AlphaReserve::set_mock_reserve(netuid1, alpha); + let price_before = Swap::current_price(netuid1); + + // Adjust reserves gradually + for _ in 0..ITERATIONS { + Swap::adjust_protocol_liquidity(netuid1, tao_delta, alpha_delta); + tao += tao_delta; + alpha += alpha_delta; + TaoReserve::set_mock_reserve(netuid1, tao); + AlphaReserve::set_mock_reserve(netuid1, alpha); + } + // Check that price didn't change + let price_after = Swap::current_price(netuid1); + assert_abs_diff_eq!( + price_before.to_num::(), + price_after.to_num::(), + epsilon = price_precision + ); -#[test] -fn test_add_liquidity_over_balance() { - new_test_ext().execute_with(|| { - let coldkey_account_id = 3; - let hotkey_account_id = 1002; + ///////////////////////// + // Now do one-time big injection with another netuid and compare weights + let netuid2 = NetUid::from(2); + + // Initialize same large reserves + TaoReserve::set_mock_reserve(netuid2, initial_tao_reserve); + AlphaReserve::set_mock_reserve(netuid2, initial_alpha_reserve); + + // Adjust reserves by one large amount at once + let tao_delta_once = TaoBalance::from(ITERATIONS * u64::from(tao_delta)); + let alpha_delta_once = AlphaBalance::from(ITERATIONS * u64::from(alpha_delta)); + Swap::adjust_protocol_liquidity(netuid2, tao_delta_once, alpha_delta_once); + TaoReserve::set_mock_reserve(netuid2, initial_tao_reserve + tao_delta_once); + AlphaReserve::set_mock_reserve(netuid2, initial_alpha_reserve + alpha_delta_once); + + // Compare reserve weights for netuid 1 and 2 + let res_weights1 = SwapBalancer::::get(netuid1); + let res_weights2 = SwapBalancer::::get(netuid2); + let actual_quote_weight1 = perquintill_to_f64(res_weights1.get_quote_weight()); + let actual_quote_weight2 = perquintill_to_f64(res_weights2.get_quote_weight()); + assert_abs_diff_eq!( + actual_quote_weight1, + actual_quote_weight2, + epsilon = WEIGHT_PRECISION + ); + }); + }); + } + /// Should work ok when initial alpha is zero + /// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::dispatchables::test_adjust_protocol_liquidity_zero_alpha --exact --nocapture + #[test] + fn test_adjust_protocol_liquidity_zero_alpha() { + // test case: tao_delta, alpha_delta [ - // Lower than price (not enough tao) - (0.1, 0.2, 100_000_000_000_u64), - // Higher than price (not enough alpha) - (0.3, 0.4, 100_000_000_000_u64), - // Around the price (not enough both) - (0.1, 0.4, 100_000_000_000_u64), + (0_u64, 0_u64), + (0_u64, 1_u64), + (1_u64, 0_u64), + (1_u64, 1_u64), + (0_u64, 10_u64), + (10_u64, 0_u64), + (10_u64, 10_u64), + (0_u64, 100_u64), + (100_u64, 0_u64), + (100_u64, 100_u64), + (0_u64, 1_000_u64), + (1_000_u64, 0_u64), + (1_000_u64, 1_000_u64), + (1_000_000_u64, 0_u64), + (0_u64, 1_000_000_u64), + (1_000_000_u64, 1_000_000_u64), + (1_000_000_000_u64, 0_u64), + (0_u64, 1_000_000_000_u64), + (1_000_000_000_u64, 1_000_000_000_u64), + (1_000_000_000_000_u64, 0_u64), + (0_u64, 1_000_000_000_000_u64), + (1_000_000_000_000_u64, 1_000_000_000_000_u64), + (1_u64, 2_u64), + (2_u64, 1_u64), + (10_u64, 20_u64), + (20_u64, 10_u64), + (100_u64, 200_u64), + (200_u64, 100_u64), + (1_000_u64, 2_000_u64), + (2_000_u64, 1_000_u64), + (1_000_000_u64, 2_000_000_u64), + (2_000_000_u64, 1_000_000_u64), + (1_000_000_000_u64, 2_000_000_000_u64), + (2_000_000_000_u64, 1_000_000_000_u64), + (1_000_000_000_000_u64, 2_000_000_000_000_u64), + (2_000_000_000_000_u64, 1_000_000_000_000_u64), + (1_234_567_u64, 2_432_765_u64), + (1_234_567_u64, 2_432_765_890_u64), ] .into_iter() - .enumerate() - .map(|(n, v)| (NetUid::from(n as u16 + 1), v.0, v.1, v.2)) - .for_each(|(netuid, price_low, price_high, liquidity)| { - // Calculate ticks - let tick_low = price_to_tick(price_low); - let tick_high = price_to_tick(price_high); - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Add liquidity - assert_err!( - Pallet::::do_add_liquidity( - netuid, - &coldkey_account_id, - &hotkey_account_id, - tick_low, - tick_high, - liquidity - ), - Error::::InsufficientBalance, - ); + .for_each(|(tao_delta, alpha_delta)| { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + let tao_delta = TaoBalance::from(tao_delta); + let alpha_delta = AlphaBalance::from(alpha_delta); + + // Initialize reserves and price + // broken state: Zero price because of zero alpha reserve + let tao = TaoBalance::from(1_000_000_000_000_u64); + let alpha = AlphaBalance::from(0_u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + let price_before = Swap::current_price(netuid); + assert_eq!(price_before, U64F64::from_num(0)); + let new_tao = u64::from(tao + tao_delta) as f64; + let new_alpha = u64::from(alpha + alpha_delta) as f64; + + // Adjust reserves + Swap::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta); + TaoReserve::set_mock_reserve(netuid, tao + tao_delta); + AlphaReserve::set_mock_reserve(netuid, alpha + alpha_delta); + + let res_weights = SwapBalancer::::get(netuid); + let actual_quote_weight = perquintill_to_f64(res_weights.get_quote_weight()); + + // Check that price didn't change + let price_after = Swap::current_price(netuid); + if new_alpha == 0. { + // If the pool state is still broken (∆x = 0), no change + assert_eq!(actual_quote_weight, 0.5); + assert_eq!(price_after, U64F64::from_num(0)); + } else { + // Price got fixed + let expected_price = new_tao / new_alpha; + assert_abs_diff_eq!( + expected_price, + price_after.to_num::(), + epsilon = price_before.to_num::() / 1_000_000_000_000. + ); + assert_eq!(actual_quote_weight, 0.5); + } + }); }); - }); + } } -// cargo test --package pallet-subtensor-swap --lib -- pallet::impls::tests::test_remove_liquidity_basic --exact --show-output #[test] -fn test_remove_liquidity_basic() { +fn test_swap_initialization() { new_test_ext().execute_with(|| { - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let max_tick = price_to_tick(max_price); - assert_eq!(max_tick, TickIndex::MAX); + let netuid = NetUid::from(1); - let (current_price_low, current_price_high) = get_ticked_prices_around_current_price(); + // Setup reserves + let tao = TaoBalance::from(1_000_000_000u64); + let alpha = AlphaBalance::from(4_000_000_000u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); - // As a user add liquidity with all possible corner cases - // - Initial price is 0.25 - // - liquidity is expressed in RAO units - // Test case is (price_low, price_high, liquidity, tao, alpha) - [ - // Repeat the protocol liquidity at maximum range: Expect all the same values - ( - min_price, - max_price, - 2_000_000_000_u64, - 1_000_000_000_u64, - 4_000_000_000_u64, - ), - // Repeat the protocol liquidity at current to max range: Expect the same alpha - ( - current_price_high, - max_price, - 2_000_000_000_u64, - 0, - 4_000_000_000, - ), - // Repeat the protocol liquidity at min to current range: Expect all the same tao - ( - min_price, - current_price_low, - 2_000_000_000_u64, - 1_000_000_000, - 0, - ), - // Half to double price - just some sane wothdraw amounts - (0.125, 0.5, 2_000_000_000_u64, 293_000_000, 1_171_000_000), - // Both below price - tao is non-zero, alpha is zero - (0.12, 0.13, 2_000_000_000_u64, 28_270_000, 0), - // Both above price - tao is zero, alpha is non-zero - (0.3, 0.4, 2_000_000_000_u64, 0, 489_200_000), - ] - .into_iter() - .enumerate() - .map(|(n, v)| (NetUid::from(n as u16 + 1), v.0, v.1, v.2, v.3, v.4)) - .for_each(|(netuid, price_low, price_high, liquidity, tao, alpha)| { - // Calculate ticks (assuming tick math is tested separately) - let tick_low = price_to_tick(price_low); - let tick_high = price_to_tick(price_high); - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - let liquidity_before = CurrentLiquidity::::get(netuid); - - // Add liquidity - let (position_id, _, _) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, - ) - .unwrap(); - - // Remove liquidity - let remove_result = - Pallet::::do_remove_liquidity(netuid, &OK_COLDKEY_ACCOUNT_ID, position_id) - .unwrap(); - assert_abs_diff_eq!(remove_result.tao.to_u64(), tao, epsilon = tao / 1000); - assert_abs_diff_eq!( - u64::from(remove_result.alpha), - alpha, - epsilon = alpha / 1000 - ); - assert_eq!(remove_result.fee_tao, TaoBalance::ZERO); - assert_eq!(remove_result.fee_alpha, AlphaBalance::ZERO); + assert_ok!(Pallet::::maybe_initialize_palswap(netuid, None)); + assert!(PalSwapInitialized::::get(netuid)); - // Liquidity position is removed - assert_eq!( - Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID), - 0 - ); - assert!(Positions::::get((netuid, OK_COLDKEY_ACCOUNT_ID, position_id)).is_none()); + // Verify current price is set + let price = Pallet::::current_price(netuid); + let expected_price = U64F64::from_num(0.25_f64); + assert_abs_diff_eq!( + price.to_num::(), + expected_price.to_num::(), + epsilon = 0.000000001 + ); - // Current liquidity is updated (back where it was) - assert_eq!(CurrentLiquidity::::get(netuid), liquidity_before); - }); + // Verify that swap reserve weight is initialized + let reserve_weight = SwapBalancer::::get(netuid); + assert_eq!( + reserve_weight.get_quote_weight(), + Perquintill::from_rational(1_u64, 2_u64), + ); }); } #[test] -fn test_remove_liquidity_nonexisting_position() { +fn test_swap_initialization_with_price() { new_test_ext().execute_with(|| { - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let max_tick = price_to_tick(max_price); - assert_eq!(max_tick.get(), TickIndex::MAX.get()); - - let liquidity = 2_000_000_000_u64; let netuid = NetUid::from(1); - // Calculate ticks (assuming tick math is tested separately) - let tick_low = price_to_tick(min_price); - let tick_high = price_to_tick(max_price); + // Setup reserves, tao / alpha = 0.25 + let tao = TaoBalance::from(1_000_000_000u64); + let alpha = AlphaBalance::from(4_000_000_000u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); - // Setup swap - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Add liquidity - assert_ok!(Pallet::::do_add_liquidity( + // Initialize with 0.2 price + assert_ok!(Pallet::::maybe_initialize_palswap( netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, + Some(U64F64::from(1u16) / U64F64::from(5u16)) )); + assert!(PalSwapInitialized::::get(netuid)); - assert!(Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID) > 0); - - // Remove liquidity - assert_err!( - Pallet::::do_remove_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - PositionId::new::() - ), - Error::::LiquidityNotFound, + // Verify current price is set to 0.2 + let price = Pallet::::current_price(netuid); + let expected_price = U64F64::from_num(0.2_f64); + assert_abs_diff_eq!( + price.to_num::(), + expected_price.to_num::(), + epsilon = 0.000000001 ); }); } -// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_modify_position_basic --exact --show-output -#[test] -fn test_modify_position_basic() { - new_test_ext().execute_with(|| { - let max_price = tick_to_price(TickIndex::MAX); - let max_tick = price_to_tick(max_price); - let limit_price = 1000.0_f64; - assert_eq!(max_tick, TickIndex::MAX); - let (current_price_low, _current_price_high) = get_ticked_prices_around_current_price(); - - // As a user add liquidity with all possible corner cases - // - Initial price is 0.25 - // - liquidity is expressed in RAO units - // Test case is (price_low, price_high, liquidity, tao, alpha) - [ - // Repeat the protocol liquidity at current to max range: Expect the same alpha - ( - current_price_low, - max_price, - 2_000_000_000_u64, - 4_000_000_000, - ), - ] - .into_iter() - .enumerate() - .map(|(n, v)| (NetUid::from(n as u16 + 1), v.0, v.1, v.2, v.3)) - .for_each(|(netuid, price_low, price_high, liquidity, alpha)| { - // Calculate ticks (assuming tick math is tested separately) - let tick_low = price_to_tick(price_low); - let tick_high = price_to_tick(price_high); - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Add liquidity - let (position_id, _, _) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, - ) - .unwrap(); - - // Get tick infos before the swap/update - let tick_low_info_before = Ticks::::get(netuid, tick_low).unwrap(); - let tick_high_info_before = Ticks::::get(netuid, tick_high).unwrap(); - - // Swap to create fees on the position - let sqrt_limit_price = SqrtPrice::from_num((limit_price).sqrt()); - let order = GetAlphaForTao::with_amount(liquidity / 10); - Pallet::::do_swap(netuid, order, sqrt_limit_price, false, false).unwrap(); - - // Modify liquidity (also causes claiming of fees) - let liquidity_before = CurrentLiquidity::::get(netuid); - let modify_result = Pallet::::do_modify_position( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - position_id, - -((liquidity / 10) as i64), - ) - .unwrap(); - assert_abs_diff_eq!( - u64::from(modify_result.alpha), - alpha / 10, - epsilon = alpha / 1000 - ); - - // Block author may get all fees - // assert!(modify_result.fee_tao > TaoBalance::ZERO); - // assert_eq!(modify_result.fee_alpha, AlphaBalance::ZERO); - - // Liquidity position is reduced - assert_eq!( - Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID), - 1 - ); - - // Current liquidity is reduced with modify_position - assert!(CurrentLiquidity::::get(netuid) < liquidity_before); - - // Position liquidity is reduced - let position = - Positions::::get((netuid, OK_COLDKEY_ACCOUNT_ID, position_id)).unwrap(); - assert_eq!(position.liquidity, liquidity * 9 / 10); - assert_eq!(position.tick_low, tick_low); - assert_eq!(position.tick_high, tick_high); - - // Tick liquidity is updated properly for low and high position ticks - let tick_low_info_after = Ticks::::get(netuid, tick_low).unwrap(); - let tick_high_info_after = Ticks::::get(netuid, tick_high).unwrap(); - - assert_eq!( - tick_low_info_before.liquidity_net - (liquidity / 10) as i128, - tick_low_info_after.liquidity_net, - ); - assert_eq!( - tick_low_info_before.liquidity_gross - (liquidity / 10), - tick_low_info_after.liquidity_gross, - ); - assert_eq!( - tick_high_info_before.liquidity_net + (liquidity / 10) as i128, - tick_high_info_after.liquidity_net, - ); - assert_eq!( - tick_high_info_before.liquidity_gross - (liquidity / 10), - tick_high_info_after.liquidity_gross, - ); - - // Modify liquidity again (ensure fees aren't double-collected) - let modify_result = Pallet::::do_modify_position( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - position_id, - -((liquidity / 100) as i64), - ) - .unwrap(); - - assert_abs_diff_eq!( - u64::from(modify_result.alpha), - alpha / 100, - epsilon = alpha / 1000 - ); - assert_eq!(modify_result.fee_tao, TaoBalance::ZERO); - assert_eq!(modify_result.fee_alpha, AlphaBalance::ZERO); - }); - }); -} - -// cargo test --package pallet-subtensor-swap --lib -- pallet::impls::tests::test_swap_basic --exact --show-output +// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_swap_basic --exact --nocapture #[test] fn test_swap_basic() { new_test_ext().execute_with(|| { @@ -713,855 +423,278 @@ fn test_swap_basic() { netuid: NetUid, order: Order, limit_price: f64, - output_amount: u64, price_should_grow: bool, ) where Order: OrderT, - Order::PaidIn: GlobalFeeInfo, BasicSwapStep: SwapStep, { - // Consumed liquidity ticks - let tick_low = TickIndex::MIN; - let tick_high = TickIndex::MAX; - let liquidity = order.amount().to_u64(); + let swap_amount = order.amount().to_u64(); // Setup swap - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Get tick infos before the swap - let tick_low_info_before = Ticks::::get(netuid, tick_low).unwrap_or_default(); - let tick_high_info_before = Ticks::::get(netuid, tick_high).unwrap_or_default(); - let liquidity_before = CurrentLiquidity::::get(netuid); + // Price is 0.25 + let initial_tao_reserve = TaoBalance::from(1_000_000_000_u64); + let initial_alpha_reserve = AlphaBalance::from(4_000_000_000_u64); + TaoReserve::set_mock_reserve(netuid, initial_tao_reserve); + AlphaReserve::set_mock_reserve(netuid, initial_alpha_reserve); + assert_ok!(Pallet::::maybe_initialize_palswap(netuid, None)); // Get current price - let current_price = Pallet::::current_price(netuid); + let current_price_before = Pallet::::current_price(netuid); + + // Get reserves + let tao_reserve = TaoReserve::reserve(netuid.into()).to_u64(); + let alpha_reserve = AlphaReserve::reserve(netuid.into()).to_u64(); + + // Expected fee amount + let fee_rate = FeeRate::::get(netuid) as f64 / u16::MAX as f64; + let expected_fee = (swap_amount as f64 * fee_rate) as u64; + + // Calculate expected output amount using f64 math + // This is a simple case when w1 = w2 = 0.5, so there's no + // exponentiation needed + let x = alpha_reserve as f64; + let y = tao_reserve as f64; + let expected_output_amount = if price_should_grow { + x * (1.0 - y / (y + (swap_amount - expected_fee) as f64)) + } else { + y * (1.0 - x / (x + (swap_amount - expected_fee) as f64)) + }; // Swap - let sqrt_limit_price = SqrtPrice::from_num((limit_price).sqrt()); + let limit_price_fixed = U64F64::from_num(limit_price); let swap_result = - Pallet::::do_swap(netuid, order.clone(), sqrt_limit_price, false, false) + Pallet::::do_swap(netuid, order.clone(), limit_price_fixed, false, false) .unwrap(); assert_abs_diff_eq!( swap_result.amount_paid_out.to_u64(), - output_amount, - epsilon = output_amount / 100 + expected_output_amount as u64, + epsilon = 1 ); assert_abs_diff_eq!( swap_result.paid_in_reserve_delta() as u64, - liquidity, - epsilon = liquidity / 10 + (swap_amount - expected_fee), + epsilon = 1 ); assert_abs_diff_eq!( swap_result.paid_out_reserve_delta() as i64, - -(output_amount as i64), - epsilon = output_amount as i64 / 10 - ); - - // Check that low and high ticks' fees were updated properly, and liquidity values were not updated - let tick_low_info = Ticks::::get(netuid, tick_low).unwrap(); - let tick_high_info = Ticks::::get(netuid, tick_high).unwrap(); - let expected_liquidity_net_low = tick_low_info_before.liquidity_net; - let expected_liquidity_gross_low = tick_low_info_before.liquidity_gross; - let expected_liquidity_net_high = tick_high_info_before.liquidity_net; - let expected_liquidity_gross_high = tick_high_info_before.liquidity_gross; - assert_eq!(tick_low_info.liquidity_net, expected_liquidity_net_low,); - assert_eq!(tick_low_info.liquidity_gross, expected_liquidity_gross_low,); - assert_eq!(tick_high_info.liquidity_net, expected_liquidity_net_high,); - assert_eq!( - tick_high_info.liquidity_gross, - expected_liquidity_gross_high, + -(expected_output_amount as i64), + epsilon = 1 ); - // Expected fee amount - let fee_rate = FeeRate::::get(netuid) as f64 / u16::MAX as f64; - let expected_fee = (liquidity as f64 * fee_rate) as u64; - - // Global fees should be updated - // let actual_global_fee = (order.amount().global_fee(netuid).to_num::() - // * (liquidity_before as f64)) as u64; - - assert!((swap_result.fee_paid.to_u64() as i64 - expected_fee as i64).abs() <= 1); - - // All fees go to block builder - // assert!((actual_global_fee as i64 - expected_fee as i64).abs() <= 1); - - // Tick fees should be updated - - // Liquidity position should not be updated - let protocol_id = Pallet::::protocol_account_id(); - let positions = - Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); - let position = positions.first().unwrap(); - - assert_eq!( - position.liquidity, - helpers_128bit::sqrt( - TaoReserve::reserve(netuid.into()).to_u64() as u128 - * AlphaReserve::reserve(netuid.into()).to_u64() as u128 - ) as u64 - ); - assert_eq!(position.tick_low, tick_low); - assert_eq!(position.tick_high, tick_high); - assert_eq!(position.fees_alpha, 0); - assert_eq!(position.fees_tao, 0); - - // Current liquidity is not updated - assert_eq!(CurrentLiquidity::::get(netuid), liquidity_before); + // Update reserves (because it happens outside of do_swap in stake_utils) + if price_should_grow { + TaoReserve::set_mock_reserve( + netuid, + TaoBalance::from( + (u64::from(initial_tao_reserve) as i128 + + swap_result.paid_in_reserve_delta()) as u64, + ), + ); + AlphaReserve::set_mock_reserve( + netuid, + AlphaBalance::from( + (u64::from(initial_alpha_reserve) as i128 + + swap_result.paid_out_reserve_delta()) as u64, + ), + ); + } else { + TaoReserve::set_mock_reserve( + netuid, + TaoBalance::from( + (u64::from(initial_tao_reserve) as i128 + + swap_result.paid_out_reserve_delta()) as u64, + ), + ); + AlphaReserve::set_mock_reserve( + netuid, + AlphaBalance::from( + (u64::from(initial_alpha_reserve) as i128 + + swap_result.paid_in_reserve_delta()) as u64, + ), + ); + } // Assert that price movement is in correct direction - let sqrt_current_price_after = AlphaSqrtPrice::::get(netuid); let current_price_after = Pallet::::current_price(netuid); - assert_eq!(current_price_after >= current_price, price_should_grow); - - // Assert that current tick is updated - let current_tick = CurrentTick::::get(netuid); - let expected_current_tick = - TickIndex::from_sqrt_price_bounded(sqrt_current_price_after); - assert_eq!(current_tick, expected_current_tick); + assert_eq!( + current_price_after >= current_price_before, + price_should_grow + ); } // Current price is 0.25 // Test case is (order_type, liquidity, limit_price, output_amount) - perform_test( - 1.into(), - GetAlphaForTao::with_amount(1_000), - 1000.0, - 3990, - true, - ); + perform_test(1.into(), GetAlphaForTao::with_amount(1_000), 1000.0, true); + perform_test(1.into(), GetAlphaForTao::with_amount(2_000), 1000.0, true); + perform_test(1.into(), GetAlphaForTao::with_amount(123_456), 1000.0, true); + perform_test(2.into(), GetTaoForAlpha::with_amount(1_000), 0.0001, false); + perform_test(2.into(), GetTaoForAlpha::with_amount(2_000), 0.0001, false); perform_test( 2.into(), - GetTaoForAlpha::with_amount(1_000), + GetTaoForAlpha::with_amount(123_456), 0.0001, - 250, false, ); perform_test( 3.into(), - GetAlphaForTao::with_amount(500_000_000), + GetAlphaForTao::with_amount(1_000_000_000), 1000.0, - 2_000_000_000, true, ); - }); -} - -// In this test the swap starts and ends within one (large liquidity) position -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_swap_single_position --exact --show-output -#[test] -fn test_swap_single_position() { - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let max_tick = price_to_tick(max_price); - let netuid = NetUid::from(1); - assert_eq!(max_tick, TickIndex::MAX); - - let mut current_price_low = 0_f64; - let mut current_price_high = 0_f64; - let mut current_price = 0_f64; - new_test_ext().execute_with(|| { - let (low, high) = get_ticked_prices_around_current_price(); - current_price_low = low; - current_price_high = high; - current_price = Pallet::::current_price(netuid).to_num::(); - }); - - macro_rules! perform_test { - ($order_t:ident, - $price_low_offset:expr, - $price_high_offset:expr, - $position_liquidity:expr, - $liquidity_fraction:expr, - $limit_price:expr, - $price_should_grow:expr - ) => { - new_test_ext().execute_with(|| { - let price_low_offset = $price_low_offset; - let price_high_offset = $price_high_offset; - let position_liquidity = $position_liquidity; - let order_liquidity_fraction = $liquidity_fraction; - let limit_price = $limit_price; - let price_should_grow = $price_should_grow; - - ////////////////////////////////////////////// - // Initialize pool and add the user position - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - let tao_reserve = TaoReserve::reserve(netuid.into()).to_u64(); - let alpha_reserve = AlphaReserve::reserve(netuid.into()).to_u64(); - let protocol_liquidity = (tao_reserve as f64 * alpha_reserve as f64).sqrt(); - - // Add liquidity - let current_price = Pallet::::current_price(netuid).to_num::(); - let sqrt_current_price = AlphaSqrtPrice::::get(netuid).to_num::(); - - let price_low = price_low_offset + current_price; - let price_high = price_high_offset + current_price; - let tick_low = price_to_tick(price_low); - let tick_high = price_to_tick(price_high); - let (_position_id, _tao, _alpha) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - position_liquidity, - ) - .unwrap(); - - // Liquidity position at correct ticks - assert_eq!( - Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID), - 1 - ); - - // Get tick infos before the swap - let tick_low_info_before = Ticks::::get(netuid, tick_low).unwrap_or_default(); - let tick_high_info_before = - Ticks::::get(netuid, tick_high).unwrap_or_default(); - let liquidity_before = CurrentLiquidity::::get(netuid); - assert_abs_diff_eq!( - liquidity_before as f64, - protocol_liquidity + position_liquidity as f64, - epsilon = liquidity_before as f64 / 1000. - ); - - ////////////////////////////////////////////// - // Swap - - // Calculate the expected output amount for the cornercase of one step - let order_liquidity = order_liquidity_fraction * position_liquidity as f64; - - let output_amount = >::approx_expected_swap_output( - sqrt_current_price, - liquidity_before as f64, - order_liquidity, - ); - - // Do the swap - let sqrt_limit_price = SqrtPrice::from_num((limit_price).sqrt()); - let order = $order_t::with_amount(order_liquidity as u64); - let swap_result = - Pallet::::do_swap(netuid, order, sqrt_limit_price, false, false).unwrap(); - assert_abs_diff_eq!( - swap_result.amount_paid_out.to_u64() as f64, - output_amount, - epsilon = output_amount / 10. - ); - - if order_liquidity_fraction <= 0.001 { - assert_abs_diff_eq!( - swap_result.paid_in_reserve_delta() as i64, - order_liquidity as i64, - epsilon = order_liquidity as i64 / 10 - ); - assert_abs_diff_eq!( - swap_result.paid_out_reserve_delta() as i64, - -(output_amount as i64), - epsilon = output_amount as i64 / 10 - ); - } - - // Assert that price movement is in correct direction - let current_price_after = Pallet::::current_price(netuid); - assert_eq!(price_should_grow, current_price_after > current_price); - - // Assert that for small amounts price stays within the user position - if (order_liquidity_fraction <= 0.001) - && (price_low_offset > 0.0001) - && (price_high_offset > 0.0001) - { - assert!(current_price_after <= price_high); - assert!(current_price_after >= price_low); - } - - // Check that low and high ticks' fees were updated properly - let tick_low_info = Ticks::::get(netuid, tick_low).unwrap(); - let tick_high_info = Ticks::::get(netuid, tick_high).unwrap(); - let expected_liquidity_net_low = tick_low_info_before.liquidity_net; - let expected_liquidity_gross_low = tick_low_info_before.liquidity_gross; - let expected_liquidity_net_high = tick_high_info_before.liquidity_net; - let expected_liquidity_gross_high = tick_high_info_before.liquidity_gross; - assert_eq!(tick_low_info.liquidity_net, expected_liquidity_net_low,); - assert_eq!(tick_low_info.liquidity_gross, expected_liquidity_gross_low,); - assert_eq!(tick_high_info.liquidity_net, expected_liquidity_net_high,); - assert_eq!( - tick_high_info.liquidity_gross, - expected_liquidity_gross_high, - ); - - // Expected fee amount - do not test, all fees go to block builder - // let fee_rate = FeeRate::::get(netuid) as f64 / u16::MAX as f64; - // let expected_fee = (order_liquidity - order_liquidity / (1.0 + fee_rate)) as u64; - - // // // Global fees should be updated - // let actual_global_fee = ($order_t::with_amount(0) - // .amount() - // .global_fee(netuid) - // .to_num::() - // * (liquidity_before as f64)) as u64; - - // assert_abs_diff_eq!( - // swap_result.fee_paid.to_u64(), - // expected_fee, - // epsilon = expected_fee / 10 - // ); - // assert_abs_diff_eq!(actual_global_fee, expected_fee, epsilon = expected_fee / 10); - - // Tick fees should be updated - - // Liquidity position should not be updated - let positions = - Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) - .collect::>(); - let position = positions.first().unwrap(); - - assert_eq!(position.liquidity, position_liquidity,); - assert_eq!(position.tick_low, tick_low); - assert_eq!(position.tick_high, tick_high); - assert_eq!(position.fees_alpha, 0); - assert_eq!(position.fees_tao, 0); - }); - }; - } - - // Current price is 0.25 - // The test case is based on the current price and position prices are defined as a price - // offset from the current price - // Outer part of test case is Position: (price_low_offset, price_high_offset, liquidity) - [ - // Very localized position at the current price - (-0.1, 0.1, 500_000_000_000_u64), - // Repeat the protocol liquidity at maximum range - ( - min_price - current_price, - max_price - current_price, - 2_000_000_000_u64, - ), - // Repeat the protocol liquidity at current to max range - ( - current_price_high - current_price, - max_price - current_price, - 2_000_000_000_u64, - ), - // Repeat the protocol liquidity at min to current range - ( - min_price - current_price, - current_price_low - current_price, - 2_000_000_000_u64, - ), - // Half to double price - (-0.125, 0.25, 2_000_000_000_u64), - // A few other price ranges and liquidity volumes - (-0.1, 0.1, 2_000_000_000_u64), - (-0.1, 0.1, 10_000_000_000_u64), - (-0.1, 0.1, 100_000_000_000_u64), - (-0.01, 0.01, 100_000_000_000_u64), - (-0.001, 0.001, 100_000_000_000_u64), - ] - .into_iter() - .for_each( - |(price_low_offset, price_high_offset, position_liquidity)| { - // Inner part of test case is Order: (order_type, order_liquidity, limit_price) - // order_liquidity is represented as a fraction of position_liquidity - for liquidity_fraction in [0.0001, 0.001, 0.01, 0.1, 0.2, 0.5] { - perform_test!( - GetAlphaForTao, - price_low_offset, - price_high_offset, - position_liquidity, - liquidity_fraction, - 1000.0_f64, - true - ); - perform_test!( - GetTaoForAlpha, - price_low_offset, - price_high_offset, - position_liquidity, - liquidity_fraction, - 0.0001_f64, - false - ); - } - }, - ); -} - -// This test is a sanity check for swap and multiple positions -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor-swap --lib -- pallet::impls::tests::test_swap_multiple_positions --exact --show-output --nocapture -#[test] -fn test_swap_multiple_positions() { - new_test_ext().execute_with(|| { - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let max_tick = price_to_tick(max_price); - let netuid = NetUid::from(1); - assert_eq!(max_tick, TickIndex::MAX); - - ////////////////////////////////////////////// - // Initialize pool and add the user position - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Add liquidity - let current_price = Pallet::::current_price(netuid).to_num::(); - - // Current price is 0.25 - // All positions below are placed at once - [ - // Very localized position at the current price - (-0.1, 0.1, 500_000_000_000_u64), - // Repeat the protocol liquidity at maximum range - ( - min_price - current_price, - max_price - current_price, - 2_000_000_000_u64, - ), - // Repeat the protocol liquidity at current to max range - (0.0, max_price - current_price, 2_000_000_000_u64), - // Repeat the protocol liquidity at min to current range - (min_price - current_price, 0.0, 2_000_000_000_u64), - // Half to double price - (-0.125, 0.25, 2_000_000_000_u64), - // A few other price ranges and liquidity volumes - (-0.1, 0.1, 2_000_000_000_u64), - (-0.1, 0.1, 10_000_000_000_u64), - (-0.1, 0.1, 100_000_000_000_u64), - (-0.01, 0.01, 100_000_000_000_u64), - (-0.001, 0.001, 100_000_000_000_u64), - // A few (overlapping) positions up the range - (0.01, 0.02, 100_000_000_000_u64), - (0.02, 0.03, 100_000_000_000_u64), - (0.03, 0.04, 100_000_000_000_u64), - (0.03, 0.05, 100_000_000_000_u64), - // A few (overlapping) positions down the range - (-0.02, -0.01, 100_000_000_000_u64), - (-0.03, -0.02, 100_000_000_000_u64), - (-0.04, -0.03, 100_000_000_000_u64), - (-0.05, -0.03, 100_000_000_000_u64), - ] - .into_iter() - .for_each( - |(price_low_offset, price_high_offset, position_liquidity)| { - let price_low = price_low_offset + current_price; - let price_high = price_high_offset + current_price; - let tick_low = price_to_tick(price_low); - let tick_high = price_to_tick(price_high); - let (_position_id, _tao, _alpha) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - position_liquidity, - ) - .unwrap(); - }, + perform_test( + 3.into(), + GetAlphaForTao::with_amount(10_000_000_000_u64), + 1000.0, + true, ); - - macro_rules! perform_test { - ($order_t:ident, $order_liquidity:expr, $limit_price:expr, $should_price_grow:expr) => { - ////////////////////////////////////////////// - // Swap - let order_liquidity = $order_liquidity; - let limit_price = $limit_price; - let should_price_grow = $should_price_grow; - - let sqrt_current_price = AlphaSqrtPrice::::get(netuid); - let current_price = (sqrt_current_price * sqrt_current_price).to_num::(); - let liquidity_before = CurrentLiquidity::::get(netuid); - let output_amount = >::approx_expected_swap_output( - sqrt_current_price.to_num(), - liquidity_before as f64, - order_liquidity as f64, - ); - - // Do the swap - let sqrt_limit_price = SqrtPrice::from_num((limit_price).sqrt()); - let order = $order_t::with_amount(order_liquidity); - let swap_result = - Pallet::::do_swap(netuid, order, sqrt_limit_price, false, false).unwrap(); - assert_abs_diff_eq!( - swap_result.amount_paid_out.to_u64() as f64, - output_amount, - epsilon = output_amount / 10. - ); - - let tao_reserve = TaoReserve::reserve(netuid.into()).to_u64(); - let alpha_reserve = AlphaReserve::reserve(netuid.into()).to_u64(); - let output_amount = output_amount as u64; - - assert!(output_amount > 0); - - if alpha_reserve > order_liquidity && tao_reserve > order_liquidity { - assert_abs_diff_eq!( - swap_result.paid_in_reserve_delta() as i64, - order_liquidity as i64, - epsilon = order_liquidity as i64 / 100 - ); - assert_abs_diff_eq!( - swap_result.paid_out_reserve_delta() as i64, - -(output_amount as i64), - epsilon = output_amount as i64 / 100 - ); - } - - // Assert that price movement is in correct direction - let sqrt_current_price_after = AlphaSqrtPrice::::get(netuid); - let current_price_after = - (sqrt_current_price_after * sqrt_current_price_after).to_num::(); - assert_eq!(should_price_grow, current_price_after > current_price); - }; - } - - // All these orders are executed without swap reset - for order_liquidity in [ - (100_000_u64), - (1_000_000), - (10_000_000), - (100_000_000), - (200_000_000), - (500_000_000), - (1_000_000_000), - (10_000_000_000), - ] { - perform_test!(GetAlphaForTao, order_liquidity, 1000.0_f64, true); - perform_test!(GetTaoForAlpha, order_liquidity, 0.0001_f64, false); - } - - // Current price shouldn't be much different from the original - let sqrt_current_price_after = AlphaSqrtPrice::::get(netuid); - let current_price_after = - (sqrt_current_price_after * sqrt_current_price_after).to_num::(); - assert_abs_diff_eq!( - current_price, - current_price_after, - epsilon = current_price / 10. - ) }); } // cargo test --package pallet-subtensor-swap --lib -- pallet::impls::tests::test_swap_precision_edge_case --exact --show-output #[test] fn test_swap_precision_edge_case() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(123); // 123 is netuid with low edge case liquidity - let order = GetTaoForAlpha::with_amount(1_000_000_000_000_000_000_u64); - let tick_low = TickIndex::MIN; - - let sqrt_limit_price: SqrtPrice = tick_low.try_to_sqrt_price().unwrap(); - - // Setup swap - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Swap - let swap_result = - Pallet::::do_swap(netuid, order, sqrt_limit_price, false, true).unwrap(); - - assert!(swap_result.amount_paid_out > TaoBalance::ZERO); - }); -} - -// cargo test --package pallet-subtensor-swap --lib -- pallet::impls::tests::test_price_tick_price_roundtrip --exact --show-output -#[test] -fn test_price_tick_price_roundtrip() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); + // Test case: tao_reserve, alpha_reserve, swap_amount + [ + (1_000_u64, 1_000_u64, 999_500_u64), + (1_000_000_u64, 1_000_000_u64, 999_500_000_u64), + ] + .into_iter() + .for_each(|(tao_reserve, alpha_reserve, swap_amount)| { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + let order = GetTaoForAlpha::with_amount(swap_amount); - // Setup swap - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); + // Very low reserves + TaoReserve::set_mock_reserve(netuid, TaoBalance::from(tao_reserve)); + AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(alpha_reserve)); - let current_price = SqrtPrice::from_num(0.500_000_512_192_122_7); - let tick = TickIndex::try_from_sqrt_price(current_price).unwrap(); + // Minimum possible limit price + let limit_price: U64F64 = get_min_price(); + println!("limit_price = {:?}", limit_price); - let round_trip_price = TickIndex::try_to_sqrt_price(&tick).unwrap(); - assert!(round_trip_price <= current_price); + // Swap + let swap_result = + Pallet::::do_swap(netuid, order, limit_price, false, true).unwrap(); - let roundtrip_tick = TickIndex::try_from_sqrt_price(round_trip_price).unwrap(); - assert!(tick == roundtrip_tick); + assert!(swap_result.amount_paid_out > TaoBalance::ZERO); + }); }); } #[test] fn test_convert_deltas() { new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - for (sqrt_price, delta_in, expected_buy, expected_sell) in [ - (SqrtPrice::from_num(1.5), 1, 0, 2), - (SqrtPrice::from_num(1.5), 10000, 4444, 22500), - (SqrtPrice::from_num(1.5), 1000000, 444444, 2250000), - ( - SqrtPrice::from_num(1.5), - u64::MAX, - 2000000000000, - 3000000000000, - ), - ( - TickIndex::MIN.as_sqrt_price_bounded(), - 1, - 18406523739291577836, - 465, - ), - (TickIndex::MIN.as_sqrt_price_bounded(), 10000, u64::MAX, 465), - ( - TickIndex::MIN.as_sqrt_price_bounded(), - 1000000, - u64::MAX, - 465, - ), - ( - TickIndex::MIN.as_sqrt_price_bounded(), - u64::MAX, - u64::MAX, - 464, - ), - ( - TickIndex::MAX.as_sqrt_price_bounded(), - 1, - 0, - 18406523745214495085, - ), - (TickIndex::MAX.as_sqrt_price_bounded(), 10000, 0, u64::MAX), - (TickIndex::MAX.as_sqrt_price_bounded(), 1000000, 0, u64::MAX), - ( - TickIndex::MAX.as_sqrt_price_bounded(), - u64::MAX, - 2000000000000, - u64::MAX, - ), + for (tao, alpha, w_quote, delta_in) in [ + (1500, 1000, 0.5, 1), + (1500, 1000, 0.5, 10000), + (1500, 1000, 0.5, 1000000), + (1500, 1000, 0.5, u64::MAX), + (1, 1000000, 0.5, 1), + (1, 1000000, 0.5, 10000), + (1, 1000000, 0.5, 1000000), + (1, 1000000, 0.5, u64::MAX), + (1000000, 1, 0.5, 1), + (1000000, 1, 0.5, 10000), + (1000000, 1, 0.5, 1000000), + (1000000, 1, 0.5, u64::MAX), + (1500, 1000, 0.50000001, 1), + (1500, 1000, 0.50000001, 10000), + (1500, 1000, 0.50000001, 1000000), + (1500, 1000, 0.50000001, u64::MAX), + (1, 1000000, 0.50000001, 1), + (1, 1000000, 0.50000001, 10000), + (1, 1000000, 0.50000001, 1000000), + (1, 1000000, 0.50000001, u64::MAX), + (1000000, 1, 0.50000001, 1), + (1000000, 1, 0.50000001, 10000), + (1000000, 1, 0.50000001, 1000000), + (1000000, 1, 0.50000001, u64::MAX), + (1500, 1000, 0.49999999, 1), + (1500, 1000, 0.49999999, 10000), + (1500, 1000, 0.49999999, 1000000), + (1500, 1000, 0.49999999, u64::MAX), + (1, 1000000, 0.49999999, 1), + (1, 1000000, 0.49999999, 10000), + (1, 1000000, 0.49999999, 1000000), + (1, 1000000, 0.49999999, u64::MAX), + (1000000, 1, 0.49999999, 1), + (1000000, 1, 0.49999999, 10000), + (1000000, 1, 0.49999999, 1000000), + (1000000, 1, 0.49999999, u64::MAX), + // Low quote weight + (1500, 1000, 0.1, 1), + (1500, 1000, 0.1, 10000), + (1500, 1000, 0.1, 1000000), + (1500, 1000, 0.1, u64::MAX), + (1, 1000000, 0.1, 1), + (1, 1000000, 0.1, 10000), + (1, 1000000, 0.1, 1000000), + (1, 1000000, 0.1, u64::MAX), + (1000000, 1, 0.1, 1), + (1000000, 1, 0.1, 10000), + (1000000, 1, 0.1, 1000000), + (1000000, 1, 0.1, u64::MAX), + // High quote weight + (1500, 1000, 0.9, 1), + (1500, 1000, 0.9, 10000), + (1500, 1000, 0.9, 1000000), + (1500, 1000, 0.9, u64::MAX), + (1, 1000000, 0.9, 1), + (1, 1000000, 0.9, 10000), + (1, 1000000, 0.9, 1000000), + (1, 1000000, 0.9, u64::MAX), + (1000000, 1, 0.9, 1), + (1000000, 1, 0.9, 10000), + (1000000, 1, 0.9, 1000000), + (1000000, 1, 0.9, u64::MAX), ] { - { - AlphaSqrtPrice::::insert(netuid, sqrt_price); + // Initialize reserves and weights + let netuid = NetUid::from(1); + TaoReserve::set_mock_reserve(netuid, TaoBalance::from(tao)); + AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(alpha)); + assert_ok!(Pallet::::maybe_initialize_palswap(netuid, None)); + + let w_accuracy = 1_000_000_000_f64; + let w_quote_pt = + Perquintill::from_rational((w_quote * w_accuracy) as u128, w_accuracy as u128); + let bal = Balancer::new(w_quote_pt).unwrap(); + SwapBalancer::::insert(netuid, bal); + + // Calculate expected swap results (buy and sell) using f64 math + let y = tao as f64; + let x = alpha as f64; + let d = delta_in as f64; + let w1_div_w2 = (1. - w_quote) / w_quote; + let w2_div_w1 = w_quote / (1. - w_quote); + let expected_sell = y * (1. - (x / (x + d)).powf(w1_div_w2)); + let expected_buy = x * (1. - (y / (y + d)).powf(w2_div_w1)); - assert_abs_diff_eq!( + assert_abs_diff_eq!( + u64::from( BasicSwapStep::::convert_deltas( netuid, delta_in.into() - ), - expected_sell.into(), - epsilon = 2.into() - ); - assert_abs_diff_eq!( + ) + ), + expected_sell as u64, + epsilon = 2u64 + ); + assert_abs_diff_eq!( + u64::from( BasicSwapStep::::convert_deltas( netuid, delta_in.into() - ), - expected_buy.into(), - epsilon = 2.into() - ); - } + ) + ), + expected_buy as u64, + epsilon = 2u64 + ); } }); } -// #[test] -// fn test_user_liquidity_disabled() { -// new_test_ext().execute_with(|| { -// // Use a netuid above 100 since our mock enables liquidity for 0-100 -// let netuid = NetUid::from(101); -// let tick_low = TickIndex::new_unchecked(-1000); -// let tick_high = TickIndex::new_unchecked(1000); -// let position_id = PositionId::from(1); -// let liquidity = 1_000_000_000; -// let liquidity_delta = 500_000_000; - -// assert!(!EnabledUserLiquidity::::get(netuid)); - -// assert_noop!( -// Swap::do_add_liquidity( -// netuid, -// &OK_COLDKEY_ACCOUNT_ID, -// &OK_HOTKEY_ACCOUNT_ID, -// tick_low, -// tick_high, -// liquidity -// ), -// Error::::UserLiquidityDisabled -// ); - -// assert_noop!( -// Swap::do_remove_liquidity(netuid, &OK_COLDKEY_ACCOUNT_ID, position_id), -// Error::::LiquidityNotFound -// ); - -// assert_noop!( -// Swap::modify_position( -// RuntimeOrigin::signed(OK_COLDKEY_ACCOUNT_ID), -// OK_HOTKEY_ACCOUNT_ID, -// netuid, -// position_id, -// liquidity_delta -// ), -// Error::::UserLiquidityDisabled -// ); - -// assert_ok!(Swap::toggle_user_liquidity( -// RuntimeOrigin::root(), -// netuid, -// true -// )); - -// let position_id = Swap::do_add_liquidity( -// netuid, -// &OK_COLDKEY_ACCOUNT_ID, -// &OK_HOTKEY_ACCOUNT_ID, -// tick_low, -// tick_high, -// liquidity, -// ) -// .unwrap() -// .0; - -// assert_ok!(Swap::do_modify_position( -// netuid.into(), -// &OK_COLDKEY_ACCOUNT_ID, -// &OK_HOTKEY_ACCOUNT_ID, -// position_id, -// liquidity_delta, -// )); - -// assert_ok!(Swap::do_remove_liquidity( -// netuid, -// &OK_COLDKEY_ACCOUNT_ID, -// position_id, -// )); -// }); -// } - -// This test is pointless: All fees go to block author -// Test correctness of swap fees: -// - Fees are distribued to (concentrated) liquidity providers -// -// #[test] -// fn test_swap_fee_correctness() { -// new_test_ext().execute_with(|| { -// let min_price = tick_to_price(TickIndex::MIN); -// let max_price = tick_to_price(TickIndex::MAX); -// let netuid = NetUid::from(1); - -// // Provide very spread liquidity at the range from min to max that matches protocol liquidity -// let liquidity = 2_000_000_000_000_u64; // 1x of protocol liquidity - -// assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - -// // Calculate ticks -// let tick_low = price_to_tick(min_price); -// let tick_high = price_to_tick(max_price); - -// // Add user liquidity -// let (position_id, _tao, _alpha) = Pallet::::do_add_liquidity( -// netuid, -// &OK_COLDKEY_ACCOUNT_ID, -// &OK_HOTKEY_ACCOUNT_ID, -// tick_low, -// tick_high, -// liquidity, -// ) -// .unwrap(); - -// // Swap buy and swap sell -// Pallet::::do_swap( -// netuid, -// GetAlphaForTao::with_amount(liquidity / 10), -// u64::MAX.into(), -// false, -// false, -// ) -// .unwrap(); -// Pallet::::do_swap( -// netuid, -// GetTaoForAlpha::with_amount(liquidity / 10), -// 0_u64.into(), -// false, -// false, -// ) -// .unwrap(); - -// // Get user position -// let mut position = -// Positions::::get((netuid, OK_COLDKEY_ACCOUNT_ID, position_id)).unwrap(); -// assert_eq!(position.liquidity, liquidity); -// assert_eq!(position.tick_low, tick_low); -// assert_eq!(position.tick_high, tick_high); - -// // Check that 50% of fees were credited to the position -// let fee_rate = FeeRate::::get(NetUid::from(netuid)) as f64 / u16::MAX as f64; -// let (actual_fee_tao, actual_fee_alpha) = position.collect_fees(); -// let expected_fee = (fee_rate * (liquidity / 10) as f64 * 0.5) as u64; - -// assert_abs_diff_eq!(actual_fee_tao, expected_fee, epsilon = 1,); -// assert_abs_diff_eq!(actual_fee_alpha, expected_fee, epsilon = 1,); -// }); -// } - -#[test] -fn test_current_liquidity_updates() { - let netuid = NetUid::from(1); - let liquidity = 1_000_000_000; - - // Get current price - let (current_price, current_price_low, current_price_high) = - new_test_ext().execute_with(|| { - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - let sqrt_current_price = AlphaSqrtPrice::::get(netuid); - let current_price = (sqrt_current_price * sqrt_current_price).to_num::(); - let (current_price_low, current_price_high) = get_ticked_prices_around_current_price(); - (current_price, current_price_low, current_price_high) - }); - - // Test case: (price_low, price_high, expect_to_update) - [ - // Current price is out of position range (lower), no current lq update - (current_price * 2., current_price * 3., false), - // Current price is out of position range (higher), no current lq update - (current_price / 3., current_price / 2., false), - // Current price is just below position range, no current lq update - (current_price_high, current_price * 3., false), - // Position lower edge is just below the current price, current lq updates - (current_price_low, current_price * 3., true), - // Current price is exactly at lower edge of position range, current lq updates - (current_price, current_price * 3., true), - // Current price is exactly at higher edge of position range, no current lq update - (current_price / 2., current_price, false), - ] - .into_iter() - .for_each(|(price_low, price_high, expect_to_update)| { - new_test_ext().execute_with(|| { - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Calculate ticks (assuming tick math is tested separately) - let tick_low = price_to_tick(price_low); - let tick_high = price_to_tick(price_high); - let liquidity_before = CurrentLiquidity::::get(netuid); - - // Add liquidity - assert_ok!(Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, - )); - - // Current liquidity is updated only when price range includes the current price - let expected_liquidity = if (price_high > current_price) && (price_low <= current_price) - { - assert!(expect_to_update); - liquidity_before + liquidity - } else { - assert!(!expect_to_update); - liquidity_before - }; - - assert_eq!(CurrentLiquidity::::get(netuid), expected_liquidity) - }); - }); -} - #[test] fn test_rollback_works() { new_test_ext().execute_with(|| { @@ -1588,794 +721,48 @@ fn test_rollback_works() { }) } -/// Test correctness of swap fees: -/// - New LP is not eligible to previously accrued fees -/// -/// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_new_lp_doesnt_get_old_fees --exact --show-output -#[test] -fn test_new_lp_doesnt_get_old_fees() { - new_test_ext().execute_with(|| { - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let netuid = NetUid::from(1); - - // Provide very spread liquidity at the range from min to max that matches protocol liquidity - let liquidity = 2_000_000_000_000_u64; // 1x of protocol liquidity - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Calculate ticks - let tick_low = price_to_tick(min_price); - let tick_high = price_to_tick(max_price); - - // Add user liquidity - Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, - ) - .unwrap(); - - // Swap buy and swap sell - Pallet::::do_swap( - netuid, - GetAlphaForTao::with_amount(liquidity / 10), - u64::MAX.into(), - false, - false, - ) - .unwrap(); - Pallet::::do_swap( - netuid, - GetTaoForAlpha::with_amount(liquidity / 10), - 0_u64.into(), - false, - false, - ) - .unwrap(); - - // Add liquidity from a different user to a new tick - let (position_id_2, _tao, _alpha) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID_2, - &OK_HOTKEY_ACCOUNT_ID_2, - tick_low.next().unwrap(), - tick_high.prev().unwrap(), - liquidity, - ) - .unwrap(); - - // Get user position - let mut position = - Positions::::get((netuid, OK_COLDKEY_ACCOUNT_ID_2, position_id_2)).unwrap(); - assert_eq!(position.liquidity, liquidity); - assert_eq!(position.tick_low, tick_low.next().unwrap()); - assert_eq!(position.tick_high, tick_high.prev().unwrap()); - - // Check that collected fees are 0 - let (actual_fee_tao, actual_fee_alpha) = position.collect_fees(); - assert_abs_diff_eq!(actual_fee_tao, 0, epsilon = 1); - assert_abs_diff_eq!(actual_fee_alpha, 0, epsilon = 1); - }); -} - -// fn bbox(t: U64F64, a: U64F64, b: U64F64) -> U64F64 { -// if t < a { -// a -// } else if t > b { -// b -// } else { -// t -// } -// } - -// fn print_current_price(netuid: NetUid) { -// let current_sqrt_price = AlphaSqrtPrice::::get(netuid).to_num::(); -// let current_price = current_sqrt_price * current_sqrt_price; -// log::trace!("Current price: {current_price:.6}"); -// } - -// All fees go to block builder -// RUST_LOG=pallet_subtensor_swap=trace cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_wrapping_fees --exact --show-output --nocapture -// #[test] -// fn test_wrapping_fees() { -// new_test_ext().execute_with(|| { -// let netuid = NetUid::from(WRAPPING_FEES_NETUID); -// let position_1_low_price = 0.20; -// let position_1_high_price = 0.255; -// let position_2_low_price = 0.255; -// let position_2_high_price = 0.257; -// assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - -// Pallet::::do_add_liquidity( -// netuid, -// &OK_COLDKEY_ACCOUNT_ID_RICH, -// &OK_COLDKEY_ACCOUNT_ID_RICH, -// price_to_tick(position_1_low_price), -// price_to_tick(position_1_high_price), -// 1_000_000_000_u64, -// ) -// .unwrap(); - -// print_current_price(netuid); - -// let order = GetTaoForAlpha::with_amount(800_000_000); -// let sqrt_limit_price = SqrtPrice::from_num(0.000001); -// Pallet::::do_swap(netuid, order, sqrt_limit_price, false, false).unwrap(); - -// let order = GetAlphaForTao::with_amount(1_850_000_000); -// let sqrt_limit_price = SqrtPrice::from_num(1_000_000.0); - -// print_current_price(netuid); - -// Pallet::::do_swap(netuid, order, sqrt_limit_price, false, false).unwrap(); - -// print_current_price(netuid); - -// let add_liquidity_result = Pallet::::do_add_liquidity( -// netuid, -// &OK_COLDKEY_ACCOUNT_ID_RICH, -// &OK_COLDKEY_ACCOUNT_ID_RICH, -// price_to_tick(position_2_low_price), -// price_to_tick(position_2_high_price), -// 1_000_000_000_u64, -// ) -// .unwrap(); - -// let order = GetTaoForAlpha::with_amount(1_800_000_000); -// let sqrt_limit_price = SqrtPrice::from_num(0.000001); - -// let initial_sqrt_price = AlphaSqrtPrice::::get(netuid); -// Pallet::::do_swap(netuid, order, sqrt_limit_price, false, false).unwrap(); -// let final_sqrt_price = AlphaSqrtPrice::::get(netuid); - -// print_current_price(netuid); - -// let mut position = -// Positions::::get((netuid, &OK_COLDKEY_ACCOUNT_ID_RICH, add_liquidity_result.0)) -// .unwrap(); - -// let initial_box_price = bbox( -// initial_sqrt_price, -// position.tick_low.try_to_sqrt_price().unwrap(), -// position.tick_high.try_to_sqrt_price().unwrap(), -// ); - -// let final_box_price = bbox( -// final_sqrt_price, -// position.tick_low.try_to_sqrt_price().unwrap(), -// position.tick_high.try_to_sqrt_price().unwrap(), -// ); - -// let fee_rate = FeeRate::::get(netuid) as f64 / u16::MAX as f64; - -// log::trace!("fee_rate: {fee_rate:.6}"); -// log::trace!("position.liquidity: {}", position.liquidity); -// log::trace!( -// "initial_box_price: {:.6}", -// initial_box_price.to_num::() -// ); -// log::trace!("final_box_price: {:.6}", final_box_price.to_num::()); - -// let expected_fee_tao = ((fee_rate / (1.0 - fee_rate)) -// * (position.liquidity as f64) -// * (final_box_price.to_num::() - initial_box_price.to_num::())) -// as u64; - -// let expected_fee_alpha = ((fee_rate / (1.0 - fee_rate)) -// * (position.liquidity as f64) -// * ((1.0 / final_box_price.to_num::()) - (1.0 / initial_box_price.to_num::()))) -// as u64; - -// log::trace!("Expected ALPHA fee: {:.6}", expected_fee_alpha as f64); - -// let (fee_tao, fee_alpha) = position.collect_fees(); - -// log::trace!("Collected fees: TAO: {fee_tao}, ALPHA: {fee_alpha}"); - -// assert_abs_diff_eq!(fee_tao, expected_fee_tao, epsilon = 1); -// assert_abs_diff_eq!(fee_alpha, expected_fee_alpha, epsilon = 1); -// }); -// } - -/// Test that price moves less with provided liquidity -/// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_less_price_movement --exact --show-output -#[test] -fn test_less_price_movement() { - let netuid = NetUid::from(1); - let mut last_end_price = U96F32::from_num(0); - let initial_stake_liquidity = 1_000_000_000; - let swapped_liquidity = 1_000_000; - - // Test case is (order_type, provided_liquidity) - // Testing algorithm: - // - Stake initial_stake_liquidity - // - Provide liquidity if iteration provides lq - // - Buy or sell - // - Save end price if iteration doesn't provide lq - macro_rules! perform_test { - ($order_t:ident, $provided_liquidity:expr, $limit_price:expr, $should_price_shrink:expr) => { - let provided_liquidity = $provided_liquidity; - let should_price_shrink = $should_price_shrink; - let limit_price = $limit_price; - new_test_ext().execute_with(|| { - // Setup swap - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Buy Alpha - assert_ok!(Pallet::::do_swap( - netuid, - GetAlphaForTao::with_amount(initial_stake_liquidity), - SqrtPrice::from_num(10_000_000_000_u64), - false, - false - )); - - // Get current price - let start_price = Pallet::::current_price(netuid); - - // Add liquidity if this test iteration provides - if provided_liquidity > 0 { - let tick_low = price_to_tick(start_price.to_num::() * 0.5); - let tick_high = price_to_tick(start_price.to_num::() * 1.5); - assert_ok!(Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - provided_liquidity, - )); - } - - // Swap - let sqrt_limit_price = SqrtPrice::from_num(limit_price); - assert_ok!(Pallet::::do_swap( - netuid, - $order_t::with_amount(swapped_liquidity), - sqrt_limit_price, - false, - false - )); - - let end_price = Pallet::::current_price(netuid); - - // Save end price if iteration doesn't provide or compare with previous end price if - // it does - if provided_liquidity > 0 { - assert_eq!(should_price_shrink, end_price < last_end_price); - } else { - last_end_price = end_price; - } - }); - }; - } - - for provided_liquidity in [0, 1_000_000_000_000_u64] { - perform_test!(GetAlphaForTao, provided_liquidity, 1000.0_f64, true); - } - for provided_liquidity in [0, 1_000_000_000_000_u64] { - perform_test!(GetTaoForAlpha, provided_liquidity, 0.001_f64, false); +#[allow(dead_code)] +fn bbox(t: U64F64, a: U64F64, b: U64F64) -> U64F64 { + if t < a { + a + } else if t > b { + b + } else { + t } } -// TODO: Revise when user liquidity is available -// #[test] -// fn test_swap_subtoken_disabled() { -// new_test_ext().execute_with(|| { -// let netuid = NetUid::from(SUBTOKEN_DISABLED_NETUID); // Use a netuid not used elsewhere -// let price_low = 0.1; -// let price_high = 0.2; -// let tick_low = price_to_tick(price_low); -// let tick_high = price_to_tick(price_high); -// let liquidity = 1_000_000_u64; - -// assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - -// assert_noop!( -// Pallet::::add_liquidity( -// RuntimeOrigin::signed(OK_COLDKEY_ACCOUNT_ID), -// OK_HOTKEY_ACCOUNT_ID, -// netuid, -// tick_low, -// tick_high, -// liquidity, -// ), -// Error::::SubtokenDisabled -// ); - -// assert_noop!( -// Pallet::::modify_position( -// RuntimeOrigin::signed(OK_COLDKEY_ACCOUNT_ID), -// OK_HOTKEY_ACCOUNT_ID, -// netuid, -// PositionId::from(0), -// liquidity as i64, -// ), -// Error::::SubtokenDisabled -// ); -// }); -// } - -#[test] -fn test_liquidate_v3_removes_positions_ticks_and_state() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - // Initialize V3 (creates protocol position, ticks, price, liquidity) - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - assert!(SwapV3Initialized::::get(netuid)); - - // Enable user LP - assert_ok!(Swap::toggle_user_liquidity( - RuntimeOrigin::root(), - netuid.into(), - true - )); - - // Add a user position across the full range to ensure ticks/bitmap are populated. - let min_price = tick_to_price(TickIndex::MIN); - let max_price = tick_to_price(TickIndex::MAX); - let tick_low = price_to_tick(min_price); - let tick_high = price_to_tick(max_price); - let liquidity = 2_000_000_000_u64; - - let (_pos_id, _tao, _alpha) = Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - liquidity, - ) - .expect("add liquidity"); - - // Accrue some global fees so we can verify fee storage is cleared later. - let sqrt_limit_price = SqrtPrice::from_num(1_000_000.0); - assert_ok!(Pallet::::do_swap( - netuid, - GetAlphaForTao::with_amount(1_000_000), - sqrt_limit_price, - false, - false - )); - - // Sanity: protocol & user positions exist, ticks exist, liquidity > 0 - let protocol_id = Pallet::::protocol_account_id(); - let prot_positions = - Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); - assert!(!prot_positions.is_empty()); - - let user_positions = Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) - .collect::>(); - assert_eq!(user_positions.len(), 1); - - assert!(Ticks::::get(netuid, TickIndex::MIN).is_some()); - assert!(Ticks::::get(netuid, TickIndex::MAX).is_some()); - assert!(CurrentLiquidity::::get(netuid) > 0); - - let had_bitmap_words = TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_some(); - assert!(had_bitmap_words); - - // ACT: users-only liquidation then protocol clear - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - - // ASSERT: positions cleared (both user and protocol) - assert_eq!( - Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID), - 0 - ); - let prot_positions_after = - Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); - assert!(prot_positions_after.is_empty()); - let user_positions_after = - Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) - .collect::>(); - assert!(user_positions_after.is_empty()); - - // ASSERT: ticks cleared - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!(Ticks::::get(netuid, TickIndex::MIN).is_none()); - assert!(Ticks::::get(netuid, TickIndex::MAX).is_none()); - - // ASSERT: fee globals cleared - assert!(!FeeGlobalTao::::contains_key(netuid)); - assert!(!FeeGlobalAlpha::::contains_key(netuid)); - - // ASSERT: price/tick/liquidity flags cleared - assert!(!AlphaSqrtPrice::::contains_key(netuid)); - assert!(!CurrentTick::::contains_key(netuid)); - assert!(!CurrentLiquidity::::contains_key(netuid)); - assert!(!SwapV3Initialized::::contains_key(netuid)); - - // ASSERT: active tick bitmap cleared - assert!( - TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_none() - ); - - // ASSERT: knobs removed on dereg - assert!(!FeeRate::::contains_key(netuid)); - assert!(!EnabledUserLiquidity::::contains_key(netuid)); - }); +#[allow(dead_code)] +fn print_current_price(netuid: NetUid) { + let current_price = Pallet::::current_price(netuid); + log::trace!("Current price: {current_price:.6}"); } -// V3 path with user liquidity disabled at teardown: -// must still remove positions and clear state (after protocol clear). -// #[test] -// fn test_liquidate_v3_with_user_liquidity_disabled() { -// new_test_ext().execute_with(|| { -// let netuid = NetUid::from(101); - -// assert_ok!(Pallet::::maybe_initialize_v3(netuid)); -// assert!(SwapV3Initialized::::get(netuid)); - -// // Enable temporarily to add a user position -// assert_ok!(Swap::toggle_user_liquidity( -// RuntimeOrigin::root(), -// netuid.into(), -// true -// )); - -// let min_price = tick_to_price(TickIndex::MIN); -// let max_price = tick_to_price(TickIndex::MAX); -// let tick_low = price_to_tick(min_price); -// let tick_high = price_to_tick(max_price); -// let liquidity = 1_000_000_000_u64; - -// let (_pos_id, _tao, _alpha) = Pallet::::do_add_liquidity( -// netuid, -// &OK_COLDKEY_ACCOUNT_ID, -// &OK_HOTKEY_ACCOUNT_ID, -// tick_low, -// tick_high, -// liquidity, -// ) -// .expect("add liquidity"); - -// // Disable user LP *before* liquidation; removal must ignore this flag. -// assert_ok!(Swap::toggle_user_liquidity( -// RuntimeOrigin::root(), -// netuid.into(), -// false -// )); - -// // Users-only dissolve, then clear protocol liquidity/state. -// assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); -// assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - -// // ASSERT: positions & ticks gone, state reset -// assert_eq!( -// Pallet::::count_positions(netuid, &OK_COLDKEY_ACCOUNT_ID), -// 0 -// ); -// assert!( -// Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) -// .next() -// .is_none() -// ); -// assert!(Ticks::::iter_prefix(netuid).next().is_none()); -// assert!( -// TickIndexBitmapWords::::iter_prefix((netuid,)) -// .next() -// .is_none() -// ); -// assert!(!SwapV3Initialized::::contains_key(netuid)); -// assert!(!AlphaSqrtPrice::::contains_key(netuid)); -// assert!(!CurrentTick::::contains_key(netuid)); -// assert!(!CurrentLiquidity::::contains_key(netuid)); -// assert!(!FeeGlobalTao::::contains_key(netuid)); -// assert!(!FeeGlobalAlpha::::contains_key(netuid)); - -// // `EnabledUserLiquidity` is removed by protocol clear stage. -// assert!(!EnabledUserLiquidity::::contains_key(netuid)); -// }); -// } - -/// Non‑V3 path: V3 not initialized (no positions); function must still clear any residual storages and succeed. +/// Simple palswap path: PalSwap is initialized, but no positions, only protocol; function +/// must still clear any residual storages and succeed. +/// TODO: Revise when user liquidity is available #[test] -fn test_liquidate_non_v3_uninitialized_ok_and_clears() { +fn test_liquidate_pal_simple_ok_and_clears() { new_test_ext().execute_with(|| { let netuid = NetUid::from(202); - // Sanity: V3 is not initialized - assert!(!SwapV3Initialized::::get(netuid)); - assert!( - Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) - .next() - .is_none() - ); + // Insert map values + FeeRate::::insert(netuid, 1_000); + PalSwapInitialized::::insert(netuid, true); + let w_quote_pt = Perquintill::from_rational(1u128, 2u128); + let bal = Balancer::new(w_quote_pt).unwrap(); + SwapBalancer::::insert(netuid, bal); - // ACT - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); + // Sanity: PalSwap is not initialized + assert!(PalSwapInitialized::::get(netuid)); - // ASSERT: Defensive clears leave no residues and do not panic - assert!( - Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) - .next() - .is_none() - ); - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!( - TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_none() - ); + // ACT + assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); // All single-key maps should not have the key after liquidation - assert!(!FeeGlobalTao::::contains_key(netuid)); - assert!(!FeeGlobalAlpha::::contains_key(netuid)); - assert!(!CurrentLiquidity::::contains_key(netuid)); - assert!(!CurrentTick::::contains_key(netuid)); - assert!(!AlphaSqrtPrice::::contains_key(netuid)); - assert!(!SwapV3Initialized::::contains_key(netuid)); assert!(!FeeRate::::contains_key(netuid)); - assert!(!EnabledUserLiquidity::::contains_key(netuid)); - }); -} - -#[test] -fn test_liquidate_idempotent() { - // V3 flavor - new_test_ext().execute_with(|| { - let netuid = NetUid::from(7); - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Add a small user position - assert_ok!(Swap::toggle_user_liquidity( - RuntimeOrigin::root(), - netuid.into(), - true - )); - let tick_low = price_to_tick(0.2); - let tick_high = price_to_tick(0.3); - assert_ok!(Pallet::::do_add_liquidity( - netuid, - &OK_COLDKEY_ACCOUNT_ID, - &OK_HOTKEY_ACCOUNT_ID, - tick_low, - tick_high, - 123_456_789 - )); - - // Users-only liquidations are idempotent. - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - - // Now clear protocol liquidity/state—also idempotent. - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - - // State remains empty - assert!( - Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) - .next() - .is_none() - ); - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!( - TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_none() - ); - assert!(!SwapV3Initialized::::contains_key(netuid)); - }); - - // Non‑V3 flavor - new_test_ext().execute_with(|| { - let netuid = NetUid::from(8); - - // Never initialize V3; both calls no-op and succeed. - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - - assert!( - Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) - .next() - .is_none() - ); - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!( - TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_none() - ); - assert!(!SwapV3Initialized::::contains_key(netuid)); - }); -} - -#[test] -fn refund_alpha_single_provider_exact() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(11); - let cold = OK_COLDKEY_ACCOUNT_ID; - let hot = OK_HOTKEY_ACCOUNT_ID; - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // --- Create an alpha‑only position (range entirely above current tick → TAO = 0, ALPHA > 0). - let ct = CurrentTick::::get(netuid); - let tick_low = ct.next().expect("current tick should not be MAX in tests"); - let tick_high = TickIndex::MAX; - - let liquidity = 1_000_000_u64; - let (_pos_id, tao_needed, alpha_needed) = - Pallet::::do_add_liquidity(netuid, &cold, &hot, tick_low, tick_high, liquidity) - .expect("add alpha-only liquidity"); - assert_eq!(tao_needed, 0, "alpha-only position must not require TAO"); - assert!(alpha_needed > 0, "alpha-only position must require ALPHA"); - - // --- Snapshot BEFORE we withdraw funds (baseline for conservation). - let alpha_before_hot = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); - let alpha_before_owner = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); - let alpha_before_total = alpha_before_hot + alpha_before_owner; - - // --- Mimic extrinsic bookkeeping: withdraw α and record provided reserve. - ::BalanceOps::decrease_stake( - &cold, - &hot, - netuid.into(), - alpha_needed.into(), - ) - .expect("decrease ALPHA"); - AlphaReserve::increase_provided(netuid.into(), alpha_needed.into()); - - // --- Act: users‑only dissolve. - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - - // --- Assert: total α conserved to owner (may be staked to validator). - let alpha_after_hot = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); - let alpha_after_owner = - ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); - let alpha_after_total = alpha_after_hot + alpha_after_owner; - assert_eq!( - alpha_after_total, alpha_before_total, - "ALPHA principal must be conserved to the account" - ); - - // Clear protocol liquidity and V3 state now. - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - - // --- State is cleared. - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert_eq!(Pallet::::count_positions(netuid, &cold), 0); - assert!(!SwapV3Initialized::::contains_key(netuid)); - }); -} - -#[test] -fn refund_alpha_multiple_providers_proportional_to_principal() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(12); - let c1 = OK_COLDKEY_ACCOUNT_ID; - let h1 = OK_HOTKEY_ACCOUNT_ID; - let c2 = OK_COLDKEY_ACCOUNT_ID_2; - let h2 = OK_HOTKEY_ACCOUNT_ID_2; - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Use the same "above current tick" trick for alpha‑only positions. - let ct = CurrentTick::::get(netuid); - let tick_low = ct.next().expect("current tick should not be MAX in tests"); - let tick_high = TickIndex::MAX; - - // Provider #1 (smaller α) - let liq1 = 700_000_u64; - let (_p1, t1, a1) = - Pallet::::do_add_liquidity(netuid, &c1, &h1, tick_low, tick_high, liq1) - .expect("add alpha-only liquidity #1"); - assert_eq!(t1, 0); - assert!(a1 > 0); - - // Provider #2 (larger α) - let liq2 = 2_100_000_u64; - let (_p2, t2, a2) = - Pallet::::do_add_liquidity(netuid, &c2, &h2, tick_low, tick_high, liq2) - .expect("add alpha-only liquidity #2"); - assert_eq!(t2, 0); - assert!(a2 > 0); - - // Baselines BEFORE withdrawing - let a1_before_hot = ::BalanceOps::alpha_balance(netuid.into(), &c1, &h1); - let a1_before_owner = ::BalanceOps::alpha_balance(netuid.into(), &c1, &c1); - let a1_before = a1_before_hot + a1_before_owner; - - let a2_before_hot = ::BalanceOps::alpha_balance(netuid.into(), &c2, &h2); - let a2_before_owner = ::BalanceOps::alpha_balance(netuid.into(), &c2, &c2); - let a2_before = a2_before_hot + a2_before_owner; - - // Withdraw alpha and account reserves for each provider. - ::BalanceOps::decrease_stake(&c1, &h1, netuid.into(), a1.into()) - .expect("decrease alpha #1"); - AlphaReserve::increase_provided(netuid.into(), a1.into()); - - ::BalanceOps::decrease_stake(&c2, &h2, netuid.into(), a2.into()) - .expect("decrease alpha #2"); - AlphaReserve::increase_provided(netuid.into(), a2.into()); - - // Act - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - - // Each owner is restored to their exact baseline. - let a1_after_hot = ::BalanceOps::alpha_balance(netuid.into(), &c1, &h1); - let a1_after_owner = ::BalanceOps::alpha_balance(netuid.into(), &c1, &c1); - let a1_after = a1_after_hot + a1_after_owner; - assert_eq!( - a1_after, a1_before, - "owner #1 must receive their α principal back" - ); - - let a2_after_hot = ::BalanceOps::alpha_balance(netuid.into(), &c2, &h2); - let a2_after_owner = ::BalanceOps::alpha_balance(netuid.into(), &c2, &c2); - let a2_after = a2_after_hot + a2_after_owner; - assert_eq!( - a2_after, a2_before, - "owner #2 must receive their α principal back" - ); - }); -} - -#[test] -fn refund_alpha_same_cold_multiple_hotkeys_conserved_to_owner() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(13); - let cold = OK_COLDKEY_ACCOUNT_ID; - let hot1 = OK_HOTKEY_ACCOUNT_ID; - let hot2 = OK_HOTKEY_ACCOUNT_ID_2; - - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - - // Two alpha‑only positions on different hotkeys of the same owner. - let ct = CurrentTick::::get(netuid); - let tick_low = ct.next().expect("current tick should not be MAX in tests"); - let tick_high = TickIndex::MAX; - - let (_p1, _t1, a1) = - Pallet::::do_add_liquidity(netuid, &cold, &hot1, tick_low, tick_high, 900_000) - .expect("add alpha-only pos (hot1)"); - let (_p2, _t2, a2) = - Pallet::::do_add_liquidity(netuid, &cold, &hot2, tick_low, tick_high, 1_500_000) - .expect("add alpha-only pos (hot2)"); - assert!(a1 > 0 && a2 > 0); - - // Baseline BEFORE: sum over (cold,hot1) + (cold,hot2) + (cold,cold). - let before_hot1 = ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot1); - let before_hot2 = ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot2); - let before_owner = ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); - let before_total = before_hot1 + before_hot2 + before_owner; - - // Withdraw alpha from both hotkeys; track provided‑reserve. - ::BalanceOps::decrease_stake(&cold, &hot1, netuid.into(), a1.into()) - .expect("decr alpha #hot1"); - AlphaReserve::increase_provided(netuid.into(), a1.into()); - - ::BalanceOps::decrease_stake(&cold, &hot2, netuid.into(), a2.into()) - .expect("decr alpha #hot2"); - AlphaReserve::increase_provided(netuid.into(), a2.into()); - - // Act - assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - - // The total α "owned" by the coldkey is conserved (credit may land on (cold,cold)). - let after_hot1 = ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot1); - let after_hot2 = ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot2); - let after_owner = ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); - let after_total = after_hot1 + after_hot2 + after_owner; - - assert_eq!( - after_total, before_total, - "owner’s α must be conserved across hot ledgers + (owner,owner)" - ); + assert!(!PalSwapInitialized::::contains_key(netuid)); + assert!(!SwapBalancer::::contains_key(netuid)); }); } @@ -2383,249 +770,100 @@ fn refund_alpha_same_cold_multiple_hotkeys_conserved_to_owner() { fn test_clear_protocol_liquidity_green_path() { new_test_ext().execute_with(|| { // --- Arrange --- - let netuid = NetUid::from(55); - - // Ensure the "user liquidity enabled" flag exists so we can verify it's removed later. - assert_ok!(Pallet::::toggle_user_liquidity( - RuntimeOrigin::root(), - netuid, - true - )); - - // Initialize V3 state; this should set price/tick flags and create a protocol position. - assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - assert!( - SwapV3Initialized::::get(netuid), - "V3 must be initialized" - ); + let netuid = NetUid::from(1); - // Sanity: protocol positions exist before clearing. - let protocol_id = Pallet::::protocol_account_id(); - let prot_positions_before = - Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); + // Initialize swap state + assert_ok!(Pallet::::maybe_initialize_palswap(netuid, None)); assert!( - !prot_positions_before.is_empty(), - "protocol positions should exist after V3 init" + PalSwapInitialized::::get(netuid), + "Swap must be initialized" ); // --- Act --- // Green path: just clear protocol liquidity and wipe all V3 state. assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - // --- Assert: all protocol positions removed --- - let prot_positions_after = - Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); - assert!( - prot_positions_after.is_empty(), - "protocol positions must be removed by do_clear_protocol_liquidity" - ); - - // --- Assert: V3 data wiped (idempotent even if some maps were empty) --- - // Ticks / active tick bitmap - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!( - TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_none(), - "active tick bitmap words must be cleared" - ); - - // Fee globals - assert!(!FeeGlobalTao::::contains_key(netuid)); - assert!(!FeeGlobalAlpha::::contains_key(netuid)); - - // Price / tick / liquidity / flags - assert!(!AlphaSqrtPrice::::contains_key(netuid)); - assert!(!CurrentTick::::contains_key(netuid)); - assert!(!CurrentLiquidity::::contains_key(netuid)); - assert!(!SwapV3Initialized::::contains_key(netuid)); + // Flags + assert!(!PalSwapInitialized::::contains_key(netuid)); // Knobs removed assert!(!FeeRate::::contains_key(netuid)); - assert!(!EnabledUserLiquidity::::contains_key(netuid)); // --- And it's idempotent --- assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); - assert!( - Positions::::iter_prefix_values((netuid, protocol_id)) - .next() - .is_none() - ); - assert!(Ticks::::iter_prefix(netuid).next().is_none()); - assert!( - TickIndexBitmapWords::::iter_prefix((netuid,)) - .next() - .is_none() - ); - assert!(!SwapV3Initialized::::contains_key(netuid)); + assert!(!PalSwapInitialized::::contains_key(netuid)); }); } -fn as_tuple( - (t_used, a_used, t_rem, a_rem): (TaoBalance, AlphaBalance, TaoBalance, AlphaBalance), -) -> (u64, u64, u64, u64) { - ( - u64::from(t_used), - u64::from(a_used), - u64::from(t_rem), - u64::from(a_rem), - ) -} - +// cargo test --package pallet-subtensor-swap --lib -- pallet::tests::test_migrate_swapv3_to_balancer --exact --nocapture #[test] -fn proportional_when_price_is_one_and_tao_is_plenty() { - // sqrt_price = 1.0 => price = 1.0 - let sqrt = U64F64::from_num(1u64); - let amount_tao: TaoBalance = 10u64.into(); - let amount_alpha: AlphaBalance = 3u64.into(); - - // alpha * price = 3 * 1 = 3 <= amount_tao(10) - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, amount_tao, amount_alpha); - assert_eq!(as_tuple(out), (3, 3, 7, 0)); -} +fn test_migrate_swapv3_to_balancer() { + use crate::migrations::migrate_swapv3_to_balancer::deprecated_swap_maps; + use substrate_fixed::types::U64F64; -#[test] -fn proportional_when_price_is_one_and_alpha_is_excess() { - // sqrt_price = 1.0 => price = 1.0 - let sqrt = U64F64::from_num(1u64); - let amount_tao: TaoBalance = 5u64.into(); - let amount_alpha: AlphaBalance = 10u64.into(); - - // tao is limiting: alpha_equiv = floor(5 / 1) = 5 - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, amount_tao, amount_alpha); - assert_eq!(as_tuple(out), (5, 5, 0, 5)); -} + new_test_ext().execute_with(|| { + let migration = + crate::migrations::migrate_swapv3_to_balancer::migrate_swapv3_to_balancer::; + let netuid = NetUid::from(1); -#[test] -fn proportional_with_higher_price_and_alpha_limiting() { - // Choose sqrt_price = 2.0 => price = 4.0 (since implementation squares it) - let sqrt = U64F64::from_num(2u64); - let amount_tao: TaoBalance = 85u64.into(); - let amount_alpha: AlphaBalance = 20u64.into(); - - // tao_equivalent = alpha * price = 20 * 4 = 80 < 85 => alpha limits tao - // remainders: tao 5, alpha 0 - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, amount_tao, amount_alpha); - assert_eq!(as_tuple(out), (80, 20, 5, 0)); -} + // Insert deprecated maps values + deprecated_swap_maps::AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1.23)); + deprecated_swap_maps::ScrapReservoirTao::::insert(netuid, TaoBalance::from(9876)); + deprecated_swap_maps::ScrapReservoirAlpha::::insert(netuid, AlphaBalance::from(9876)); -#[test] -fn proportional_with_higher_price_and_tao_limiting() { - // Choose sqrt_price = 2.0 => price = 4.0 (since implementation squares it) - let sqrt = U64F64::from_num(2u64); - let amount_tao: TaoBalance = 50u64.into(); - let amount_alpha: AlphaBalance = 20u64.into(); - - // tao_equivalent = alpha * price = 20 * 4 = 80 > 50 => tao limits alpha - // alpha_equivalent = floor(50 / 4) = 12 - // remainders: tao 0, alpha 20 - 12 = 8 - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, amount_tao, amount_alpha); - assert_eq!(as_tuple(out), (50, 12, 0, 8)); -} + // Insert reserves that do not match the 1.23 price + TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1_000_000_000)); + AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(4_000_000_000_u64)); -#[test] -fn zero_price_uses_no_tao_and_all_alpha() { - // sqrt_price = 0 => price = 0 - let sqrt = U64F64::from_num(0u64); - let amount_tao: TaoBalance = 42u64.into(); - let amount_alpha: AlphaBalance = 17u64.into(); - - // tao_equivalent = 17 * 0 = 0 <= 42 - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, amount_tao, amount_alpha); - assert_eq!(as_tuple(out), (0, 17, 42, 0)); -} + // Run migration + migration(); -#[test] -fn rounding_down_behavior_when_dividing_by_price() { - // sqrt_price = 2.0 => price = 4.0 - let sqrt = U64F64::from_num(2u64); - let amount_tao: TaoBalance = 13u64.into(); - let amount_alpha: AlphaBalance = 100u64.into(); - - // tao is limiting; alpha_equiv = floor(13 / 4) = 3 - // remainders: tao 0, alpha 100 - 3 = 97 - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, amount_tao, amount_alpha); - assert_eq!(as_tuple(out), (13, 3, 0, 97)); -} + // Test that values are removed from state + assert!(!deprecated_swap_maps::AlphaSqrtPrice::::contains_key( + netuid + )); + assert!(!deprecated_swap_maps::ScrapReservoirAlpha::::contains_key(netuid)); -#[test] -fn exact_fit_when_tao_matches_alpha_times_price() { - // sqrt_price = 1.0 => price = 1.0 - let sqrt = U64F64::from_num(1u64); - let amount_tao: TaoBalance = 9u64.into(); - let amount_alpha: AlphaBalance = 9u64.into(); - - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, amount_tao, amount_alpha); - assert_eq!(as_tuple(out), (9, 9, 0, 0)); + // Test that subnet price is still 1.23^2 + assert_abs_diff_eq!( + Swap::current_price(netuid).to_num::(), + 1.23 * 1.23, + epsilon = 0.1 + ); + }); } #[test] -fn handles_zero_balances() { - let sqrt = U64F64::from_num(1u64); - - // Zero TAO, some alpha - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, 0u64.into(), 7u64.into()); - // tao limits; alpha_equiv = floor(0 / 1) = 0 - assert_eq!(as_tuple(out), (0, 0, 0, 7)); - - // Some TAO, zero alpha - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, 7u64.into(), 0u64.into()); - // tao_equiv = 0 * 1 = 0 <= 7 - assert_eq!(as_tuple(out), (0, 0, 7, 0)); - - // Both zero - let out = - Pallet::::get_proportional_alpha_tao_and_remainders(sqrt, 0u64.into(), 0u64.into()); - assert_eq!(as_tuple(out), (0, 0, 0, 0)); -} +fn test_migrate_swapv3_to_balancer_falls_back_to_default_when_price_init_fails() { + use crate::migrations::migrate_swapv3_to_balancer::deprecated_swap_maps; + use substrate_fixed::types::U64F64; -#[test] -fn adjust_protocol_liquidity_uses_and_sets_scrap_reservoirs() { new_test_ext().execute_with(|| { - // --- Arrange - let netuid: NetUid = 1u16.into(); - // Price = 1.0 (since sqrt_price^2 = 1), so proportional match is 1:1 - AlphaSqrtPrice::::insert(netuid, U64F64::saturating_from_num(1u64)); - - // Start with some non-zero scrap reservoirs - ScrapReservoirTao::::insert(netuid, TaoBalance::from(7u64)); - ScrapReservoirAlpha::::insert(netuid, AlphaBalance::from(5u64)); - - // Create a minimal protocol position so the function’s body executes. - let protocol = Pallet::::protocol_account_id(); - let position = Position::new( - PositionId::from(0), - netuid, - TickIndex::MIN, - TickIndex::MAX, - 0, - ); - // Ensure collect_fees() returns (0,0) via zeroed fees in `position` (default). - Positions::::insert((netuid, protocol, position.id), position.clone()); + let migration = + crate::migrations::migrate_swapv3_to_balancer::migrate_swapv3_to_balancer::; + let migration_name = + frame_support::BoundedVec::truncate_from(b"migrate_swapv3_to_balancer".to_vec()); + let netuid = NetUid::from(1); - // --- Act - // No external deltas or fees; only reservoirs should be considered. - // With price=1, the exact proportional pair uses 5 alpha and 5 tao, - // leaving tao scrap = 7 - 5 = 2, alpha scrap = 5 - 5 = 0. - Pallet::::adjust_protocol_liquidity(netuid, 0u64.into(), 0u64.into()); + deprecated_swap_maps::AlphaSqrtPrice::::insert(netuid, U64F64::from_num(1)); + deprecated_swap_maps::ScrapReservoirTao::::insert(netuid, TaoBalance::from(9876)); + deprecated_swap_maps::ScrapReservoirAlpha::::insert(netuid, AlphaBalance::from(9876)); - // --- Assert: reservoirs were READ (used in proportional calc) and then SET (updated) - assert_eq!( - ScrapReservoirTao::::get(netuid), - TaoBalance::from(2u64) - ); + TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1)); + AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(1_000_000_000_000_u64)); + + migration(); + + assert!(!deprecated_swap_maps::AlphaSqrtPrice::::contains_key( + netuid + )); + assert!(!deprecated_swap_maps::ScrapReservoirTao::::contains_key(netuid)); + assert!(!deprecated_swap_maps::ScrapReservoirAlpha::::contains_key(netuid)); + assert!(PalSwapInitialized::::get(netuid)); assert_eq!( - ScrapReservoirAlpha::::get(netuid), - AlphaBalance::from(0u64) + SwapBalancer::::get(netuid).get_quote_weight(), + Perquintill::from_rational(1_u64, 2_u64) ); + assert!(HasMigrationRun::::get(&migration_name)); }); } diff --git a/pallets/swap/src/position.rs b/pallets/swap/src/position.rs deleted file mode 100644 index 5a57928a93..0000000000 --- a/pallets/swap/src/position.rs +++ /dev/null @@ -1,198 +0,0 @@ -use core::marker::PhantomData; - -use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; -use frame_support::pallet_prelude::*; -use safe_math::*; -use substrate_fixed::types::{I64F64, U64F64}; -use subtensor_macros::freeze_struct; -use subtensor_runtime_common::NetUid; - -use crate::SqrtPrice; -use crate::pallet::{Config, Error, FeeGlobalAlpha, FeeGlobalTao, LastPositionId}; -use crate::tick::TickIndex; - -/// Position designates one liquidity position. -/// -/// Alpha price is expressed in rao units per one 10^9 unit. For example, -/// price 1_000_000 is equal to 0.001 TAO per Alpha. -#[freeze_struct("27a1bf8c59480f0")] -#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen, Default)] -#[scale_info(skip_type_params(T))] -pub struct Position { - /// Unique ID of the position - pub id: PositionId, - /// Network identifier - pub netuid: NetUid, - /// Tick index for lower boundary of price - pub tick_low: TickIndex, - /// Tick index for higher boundary of price - pub tick_high: TickIndex, - /// Position liquidity - pub liquidity: u64, - /// Fees accrued by the position in quote currency (TAO) relative to global fees - pub fees_tao: I64F64, - /// Fees accrued by the position in base currency (Alpha) relative to global fees - pub fees_alpha: I64F64, - /// Phantom marker for generic Config type - pub _phantom: PhantomData, -} - -impl Position { - pub fn new( - id: PositionId, - netuid: NetUid, - tick_low: TickIndex, - tick_high: TickIndex, - liquidity: u64, - ) -> Self { - let mut position = Position { - id, - netuid, - tick_low, - tick_high, - liquidity, - fees_tao: I64F64::saturating_from_num(0), - fees_alpha: I64F64::saturating_from_num(0), - _phantom: PhantomData, - }; - - position.fees_tao = position.fees_in_range(true); - position.fees_alpha = position.fees_in_range(false); - - position - } - - /// Converts position to token amounts - /// - /// returns tuple of (TAO, Alpha) - /// - /// Pseudocode: - /// if self.sqrt_price_curr < sqrt_pa: - /// tao = 0 - /// alpha = L * (1 / sqrt_pa - 1 / sqrt_pb) - /// elif self.sqrt_price_curr > sqrt_pb: - /// tao = L * (sqrt_pb - sqrt_pa) - /// alpha = 0 - /// else: - /// tao = L * (self.sqrt_price_curr - sqrt_pa) - /// alpha = L * (1 / self.sqrt_price_curr - 1 / sqrt_pb) - /// - pub fn to_token_amounts(&self, sqrt_price_curr: SqrtPrice) -> Result<(u64, u64), Error> { - let one = U64F64::saturating_from_num(1); - - let sqrt_price_low = self - .tick_low - .try_to_sqrt_price() - .map_err(|_| Error::::InvalidTickRange)?; - let sqrt_price_high = self - .tick_high - .try_to_sqrt_price() - .map_err(|_| Error::::InvalidTickRange)?; - let liquidity_fixed = U64F64::saturating_from_num(self.liquidity); - - Ok(if sqrt_price_curr < sqrt_price_low { - ( - 0, - liquidity_fixed - .saturating_mul( - one.safe_div(sqrt_price_low) - .saturating_sub(one.safe_div(sqrt_price_high)), - ) - .saturating_to_num::(), - ) - } else if sqrt_price_curr > sqrt_price_high { - ( - liquidity_fixed - .saturating_mul(sqrt_price_high.saturating_sub(sqrt_price_low)) - .saturating_to_num::(), - 0, - ) - } else { - ( - liquidity_fixed - .saturating_mul(sqrt_price_curr.saturating_sub(sqrt_price_low)) - .saturating_to_num::(), - liquidity_fixed - .saturating_mul( - one.safe_div(sqrt_price_curr) - .saturating_sub(one.safe_div(sqrt_price_high)), - ) - .saturating_to_num::(), - ) - }) - } - - /// Collect fees for a position - /// Updates the position - pub fn collect_fees(&mut self) -> (u64, u64) { - let fee_tao_agg = self.fees_in_range(true); - let fee_alpha_agg = self.fees_in_range(false); - - let mut fee_tao = fee_tao_agg.saturating_sub(self.fees_tao); - let mut fee_alpha = fee_alpha_agg.saturating_sub(self.fees_alpha); - - self.fees_tao = fee_tao_agg; - self.fees_alpha = fee_alpha_agg; - - let liquidity_frac = I64F64::saturating_from_num(self.liquidity); - - fee_tao = liquidity_frac.saturating_mul(fee_tao); - fee_alpha = liquidity_frac.saturating_mul(fee_alpha); - - ( - fee_tao.saturating_to_num::(), - fee_alpha.saturating_to_num::(), - ) - } - - /// Get fees in a position's range - /// - /// If quote flag is true, Tao is returned, otherwise alpha. - fn fees_in_range(&self, quote: bool) -> I64F64 { - if quote { - I64F64::saturating_from_num(FeeGlobalTao::::get(self.netuid)) - } else { - I64F64::saturating_from_num(FeeGlobalAlpha::::get(self.netuid)) - } - .saturating_sub(self.tick_low.fees_below::(self.netuid, quote)) - .saturating_sub(self.tick_high.fees_above::(self.netuid, quote)) - } -} - -#[freeze_struct("8501fa251c9d74c")] -#[derive( - Clone, - Copy, - Decode, - DecodeWithMemTracking, - Default, - Encode, - Eq, - MaxEncodedLen, - PartialEq, - RuntimeDebug, - TypeInfo, -)] -pub struct PositionId(u128); - -impl PositionId { - /// Create a new position ID - pub fn new() -> Self { - let new = LastPositionId::::get().saturating_add(1); - LastPositionId::::put(new); - - Self(new) - } -} - -impl From for PositionId { - fn from(value: u128) -> Self { - Self(value) - } -} - -impl From for u128 { - fn from(value: PositionId) -> Self { - value.0 - } -} diff --git a/pallets/swap/src/tick.rs b/pallets/swap/src/tick.rs deleted file mode 100644 index d3493fde45..0000000000 --- a/pallets/swap/src/tick.rs +++ /dev/null @@ -1,2198 +0,0 @@ -//! The math is adapted from github.com/0xKitsune/uniswap-v3-math -use core::cmp::Ordering; -use core::convert::TryFrom; -use core::error::Error; -use core::fmt; -use core::hash::Hash; -use core::ops::{Add, AddAssign, BitOr, Deref, Neg, Shl, Shr, Sub, SubAssign}; - -use alloy_primitives::{I256, U256}; -use codec::{Decode, DecodeWithMemTracking, Encode, Error as CodecError, Input, MaxEncodedLen}; -use frame_support::pallet_prelude::*; -use safe_math::*; -use sp_std::vec; -use sp_std::vec::Vec; -use substrate_fixed::types::{I64F64, U64F64}; -use subtensor_macros::freeze_struct; -use subtensor_runtime_common::NetUid; - -use crate::SqrtPrice; -use crate::pallet::{ - Config, CurrentTick, FeeGlobalAlpha, FeeGlobalTao, TickIndexBitmapWords, Ticks, -}; - -const U256_1: U256 = U256::from_limbs([1, 0, 0, 0]); -const U256_2: U256 = U256::from_limbs([2, 0, 0, 0]); -const U256_3: U256 = U256::from_limbs([3, 0, 0, 0]); -const U256_4: U256 = U256::from_limbs([4, 0, 0, 0]); -const U256_5: U256 = U256::from_limbs([5, 0, 0, 0]); -const U256_6: U256 = U256::from_limbs([6, 0, 0, 0]); -const U256_7: U256 = U256::from_limbs([7, 0, 0, 0]); -const U256_8: U256 = U256::from_limbs([8, 0, 0, 0]); -const U256_15: U256 = U256::from_limbs([15, 0, 0, 0]); -const U256_16: U256 = U256::from_limbs([16, 0, 0, 0]); -const U256_32: U256 = U256::from_limbs([32, 0, 0, 0]); -const U256_64: U256 = U256::from_limbs([64, 0, 0, 0]); -const U256_127: U256 = U256::from_limbs([127, 0, 0, 0]); -const U256_128: U256 = U256::from_limbs([128, 0, 0, 0]); -const U256_255: U256 = U256::from_limbs([255, 0, 0, 0]); - -const U256_256: U256 = U256::from_limbs([256, 0, 0, 0]); -const U256_512: U256 = U256::from_limbs([512, 0, 0, 0]); -const U256_1024: U256 = U256::from_limbs([1024, 0, 0, 0]); -const U256_2048: U256 = U256::from_limbs([2048, 0, 0, 0]); -const U256_4096: U256 = U256::from_limbs([4096, 0, 0, 0]); -const U256_8192: U256 = U256::from_limbs([8192, 0, 0, 0]); -const U256_16384: U256 = U256::from_limbs([16384, 0, 0, 0]); -const U256_32768: U256 = U256::from_limbs([32768, 0, 0, 0]); -const U256_65536: U256 = U256::from_limbs([65536, 0, 0, 0]); -const U256_131072: U256 = U256::from_limbs([131072, 0, 0, 0]); -const U256_262144: U256 = U256::from_limbs([262144, 0, 0, 0]); -const U256_524288: U256 = U256::from_limbs([524288, 0, 0, 0]); - -const U256_MAX_TICK: U256 = U256::from_limbs([887272, 0, 0, 0]); - -const MIN_TICK: i32 = -887272; -const MAX_TICK: i32 = -MIN_TICK; - -const MIN_SQRT_RATIO: U256 = U256::from_limbs([4295128739, 0, 0, 0]); -const MAX_SQRT_RATIO: U256 = - U256::from_limbs([6743328256752651558, 17280870778742802505, 4294805859, 0]); - -const SQRT_10001: I256 = I256::from_raw(U256::from_limbs([11745905768312294533, 13863, 0, 0])); -const TICK_LOW: I256 = I256::from_raw(U256::from_limbs([ - 6552757943157144234, - 184476617836266586, - 0, - 0, -])); -const TICK_HIGH: I256 = I256::from_raw(U256::from_limbs([ - 4998474450511881007, - 15793544031827761793, - 0, - 0, -])); - -/// Tick is the price range determined by tick index (not part of this struct, but is the key at -/// which the Tick is stored in state hash maps). Tick struct stores liquidity and fee information. -/// -/// - Net liquidity -/// - Gross liquidity -/// - Fees (above global) in both currencies -#[freeze_struct("ff1bce826e64c4aa")] -#[derive(Debug, Default, Clone, Encode, Decode, TypeInfo, MaxEncodedLen, PartialEq, Eq)] -pub struct Tick { - pub liquidity_net: i128, - pub liquidity_gross: u64, - pub fees_out_tao: I64F64, - pub fees_out_alpha: I64F64, -} - -impl Tick { - pub fn liquidity_net_as_u64(&self) -> u64 { - self.liquidity_net.abs().min(u64::MAX as i128) as u64 - } -} - -/// Struct representing a tick index -#[freeze_struct("13c1f887258657f2")] -#[derive( - Debug, - Default, - Clone, - Copy, - Encode, - DecodeWithMemTracking, - TypeInfo, - MaxEncodedLen, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, -)] -pub struct TickIndex(i32); - -impl Decode for TickIndex { - fn decode(input: &mut I) -> Result { - let raw = i32::decode(input)?; - TickIndex::new(raw).map_err(|_| "TickIndex out of bounds".into()) - } -} - -impl Add for TickIndex { - type Output = Self; - - #[allow(clippy::arithmetic_side_effects)] - fn add(self, rhs: Self) -> Self::Output { - // Note: This assumes the result is within bounds. - // For a safer implementation, consider using checked_add. - Self::new_unchecked(self.get() + rhs.get()) - } -} - -impl Sub for TickIndex { - type Output = Self; - - #[allow(clippy::arithmetic_side_effects)] - fn sub(self, rhs: Self) -> Self::Output { - // Note: This assumes the result is within bounds. - // For a safer implementation, consider using checked_sub. - Self::new_unchecked(self.get() - rhs.get()) - } -} - -impl AddAssign for TickIndex { - #[allow(clippy::arithmetic_side_effects)] - fn add_assign(&mut self, rhs: Self) { - *self = Self::new_unchecked(self.get() + rhs.get()); - } -} - -impl SubAssign for TickIndex { - #[allow(clippy::arithmetic_side_effects)] - fn sub_assign(&mut self, rhs: Self) { - *self = Self::new_unchecked(self.get() - rhs.get()); - } -} - -impl TryFrom for TickIndex { - type Error = TickMathError; - - fn try_from(value: i32) -> Result { - Self::new(value) - } -} - -impl Deref for TickIndex { - type Target = i32; - - fn deref(&self) -> &Self::Target { - // Using get() would create an infinite recursion, so this is one place where we need direct - // field access. This is safe because Self::Target is i32, which is exactly what we're - // storing - &self.0 - } -} - -/// Extension trait to make working with TryFrom more ergonomic -pub trait TryIntoTickIndex { - /// Convert an i32 into a TickIndex, with bounds checking - fn into_tick_index(self) -> Result; -} - -impl TryIntoTickIndex for i32 { - fn into_tick_index(self) -> Result { - TickIndex::try_from(self) - } -} - -impl TickIndex { - /// Minimum value of the tick index - /// The tick_math library uses different bitness, so we have to divide by 2. - /// It's unsafe to change this value to something else. - pub const MIN: Self = Self(MIN_TICK.saturating_div(2)); - - /// Maximum value of the tick index - /// The tick_math library uses different bitness, so we have to divide by 2. - /// It's unsafe to change this value to something else. - pub const MAX: Self = Self(MAX_TICK.saturating_div(2)); - - /// All tick indexes are offset by this value for storage needs - /// so that tick indexes are positive, which simplifies bit logic - const OFFSET: Self = Self(MAX_TICK); - - /// The MIN sqrt price, which is caclculated at Self::MIN - pub fn min_sqrt_price() -> SqrtPrice { - SqrtPrice::saturating_from_num(0.0000000002328350195) - } - - /// The MAX sqrt price, which is calculated at Self::MAX - #[allow(clippy::excessive_precision)] - pub fn max_sqrt_price() -> SqrtPrice { - SqrtPrice::saturating_from_num(4294886577.20989222513899790805) - } - - /// Get fees above a tick - pub fn fees_above(&self, netuid: NetUid, quote: bool) -> I64F64 { - let current_tick = Self::current_bounded::(netuid); - - let tick = Ticks::::get(netuid, *self).unwrap_or_default(); - if *self <= current_tick { - if quote { - I64F64::saturating_from_num(FeeGlobalTao::::get(netuid)) - .saturating_sub(tick.fees_out_tao) - } else { - I64F64::saturating_from_num(FeeGlobalAlpha::::get(netuid)) - .saturating_sub(tick.fees_out_alpha) - } - } else if quote { - tick.fees_out_tao - } else { - tick.fees_out_alpha - } - } - - /// Get fees below a tick - pub fn fees_below(&self, netuid: NetUid, quote: bool) -> I64F64 { - let current_tick = Self::current_bounded::(netuid); - - let tick = Ticks::::get(netuid, *self).unwrap_or_default(); - if *self <= current_tick { - if quote { - tick.fees_out_tao - } else { - tick.fees_out_alpha - } - } else if quote { - I64F64::saturating_from_num(FeeGlobalTao::::get(netuid)) - .saturating_sub(tick.fees_out_tao) - } else { - I64F64::saturating_from_num(FeeGlobalAlpha::::get(netuid)) - .saturating_sub(tick.fees_out_alpha) - } - } - - /// Get the current tick index for a subnet, ensuring it's within valid bounds - pub fn current_bounded(netuid: NetUid) -> Self { - let current_tick = CurrentTick::::get(netuid); - if current_tick > Self::MAX { - Self::MAX - } else if current_tick < Self::MIN { - Self::MIN - } else { - current_tick - } - } - - /// Converts a sqrt price to a tick index, ensuring it's within valid bounds - /// - /// If the price is outside the valid range, this function will return the appropriate boundary - /// tick index (MIN or MAX) instead of an error. - /// - /// # Arguments - /// * `sqrt_price` - The square root price to convert to a tick index - /// - /// # Returns - /// * `TickIndex` - A tick index that is guaranteed to be within valid bounds - pub fn from_sqrt_price_bounded(sqrt_price: SqrtPrice) -> Self { - match Self::try_from_sqrt_price(sqrt_price) { - Ok(index) => index, - Err(_) => { - let max_price = Self::MAX.as_sqrt_price_bounded(); - - if sqrt_price > max_price { - Self::MAX - } else { - Self::MIN - } - } - } - } - - /// Converts a tick index to a sqrt price, ensuring it's within valid bounds - /// - /// Unlike try_to_sqrt_price which returns an error for boundary indices, this function - /// guarantees a valid sqrt price by using fallback values if conversion fails. - /// - /// # Returns - /// * `SqrtPrice` - A sqrt price that is guaranteed to be a valid value - pub fn as_sqrt_price_bounded(&self) -> SqrtPrice { - self.try_to_sqrt_price().unwrap_or_else(|_| { - if *self >= Self::MAX { - Self::max_sqrt_price() - } else { - Self::min_sqrt_price() - } - }) - } - - /// Creates a new TickIndex instance with bounds checking - pub fn new(value: i32) -> Result { - if !(Self::MIN.0..=Self::MAX.0).contains(&value) { - Err(TickMathError::TickOutOfBounds) - } else { - Ok(Self(value)) - } - } - - /// Creates a new TickIndex without bounds checking - /// Use this function with caution, only when you're certain the value is valid - pub fn new_unchecked(value: i32) -> Self { - Self(value) - } - - /// Get the inner value - pub fn get(&self) -> i32 { - self.0 - } - - /// Creates a TickIndex from an offset representation (u32) - /// - /// # Arguments - /// * `offset_index` - An offset index (u32 value) representing a tick index - /// - /// # Returns - /// * `Result` - The corresponding TickIndex if within valid bounds - pub fn from_offset_index(offset_index: u32) -> Result { - // while it's safe, we use saturating math to mute the linter and just in case - let signed_index = ((offset_index as i64).saturating_sub(Self::OFFSET.get() as i64)) as i32; - Self::new(signed_index) - } - - /// Get the next tick index (incrementing by 1) - pub fn next(&self) -> Result { - Self::new(self.0.saturating_add(1)) - } - - /// Get the previous tick index (decrementing by 1) - pub fn prev(&self) -> Result { - Self::new(self.0.saturating_sub(1)) - } - - /// Add a value to this tick index with bounds checking - pub fn checked_add(&self, value: i32) -> Result { - Self::new(self.0.saturating_add(value)) - } - - /// Subtract a value from this tick index with bounds checking - pub fn checked_sub(&self, value: i32) -> Result { - Self::new(self.0.saturating_sub(value)) - } - - /// Add a value to this tick index, saturating at the bounds instead of overflowing - pub fn saturating_add(&self, value: i32) -> Self { - match self.checked_add(value) { - Ok(result) => result, - Err(_) => { - if value > 0 { - Self::MAX - } else { - Self::MIN - } - } - } - } - - /// Subtract a value from this tick index, saturating at the bounds instead of overflowing - pub fn saturating_sub(&self, value: i32) -> Self { - match self.checked_sub(value) { - Ok(result) => result, - Err(_) => { - if value > 0 { - Self::MIN - } else { - Self::MAX - } - } - } - } - - /// Divide the tick index by a value with bounds checking - #[allow(clippy::arithmetic_side_effects)] - pub fn checked_div(&self, value: i32) -> Result { - if value == 0 { - return Err(TickMathError::DivisionByZero); - } - Self::new(self.0.saturating_div(value)) - } - - /// Divide the tick index by a value, saturating at the bounds - pub fn saturating_div(&self, value: i32) -> Self { - if value == 0 { - return Self::MAX; // Return MAX for division by zero - } - match self.checked_div(value) { - Ok(result) => result, - Err(_) => { - if (self.0 < 0 && value > 0) || (self.0 > 0 && value < 0) { - Self::MIN - } else { - Self::MAX - } - } - } - } - - /// Multiply the tick index by a value with bounds checking - pub fn checked_mul(&self, value: i32) -> Result { - // Check for potential overflow - match self.0.checked_mul(value) { - Some(result) => Self::new(result), - None => Err(TickMathError::Overflow), - } - } - - /// Multiply the tick index by a value, saturating at the bounds - pub fn saturating_mul(&self, value: i32) -> Self { - match self.checked_mul(value) { - Ok(result) => result, - Err(_) => { - if (self.0 < 0 && value > 0) || (self.0 > 0 && value < 0) { - Self::MIN - } else { - Self::MAX - } - } - } - } - - /// Converts tick index into SQRT of lower price of this tick In order to find the higher price - /// of this tick, call tick_index_to_sqrt_price(tick_idx + 1) - pub fn try_to_sqrt_price(&self) -> Result { - // because of u256->u128 conversion we have twice less values for min/max ticks - if !(Self::MIN..=Self::MAX).contains(self) { - return Err(TickMathError::TickOutOfBounds); - } - get_sqrt_ratio_at_tick(self.0).and_then(u256_q64_96_to_u64f64) - } - - /// Converts SQRT price to tick index - /// Because the tick is the range of prices [sqrt_lower_price, sqrt_higher_price), the resulting - /// tick index matches the price by the following inequality: - /// sqrt_lower_price <= sqrt_price < sqrt_higher_price - pub fn try_from_sqrt_price(sqrt_price: SqrtPrice) -> Result { - // price in the native Q64.96 integer format - let price_x96 = u64f64_to_u256_q64_96(sqrt_price); - - // first‑pass estimate from the log calculation - let mut tick = get_tick_at_sqrt_ratio(price_x96)?; - - // post‑verification, *both* directions - let price_at_tick = get_sqrt_ratio_at_tick(tick)?; - if price_at_tick > price_x96 { - tick = tick.saturating_sub(1); // estimate was too high - } else { - // it may still be one too low - let price_at_tick_plus = get_sqrt_ratio_at_tick(tick.saturating_add(1))?; - if price_at_tick_plus <= price_x96 { - tick = tick.saturating_add(1); // step up when required - } - } - - tick.into_tick_index() - } -} - -pub struct ActiveTickIndexManager(PhantomData); - -impl ActiveTickIndexManager { - pub fn insert(netuid: NetUid, index: TickIndex) { - // Check the range - if (index < TickIndex::MIN) || (index > TickIndex::MAX) { - return; - } - - // Convert to bitmap representation - let bitmap = TickIndexBitmap::from(index); - - // Update layer words - let mut word0_value = TickIndexBitmapWords::::get(( - netuid, - LayerLevel::Top, - bitmap.word_at(LayerLevel::Top), - )); - let mut word1_value = TickIndexBitmapWords::::get(( - netuid, - LayerLevel::Middle, - bitmap.word_at(LayerLevel::Middle), - )); - let mut word2_value = TickIndexBitmapWords::::get(( - netuid, - LayerLevel::Bottom, - bitmap.word_at(LayerLevel::Bottom), - )); - - // Set bits in each layer - word0_value |= bitmap.bit_mask(LayerLevel::Top); - word1_value |= bitmap.bit_mask(LayerLevel::Middle); - word2_value |= bitmap.bit_mask(LayerLevel::Bottom); - - // Update the storage - TickIndexBitmapWords::::set( - (netuid, LayerLevel::Top, bitmap.word_at(LayerLevel::Top)), - word0_value, - ); - TickIndexBitmapWords::::set( - ( - netuid, - LayerLevel::Middle, - bitmap.word_at(LayerLevel::Middle), - ), - word1_value, - ); - TickIndexBitmapWords::::set( - ( - netuid, - LayerLevel::Bottom, - bitmap.word_at(LayerLevel::Bottom), - ), - word2_value, - ); - } - - pub fn remove(netuid: NetUid, index: TickIndex) { - // Check the range - if (index < TickIndex::MIN) || (index > TickIndex::MAX) { - return; - } - - // Convert to bitmap representation - let bitmap = TickIndexBitmap::from(index); - - // Update layer words - let mut word0_value = TickIndexBitmapWords::::get(( - netuid, - LayerLevel::Top, - bitmap.word_at(LayerLevel::Top), - )); - let mut word1_value = TickIndexBitmapWords::::get(( - netuid, - LayerLevel::Middle, - bitmap.word_at(LayerLevel::Middle), - )); - let mut word2_value = TickIndexBitmapWords::::get(( - netuid, - LayerLevel::Bottom, - bitmap.word_at(LayerLevel::Bottom), - )); - - // Turn the bit off (& !bit) and save as needed - word2_value &= !bitmap.bit_mask(LayerLevel::Bottom); - TickIndexBitmapWords::::set( - ( - netuid, - LayerLevel::Bottom, - bitmap.word_at(LayerLevel::Bottom), - ), - word2_value, - ); - - if word2_value == 0 { - word1_value &= !bitmap.bit_mask(LayerLevel::Middle); - TickIndexBitmapWords::::set( - ( - netuid, - LayerLevel::Middle, - bitmap.word_at(LayerLevel::Middle), - ), - word1_value, - ); - } - - if word1_value == 0 { - word0_value &= !bitmap.bit_mask(LayerLevel::Top); - TickIndexBitmapWords::::set( - (netuid, LayerLevel::Top, bitmap.word_at(LayerLevel::Top)), - word0_value, - ); - } - } - - pub fn find_closest_lower(netuid: NetUid, index: TickIndex) -> Option { - Self::find_closest(netuid, index, true) - } - - pub fn find_closest_higher(netuid: NetUid, index: TickIndex) -> Option { - Self::find_closest(netuid, index, false) - } - - fn find_closest(netuid: NetUid, index: TickIndex, lower: bool) -> Option { - // Check the range - if (index < TickIndex::MIN) || (index > TickIndex::MAX) { - return None; - } - - // Convert to bitmap representation - let bitmap = TickIndexBitmap::from(index); - let mut found = false; - let mut result: u32 = 0; - - // Layer positions from bitmap - let layer0_word = bitmap.word_at(LayerLevel::Top); - let layer0_bit = bitmap.bit_at(LayerLevel::Top); - let layer1_word = bitmap.word_at(LayerLevel::Middle); - let layer1_bit = bitmap.bit_at(LayerLevel::Middle); - let layer2_word = bitmap.word_at(LayerLevel::Bottom); - let layer2_bit = bitmap.bit_at(LayerLevel::Bottom); - - // Find the closest active bits in layer 0, then 1, then 2 - - /////////////// - // Level 0 - let word0 = TickIndexBitmapWords::::get((netuid, LayerLevel::Top, layer0_word)); - let closest_bits_l0 = - TickIndexBitmap::find_closest_active_bit_candidates(word0, layer0_bit, lower); - - for closest_bit_l0 in closest_bits_l0.iter() { - /////////////// - // Level 1 - let word1_index = TickIndexBitmap::layer_to_index(BitmapLayer::new(0, *closest_bit_l0)); - - // Layer 1 words are different, shift the bit to the word edge - let start_from_l1_bit = match word1_index.cmp(&layer1_word) { - Ordering::Less => 127, - Ordering::Greater => 0, - _ => layer1_bit, - }; - let word1_value = - TickIndexBitmapWords::::get((netuid, LayerLevel::Middle, word1_index)); - let closest_bits_l1 = TickIndexBitmap::find_closest_active_bit_candidates( - word1_value, - start_from_l1_bit, - lower, - ); - - for closest_bit_l1 in closest_bits_l1.iter() { - /////////////// - // Level 2 - let word2_index = - TickIndexBitmap::layer_to_index(BitmapLayer::new(word1_index, *closest_bit_l1)); - - // Layer 2 words are different, shift the bit to the word edge - let start_from_l2_bit = match word2_index.cmp(&layer2_word) { - Ordering::Less => 127, - Ordering::Greater => 0, - _ => layer2_bit, - }; - - let word2_value = - TickIndexBitmapWords::::get((netuid, LayerLevel::Bottom, word2_index)); - - let closest_bits_l2 = TickIndexBitmap::find_closest_active_bit_candidates( - word2_value, - start_from_l2_bit, - lower, - ); - - if !closest_bits_l2.is_empty() { - // The active tick is found, restore its full index and return - let offset_found_index = TickIndexBitmap::layer_to_index(BitmapLayer::new( - word2_index, - // it's safe to unwrap, because the len is > 0, but to prevent errors in - // refactoring, we use default fallback here for extra safety - closest_bits_l2.first().copied().unwrap_or_default(), - )); - - if lower { - if (offset_found_index > result) || (!found) { - result = offset_found_index; - found = true; - } - } else if (offset_found_index < result) || (!found) { - result = offset_found_index; - found = true; - } - } - } - } - - if !found { - return None; - } - - // Convert the result offset_index back to a tick index - TickIndex::from_offset_index(result).ok() - } - - pub fn tick_is_active(netuid: NetUid, tick: TickIndex) -> bool { - Self::find_closest_lower(netuid, tick).unwrap_or(TickIndex::MAX) == tick - } -} - -/// Represents the three layers in the Uniswap V3 bitmap structure -#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, TypeInfo, MaxEncodedLen)] -pub enum LayerLevel { - /// Top layer (highest level of the hierarchy) - Top = 0, - /// Middle layer - Middle = 1, - /// Bottom layer (contains the actual ticks) - Bottom = 2, -} - -#[freeze_struct("4015a04919eb5e2e")] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, TypeInfo, MaxEncodedLen)] -pub(crate) struct BitmapLayer { - word: u32, - bit: u32, -} - -impl BitmapLayer { - pub fn new(word: u32, bit: u32) -> Self { - Self { word, bit } - } -} - -/// A bitmap representation of a tick index position across the three-layer structure -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct TickIndexBitmap { - /// The position in layer 0 (top layer) - layer0: BitmapLayer, - /// The position in layer 1 (middle layer) - layer1: BitmapLayer, - /// The position in layer 2 (bottom layer) - layer2: BitmapLayer, -} - -impl TickIndexBitmap { - /// Helper function to convert a bitmap index to a (word, bit) tuple in a bitmap layer using - /// safe methods - /// - /// Note: This function operates on bitmap navigation indices, NOT tick indices. - /// It converts a flat index within the bitmap structure to a (word, bit) position. - fn index_to_layer(index: u32) -> BitmapLayer { - let word = index.safe_div(128); - let bit = index.checked_rem(128).unwrap_or_default(); - BitmapLayer { word, bit } - } - - /// Converts a position (word, bit) within a layer to a word index in the next layer down - /// Note: This returns a bitmap navigation index, NOT a tick index - pub(crate) fn layer_to_index(layer: BitmapLayer) -> u32 { - layer.word.saturating_mul(128).saturating_add(layer.bit) - } - - /// Get the mask for a bit in the specified layer - pub(crate) fn bit_mask(&self, layer: LayerLevel) -> u128 { - match layer { - LayerLevel::Top => 1u128 << self.layer0.bit, - LayerLevel::Middle => 1u128 << self.layer1.bit, - LayerLevel::Bottom => 1u128 << self.layer2.bit, - } - } - - /// Get the word for the specified layer - pub(crate) fn word_at(&self, layer: LayerLevel) -> u32 { - match layer { - LayerLevel::Top => self.layer0.word, - LayerLevel::Middle => self.layer1.word, - LayerLevel::Bottom => self.layer2.word, - } - } - - /// Get the bit for the specified layer - pub(crate) fn bit_at(&self, layer: LayerLevel) -> u32 { - match layer { - LayerLevel::Top => self.layer0.bit, - LayerLevel::Middle => self.layer1.bit, - LayerLevel::Bottom => self.layer2.bit, - } - } - - /// Finds the closest active bit in a bitmap word, and if the active bit exactly matches the - /// requested bit, then it finds the next one as well - /// - /// # Arguments - /// * `word` - The bitmap word to search within - /// * `bit` - The bit position to start searching from - /// * `lower` - If true, search for lower bits (decreasing bit position), if false, search for - /// higher bits (increasing bit position) - /// - /// # Returns - /// * Exact match: Vec with [next_bit, bit] - /// * Non-exact match: Vec with [closest_bit] - /// * No match: Empty Vec - pub(crate) fn find_closest_active_bit_candidates( - word: u128, - bit: u32, - lower: bool, - ) -> Vec { - let mut result = vec![]; - let mut mask: u128 = 1_u128.wrapping_shl(bit); - let mut active_bit: u32 = bit; - - while mask > 0 { - if mask & word != 0 { - result.push(active_bit); - if active_bit != bit { - break; - } - } - - mask = if lower { - active_bit = active_bit.saturating_sub(1); - mask.wrapping_shr(1) - } else { - active_bit = active_bit.saturating_add(1); - mask.wrapping_shl(1) - }; - } - - result - } -} - -impl From for TickIndexBitmap { - fn from(tick_index: TickIndex) -> Self { - // Convert to offset index (internal operation only) - let offset_index = (tick_index.get().saturating_add(TickIndex::OFFSET.get())) as u32; - - // Calculate layer positions - let layer2 = Self::index_to_layer(offset_index); - let layer1 = Self::index_to_layer(layer2.word); - let layer0 = Self::index_to_layer(layer1.word); - - Self { - layer0, - layer1, - layer2, - } - } -} - -#[allow(clippy::arithmetic_side_effects)] -fn get_sqrt_ratio_at_tick(tick: i32) -> Result { - let abs_tick = if tick < 0 { - U256::from(tick.neg()) - } else { - U256::from(tick) - }; - - if abs_tick > U256_MAX_TICK { - return Err(TickMathError::TickOutOfBounds); - } - - let mut ratio = if abs_tick & (U256_1) != U256::ZERO { - U256::from_limbs([12262481743371124737, 18445821805675392311, 0, 0]) - } else { - U256::from_limbs([0, 0, 1, 0]) - }; - - if !(abs_tick & U256_2).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 6459403834229662010, - 18444899583751176498, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_4).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 17226890335427755468, - 18443055278223354162, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_8).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 2032852871939366096, - 18439367220385604838, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_16).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 14545316742740207172, - 18431993317065449817, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_32).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 5129152022828963008, - 18417254355718160513, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_64).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 4894419605888772193, - 18387811781193591352, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_128).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 1280255884321894483, - 18329067761203520168, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_256).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 15924666964335305636, - 18212142134806087854, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_512).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 8010504389359918676, - 17980523815641551639, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_1024).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 10668036004952895731, - 17526086738831147013, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_2048).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 4878133418470705625, - 16651378430235024244, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_4096).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 9537173718739605541, - 15030750278693429944, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_8192).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 9972618978014552549, - 12247334978882834399, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_16384).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 10428997489610666743, - 8131365268884726200, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_32768).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 9305304367709015974, - 3584323654723342297, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_65536).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 14301143598189091785, - 696457651847595233, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_131072).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 7393154844743099908, - 26294789957452057, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_262144).is_zero() { - ratio = (ratio.saturating_mul(U256::from_limbs([ - 2209338891292245656, - 37481735321082, - 0, - 0, - ]))) >> 128 - } - if !(abs_tick & U256_524288).is_zero() { - ratio = - (ratio.saturating_mul(U256::from_limbs([10518117631919034274, 76158723, 0, 0]))) >> 128 - } - - if tick > 0 { - ratio = U256::MAX / ratio; - } - - let shifted: U256 = ratio >> 32; - let ceil = if ratio & U256::from((1u128 << 32) - 1) != U256::ZERO { - shifted.saturating_add(U256_1) - } else { - shifted - }; - Ok(ceil) -} - -#[allow(clippy::arithmetic_side_effects)] -fn get_tick_at_sqrt_ratio(sqrt_price_x_96: U256) -> Result { - if !(sqrt_price_x_96 >= MIN_SQRT_RATIO && sqrt_price_x_96 < MAX_SQRT_RATIO) { - return Err(TickMathError::SqrtPriceOutOfBounds); - } - - let ratio: U256 = sqrt_price_x_96.shl(32); - let mut r = ratio; - let mut msb = U256::ZERO; - - let mut f = if r > U256::from_limbs([18446744073709551615, 18446744073709551615, 0, 0]) { - U256_1.shl(U256_7) - } else { - U256::ZERO - }; - msb = msb.bitor(f); - r = r.shr(f); - - f = if r > U256::from_limbs([18446744073709551615, 0, 0, 0]) { - U256_1.shl(U256_6) - } else { - U256::ZERO - }; - msb = msb.bitor(f); - r = r.shr(f); - - f = if r > U256::from_limbs([4294967295, 0, 0, 0]) { - U256_1.shl(U256_5) - } else { - U256::ZERO - }; - msb = msb.bitor(f); - r = r.shr(f); - - f = if r > U256::from_limbs([65535, 0, 0, 0]) { - U256_1.shl(U256_4) - } else { - U256::ZERO - }; - msb = msb.bitor(f); - r = r.shr(f); - - f = if r > U256_255 { - U256_1.shl(U256_3) - } else { - U256::ZERO - }; - msb = msb.bitor(f); - r = r.shr(f); - - f = if r > U256_15 { - U256_1.shl(U256_2) - } else { - U256::ZERO - }; - msb = msb.bitor(f); - r = r.shr(f); - - f = if r > U256_3 { - U256_1.shl(U256_1) - } else { - U256::ZERO - }; - msb = msb.bitor(f); - r = r.shr(f); - - f = if r > U256_1 { U256_1 } else { U256::ZERO }; - - msb = msb.bitor(f); - - r = if msb >= U256_128 { - ratio.shr(msb.saturating_sub(U256_127)) - } else { - ratio.shl(U256_127.saturating_sub(msb)) - }; - - let mut log_2: I256 = - (I256::from_raw(msb).saturating_sub(I256::from_limbs([128, 0, 0, 0]))).shl(64); - - for i in (51..=63).rev() { - r = r.overflowing_mul(r).0.shr(U256_127); - let f: U256 = r.shr(128); - log_2 = log_2.bitor(I256::from_raw(f.shl(i))); - - r = r.shr(f); - } - - r = r.overflowing_mul(r).0.shr(U256_127); - let f: U256 = r.shr(128); - log_2 = log_2.bitor(I256::from_raw(f.shl(50))); - - let log_sqrt10001 = log_2.wrapping_mul(SQRT_10001); - - let tick_low = (log_sqrt10001.saturating_sub(TICK_LOW) >> 128_u8).low_i32(); - - let tick_high = (log_sqrt10001.saturating_add(TICK_HIGH) >> 128_u8).low_i32(); - - let tick = if tick_low == tick_high { - tick_low - } else if get_sqrt_ratio_at_tick(tick_high)? <= sqrt_price_x_96 { - tick_high - } else { - tick_low - }; - - Ok(tick) -} - -// Convert U64F64 to U256 in Q64.96 format (Uniswap's sqrt price format) -fn u64f64_to_u256_q64_96(value: U64F64) -> U256 { - u64f64_to_u256(value, 96) -} - -/// Convert U64F64 to U256 -/// -/// # Arguments -/// * `value` - The U64F64 value to convert -/// * `target_fractional_bits` - Number of fractional bits in the target U256 format -/// -/// # Returns -/// * `U256` - Converted value -#[allow(clippy::arithmetic_side_effects)] -fn u64f64_to_u256(value: U64F64, target_fractional_bits: u32) -> U256 { - let raw = U256::from(value.to_bits()); - - match target_fractional_bits.cmp(&64) { - Ordering::Less => raw >> (64 - target_fractional_bits), - Ordering::Greater => raw.saturating_shl((target_fractional_bits - 64) as usize), - Ordering::Equal => raw, - } -} - -/// Convert U256 in Q64.96 format (Uniswap's sqrt price format) to U64F64 -fn u256_q64_96_to_u64f64(value: U256) -> Result { - q_to_u64f64(value, 96) -} - -#[allow(clippy::arithmetic_side_effects)] -fn q_to_u64f64(x: U256, frac_bits: u32) -> Result { - let diff = frac_bits.saturating_sub(64) as usize; - - // 1. shift right diff bits - let shifted = if diff != 0 { x >> diff } else { x }; - - // 2. **round up** if we threw away any 1‑bits - let mask = if diff != 0 { - (U256_1.saturating_shl(diff)).saturating_sub(U256_1) - } else { - U256::ZERO - }; - let rounded = if diff != 0 && (x & mask) != U256::ZERO { - shifted.saturating_add(U256_1) - } else { - shifted - }; - - // 3. check that it fits in 128 bits and transmute - if (rounded >> 128) != U256::ZERO { - return Err(TickMathError::Overflow); - } - Ok(U64F64::from_bits(rounded.to::())) -} - -#[derive(Debug, PartialEq, Eq)] -pub enum TickMathError { - TickOutOfBounds, - SqrtPriceOutOfBounds, - ConversionError, - Overflow, - DivisionByZero, -} - -impl fmt::Display for TickMathError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::TickOutOfBounds => f.write_str("The given tick is outside of the minimum/maximum values."), - Self::SqrtPriceOutOfBounds =>f.write_str("Second inequality must be < because the price can never reach the price at the max tick"), - Self::ConversionError => f.write_str("Error converting from one number type into another"), - Self::Overflow => f.write_str("Number overflow in arithmetic operation"), - Self::DivisionByZero => f.write_str("Division by zero is not allowed") - } - } -} - -impl Error for TickMathError {} - -#[allow(clippy::unwrap_used)] -#[cfg(test)] -mod tests { - use safe_math::FixedExt; - use std::{ops::Sub, str::FromStr}; - - use super::*; - use crate::mock::*; - - #[test] - fn test_get_sqrt_ratio_at_tick_bounds() { - // the function should return an error if the tick is out of bounds - if let Err(err) = get_sqrt_ratio_at_tick(MIN_TICK - 1) { - assert!(matches!(err, TickMathError::TickOutOfBounds)); - } else { - panic!("get_qrt_ratio_at_tick did not respect lower tick bound") - } - if let Err(err) = get_sqrt_ratio_at_tick(MAX_TICK + 1) { - assert!(matches!(err, TickMathError::TickOutOfBounds)); - } else { - panic!("get_qrt_ratio_at_tick did not respect upper tick bound") - } - } - - #[test] - fn test_get_sqrt_ratio_at_tick_values() { - // test individual values for correct results - assert_eq!( - get_sqrt_ratio_at_tick(MIN_TICK).unwrap(), - U256::from(4295128739u64), - "sqrt ratio at min incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(MIN_TICK + 1).unwrap(), - U256::from(4295343490u64), - "sqrt ratio at min + 1 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(MAX_TICK - 1).unwrap(), - U256::from_str("1461373636630004318706518188784493106690254656249").unwrap(), - "sqrt ratio at max - 1 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(MAX_TICK).unwrap(), - U256::from_str("1461446703485210103287273052203988822378723970342").unwrap(), - "sqrt ratio at max incorrect" - ); - // checking hard coded values against solidity results - assert_eq!( - get_sqrt_ratio_at_tick(50).unwrap(), - U256::from(79426470787362580746886972461u128), - "sqrt ratio at 50 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(100).unwrap(), - U256::from(79625275426524748796330556128u128), - "sqrt ratio at 100 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(250).unwrap(), - U256::from(80224679980005306637834519095u128), - "sqrt ratio at 250 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(500).unwrap(), - U256::from(81233731461783161732293370115u128), - "sqrt ratio at 500 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(1000).unwrap(), - U256::from(83290069058676223003182343270u128), - "sqrt ratio at 1000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(2500).unwrap(), - U256::from(89776708723587163891445672585u128), - "sqrt ratio at 2500 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(3000).unwrap(), - U256::from(92049301871182272007977902845u128), - "sqrt ratio at 3000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(4000).unwrap(), - U256::from(96768528593268422080558758223u128), - "sqrt ratio at 4000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(5000).unwrap(), - U256::from(101729702841318637793976746270u128), - "sqrt ratio at 5000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(50000).unwrap(), - U256::from(965075977353221155028623082916u128), - "sqrt ratio at 50000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(150000).unwrap(), - U256::from(143194173941309278083010301478497u128), - "sqrt ratio at 150000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(250000).unwrap(), - U256::from(21246587762933397357449903968194344u128), - "sqrt ratio at 250000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(500000).unwrap(), - U256::from_str("5697689776495288729098254600827762987878").unwrap(), - "sqrt ratio at 500000 incorrect" - ); - assert_eq!( - get_sqrt_ratio_at_tick(738203).unwrap(), - U256::from_str("847134979253254120489401328389043031315994541").unwrap(), - "sqrt ratio at 738203 incorrect" - ); - } - - #[test] - fn test_get_tick_at_sqrt_ratio() { - //throws for too low - let result = get_tick_at_sqrt_ratio(MIN_SQRT_RATIO.sub(U256_1)); - assert_eq!( - result.unwrap_err().to_string(), - "Second inequality must be < because the price can never reach the price at the max tick" - ); - - //throws for too high - let result = get_tick_at_sqrt_ratio(MAX_SQRT_RATIO); - assert_eq!( - result.unwrap_err().to_string(), - "Second inequality must be < because the price can never reach the price at the max tick" - ); - - //ratio of min tick - let result = get_tick_at_sqrt_ratio(MIN_SQRT_RATIO).unwrap(); - assert_eq!(result, MIN_TICK); - - //ratio of min tick + 1 - let result = get_tick_at_sqrt_ratio(U256::from_str("4295343490").unwrap()).unwrap(); - assert_eq!(result, MIN_TICK + 1); - } - - #[test] - fn test_roundtrip() { - for tick_index in [ - MIN_TICK + 1, // we can't use extremes because of rounding during roundtrip conversion - -1000, - -100, - -10, - -4, - -2, - 0, - 2, - 4, - 10, - 100, - 1000, - MAX_TICK - 1, - ] - .iter() - { - let sqrt_price = get_sqrt_ratio_at_tick(*tick_index).unwrap(); - let round_trip_tick_index = get_tick_at_sqrt_ratio(sqrt_price).unwrap(); - assert_eq!(round_trip_tick_index, *tick_index); - } - } - - #[test] - fn test_u256_to_u64f64_q64_96() { - // Test tick 0 (sqrt price = 1.0 * 2^96) - let tick0_sqrt_price = U256::from(1u128 << 96); - let fixed_price = u256_q64_96_to_u64f64(tick0_sqrt_price).unwrap(); - - // Should be 1.0 in U64F64 - assert_eq!(fixed_price, U64F64::from_num(1.0)); - - // Round trip back to U256 Q64.96 - let back_to_u256 = u64f64_to_u256_q64_96(fixed_price); - assert_eq!(back_to_u256, tick0_sqrt_price); - } - - #[test] - fn test_tick_index_to_sqrt_price() { - let tick_spacing = SqrtPrice::from_num(1.0001); - - // check tick bounds - assert_eq!( - TickIndex(MIN_TICK).try_to_sqrt_price(), - Err(TickMathError::TickOutOfBounds) - ); - - assert_eq!( - TickIndex(MAX_TICK).try_to_sqrt_price(), - Err(TickMathError::TickOutOfBounds), - ); - - assert!( - TickIndex::MAX.try_to_sqrt_price().unwrap().abs_diff( - TickIndex::new_unchecked(TickIndex::MAX.get() + 1).as_sqrt_price_bounded() - ) < SqrtPrice::from_num(1e-6) - ); - - assert!( - TickIndex::MIN.try_to_sqrt_price().unwrap().abs_diff( - TickIndex::new_unchecked(TickIndex::MIN.get() - 1).as_sqrt_price_bounded() - ) < SqrtPrice::from_num(1e-6) - ); - - // At tick index 0, the sqrt price should be 1.0 - let sqrt_price = TickIndex(0).try_to_sqrt_price().unwrap(); - assert_eq!(sqrt_price, SqrtPrice::from_num(1.0)); - - let sqrt_price = TickIndex(2).try_to_sqrt_price().unwrap(); - assert!(sqrt_price.abs_diff(tick_spacing) < SqrtPrice::from_num(1e-10)); - - let sqrt_price = TickIndex(4).try_to_sqrt_price().unwrap(); - // Calculate the expected value: (1 + TICK_SPACING/1e9 + 1.0)^2 - let expected = tick_spacing * tick_spacing; - assert!(sqrt_price.abs_diff(expected) < SqrtPrice::from_num(1e-10)); - - // Test with tick index 10 - let sqrt_price = TickIndex(10).try_to_sqrt_price().unwrap(); - // Calculate the expected value: (1 + TICK_SPACING/1e9 + 1.0)^5 - let expected = tick_spacing.checked_pow(5).unwrap(); - assert!( - sqrt_price.abs_diff(expected) < SqrtPrice::from_num(1e-10), - "diff: {}", - sqrt_price.abs_diff(expected), - ); - } - - #[test] - fn test_sqrt_price_to_tick_index() { - let tick_spacing = SqrtPrice::from_num(1.0001); - let tick_index = TickIndex::try_from_sqrt_price(SqrtPrice::from_num(1.0)).unwrap(); - assert_eq!(tick_index, TickIndex::new_unchecked(0)); - - // Test with sqrt price equal to tick_spacing_tao (should be tick index 2) - let epsilon = SqrtPrice::from_num(0.0000000000000001); - assert!( - TickIndex::new_unchecked(2) - .as_sqrt_price_bounded() - .abs_diff(tick_spacing) - < epsilon - ); - - // Test with sqrt price equal to tick_spacing_tao^2 (should be tick index 4) - let sqrt_price = tick_spacing * tick_spacing; - assert!( - TickIndex::new_unchecked(4) - .as_sqrt_price_bounded() - .abs_diff(sqrt_price) - < epsilon - ); - - // Test with sqrt price equal to tick_spacing_tao^5 (should be tick index 10) - let sqrt_price = tick_spacing.checked_pow(5).unwrap(); - assert!( - TickIndex::new_unchecked(10) - .as_sqrt_price_bounded() - .abs_diff(sqrt_price) - < epsilon - ); - } - - #[test] - fn test_roundtrip_tick_index_sqrt_price() { - for i32_value in [ - TickIndex::MIN.get(), - -1000, - -100, - -10, - -4, - -2, - 0, - 2, - 4, - 10, - 100, - 1000, - TickIndex::MAX.get(), - ] - .into_iter() - { - let tick_index = TickIndex::new_unchecked(i32_value); - let sqrt_price = tick_index.try_to_sqrt_price().unwrap(); - let round_trip_tick_index = TickIndex::try_from_sqrt_price(sqrt_price).unwrap(); - assert_eq!(round_trip_tick_index, tick_index); - } - } - - #[test] - fn test_from_offset_index() { - // Test various tick indices - for i32_value in [ - TickIndex::MIN.get(), - -1000, - -100, - -10, - 0, - 10, - 100, - 1000, - TickIndex::MAX.get(), - ] { - let original_tick = TickIndex::new_unchecked(i32_value); - - // Calculate the offset index (adding OFFSET) - let offset_index = (i32_value + TickIndex::OFFSET.get()) as u32; - - // Convert back from offset index to tick index - let roundtrip_tick = TickIndex::from_offset_index(offset_index).unwrap(); - - // Check that we get the same tick index back - assert_eq!(original_tick, roundtrip_tick); - } - - // Test out of bounds values - let too_large = (TickIndex::MAX.get() + TickIndex::OFFSET.get() + 1) as u32; - assert!(TickIndex::from_offset_index(too_large).is_err()); - } - - #[test] - fn test_tick_price_sanity_check() { - let min_price = TickIndex::MIN.try_to_sqrt_price().unwrap(); - let max_price = TickIndex::MAX.try_to_sqrt_price().unwrap(); - - assert!(min_price > 0.); - assert!(max_price > 0.); - assert!(max_price > min_price); - assert!(min_price < 0.000001); - assert!(max_price > 10.); - - // Roundtrip conversions - let min_price_sqrt = TickIndex::MIN.try_to_sqrt_price().unwrap(); - let min_tick = TickIndex::try_from_sqrt_price(min_price_sqrt).unwrap(); - assert_eq!(min_tick, TickIndex::MIN); - - let max_price_sqrt: SqrtPrice = TickIndex::MAX.try_to_sqrt_price().unwrap(); - let max_tick = TickIndex::try_from_sqrt_price(max_price_sqrt).unwrap(); - assert_eq!(max_tick, TickIndex::MAX); - } - - #[test] - fn test_to_sqrt_price_bounded() { - assert_eq!( - TickIndex::MAX.as_sqrt_price_bounded(), - TickIndex::MAX.try_to_sqrt_price().unwrap() - ); - - assert_eq!( - TickIndex::MIN.as_sqrt_price_bounded(), - TickIndex::MIN.try_to_sqrt_price().unwrap() - ); - } - - mod active_tick_index_manager { - - use super::*; - - #[test] - fn test_tick_search_basic() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - ActiveTickIndexManager::::insert(netuid, TickIndex::MIN); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, TickIndex::MIN) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, TickIndex::MAX) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MAX.saturating_div(2) - ) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MAX.prev().unwrap() - ) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MIN.next().unwrap() - ) - .unwrap(), - TickIndex::MIN - ); - - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, TickIndex::MIN) - .unwrap(), - TickIndex::MIN - ); - assert!( - ActiveTickIndexManager::::find_closest_higher(netuid, TickIndex::MAX) - .is_none() - ); - assert!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MAX.saturating_div(2) - ) - .is_none() - ); - assert!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MAX.prev().unwrap() - ) - .is_none() - ); - assert!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MIN.next().unwrap() - ) - .is_none() - ); - - ActiveTickIndexManager::::insert(netuid, TickIndex::MAX); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, TickIndex::MIN) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, TickIndex::MAX) - .unwrap(), - TickIndex::MAX - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MAX.saturating_div(2) - ) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MAX.prev().unwrap() - ) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MIN.next().unwrap() - ) - .unwrap(), - TickIndex::MIN - ); - }); - } - - #[test] - fn test_tick_search_sparse_queries() { - new_test_ext().execute_with(|| { - let active_index = TickIndex::MIN.saturating_add(10); - let netuid = NetUid::from(1); - - ActiveTickIndexManager::::insert(netuid, active_index); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, active_index) - .unwrap(), - active_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MIN.saturating_add(11) - ) - .unwrap(), - active_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MIN.saturating_add(12) - ) - .unwrap(), - active_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, TickIndex::MIN), - None - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MIN.saturating_add(9) - ), - None - ); - - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, active_index) - .unwrap(), - active_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MIN.saturating_add(11) - ), - None - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MIN.saturating_add(12) - ), - None - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, TickIndex::MIN) - .unwrap(), - active_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MIN.saturating_add(9) - ) - .unwrap(), - active_index - ); - }); - } - - #[test] - fn test_tick_search_many_lows() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - (0..1000).for_each(|i| { - ActiveTickIndexManager::::insert( - netuid, - TickIndex::MIN.saturating_add(i), - ); - }); - - for i in 0..1000 { - let test_index = TickIndex::MIN.saturating_add(i); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, test_index) - .unwrap(), - test_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, test_index) - .unwrap(), - test_index - ); - } - }); - } - - #[test] - fn test_tick_search_many_sparse() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let count = 1000; - - for i in 0..=count { - ActiveTickIndexManager::::insert( - netuid, - TickIndex::new_unchecked(i * 10), - ); - } - - for i in 1..count { - let tick = TickIndex::new_unchecked(i * 10); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, tick).unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, tick).unwrap(), - tick - ); - for j in 1..=9 { - let before_tick = TickIndex::new_unchecked(i * 10 - j); - let after_tick = TickIndex::new_unchecked(i * 10 + j); - let prev_tick = TickIndex::new_unchecked((i - 1) * 10); - let next_tick = TickIndex::new_unchecked((i + 1) * 10); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, before_tick) - .unwrap(), - prev_tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, after_tick) - .unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - before_tick - ) - .unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, after_tick) - .unwrap(), - next_tick - ); - } - } - }); - } - - #[test] - fn test_tick_search_many_lows_sparse_reversed() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let count = 1000; - - for i in (0..=count).rev() { - ActiveTickIndexManager::::insert( - netuid, - TickIndex::new_unchecked(i * 10), - ); - } - - for i in 1..count { - let tick = TickIndex::new_unchecked(i * 10); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, tick).unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, tick).unwrap(), - tick - ); - for j in 1..=9 { - let before_tick = TickIndex::new_unchecked(i * 10 - j); - let after_tick = TickIndex::new_unchecked(i * 10 + j); - let prev_tick = TickIndex::new_unchecked((i - 1) * 10); - let next_tick = TickIndex::new_unchecked((i + 1) * 10); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, before_tick) - .unwrap(), - prev_tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, after_tick) - .unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - before_tick - ) - .unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, after_tick) - .unwrap(), - next_tick - ); - } - } - }); - } - - #[test] - fn test_tick_search_repeated_insertions() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let count = 1000; - - for _ in 0..10 { - for i in 0..=count { - let tick = TickIndex::new_unchecked(i * 10); - ActiveTickIndexManager::::insert(netuid, tick); - } - - for i in 1..count { - let tick = TickIndex::new_unchecked(i * 10); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, tick) - .unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, tick) - .unwrap(), - tick - ); - for j in 1..=9 { - let before_tick = TickIndex::new_unchecked(i * 10 - j); - let after_tick = TickIndex::new_unchecked(i * 10 + j); - let prev_tick = TickIndex::new_unchecked((i - 1) * 10); - let next_tick = TickIndex::new_unchecked((i + 1) * 10); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - before_tick - ) - .unwrap(), - prev_tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, after_tick - ) - .unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - before_tick - ) - .unwrap(), - tick - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, after_tick - ) - .unwrap(), - next_tick - ); - } - } - } - }); - } - - #[test] - fn test_tick_search_full_range() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let step = 1019; - // Get the full valid tick range by subtracting MIN from MAX - let count = (TickIndex::MAX.get() - TickIndex::MIN.get()) / step; - - for i in 0..=count { - let index = TickIndex::MIN.saturating_add(i * step); - ActiveTickIndexManager::::insert(netuid, index); - } - for i in 1..count { - let index = TickIndex::MIN.saturating_add(i * step); - - let prev_index = TickIndex::new_unchecked(index.get() - step); - let next_minus_one = TickIndex::new_unchecked(index.get() + step - 1); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, prev_index) - .unwrap(), - prev_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, index).unwrap(), - index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, next_minus_one) - .unwrap(), - index - ); - - let mid_next = TickIndex::new_unchecked(index.get() + step / 2); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, mid_next) - .unwrap(), - index - ); - - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, index).unwrap(), - index - ); - - let next_index = TickIndex::new_unchecked(index.get() + step); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, next_index) - .unwrap(), - next_index - ); - - let mid_next = TickIndex::new_unchecked(index.get() + step / 2); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, mid_next) - .unwrap(), - next_index - ); - - let next_minus_1 = TickIndex::new_unchecked(index.get() + step - 1); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, next_minus_1) - .unwrap(), - next_index - ); - for j in 1..=9 { - let before_index = TickIndex::new_unchecked(index.get() - j); - let after_index = TickIndex::new_unchecked(index.get() + j); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - before_index - ) - .unwrap(), - prev_index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, after_index) - .unwrap(), - index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - before_index - ) - .unwrap(), - index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - after_index - ) - .unwrap(), - next_index - ); - } - } - }); - } - - #[test] - fn test_tick_remove_basic() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - - ActiveTickIndexManager::::insert(netuid, TickIndex::MIN); - ActiveTickIndexManager::::insert(netuid, TickIndex::MAX); - ActiveTickIndexManager::::remove(netuid, TickIndex::MAX); - - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, TickIndex::MIN) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, TickIndex::MAX) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MAX.saturating_div(2) - ) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MAX.prev().unwrap() - ) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_lower( - netuid, - TickIndex::MIN.next().unwrap() - ) - .unwrap(), - TickIndex::MIN - ); - - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, TickIndex::MIN) - .unwrap(), - TickIndex::MIN - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, TickIndex::MAX), - None - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MAX.saturating_div(2) - ), - None - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MAX.prev().unwrap() - ), - None - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher( - netuid, - TickIndex::MIN.next().unwrap() - ), - None - ); - }); - } - - #[test] - fn test_tick_remove_full_range() { - new_test_ext().execute_with(|| { - let netuid = NetUid::from(1); - let step = 1019; - // Get the full valid tick range by subtracting MIN from MAX - let count = (TickIndex::MAX.get() - TickIndex::MIN.get()) / step; - let remove_frequency = 5; // Remove every 5th tick - - // Insert ticks - for i in 0..=count { - let index = TickIndex::MIN.saturating_add(i * step); - ActiveTickIndexManager::::insert(netuid, index); - } - - // Remove some ticks - for i in 1..count { - if i % remove_frequency == 0 { - let index = TickIndex::MIN.saturating_add(i * step); - ActiveTickIndexManager::::remove(netuid, index); - } - } - - // Verify - for i in 1..count { - let index = TickIndex::MIN.saturating_add(i * step); - - if i % remove_frequency == 0 { - let lower = - ActiveTickIndexManager::::find_closest_lower(netuid, index); - let higher = - ActiveTickIndexManager::::find_closest_higher(netuid, index); - assert!(lower != Some(index)); - assert!(higher != Some(index)); - } else { - assert_eq!( - ActiveTickIndexManager::::find_closest_lower(netuid, index) - .unwrap(), - index - ); - assert_eq!( - ActiveTickIndexManager::::find_closest_higher(netuid, index) - .unwrap(), - index - ); - } - } - }); - } - } -} diff --git a/pallets/swap/src/weights.rs b/pallets/swap/src/weights.rs index 70a87eff3d..508626f6ae 100644 --- a/pallets/swap/src/weights.rs +++ b/pallets/swap/src/weights.rs @@ -2,9 +2,9 @@ //! Autogenerated weights for `pallet_subtensor_swap` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 49.1.0 -//! DATE: 2026-04-03, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-06-11, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `runnervmrg6be`, CPU: `AMD EPYC 7763 64-Core Processor` +//! HOSTNAME: `runnervm3jyl0`, CPU: `AMD EPYC 9V74 80-Core Processor` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` // Executed Command: @@ -22,7 +22,7 @@ // --no-storage-info // --no-min-squares // --no-median-slopes -// --output=/tmp/tmp.xtxn9d9WXq +// --output=/tmp/tmp.BMe4BAnDcE // --template=/home/runner/work/subtensor/subtensor/.maintain/frame-weight-template.hbs #![cfg_attr(rustfmt, rustfmt_skip)] @@ -37,11 +37,6 @@ use core::marker::PhantomData; /// Weight functions needed for `pallet_subtensor_swap`. pub trait WeightInfo { fn set_fee_rate() -> Weight; - fn add_liquidity() -> Weight; - fn remove_liquidity() -> Weight; - fn modify_position() -> Weight; - fn disable_lp() -> Weight; - fn toggle_user_liquidity() -> Weight; } /// Weights for `pallet_subtensor_swap` using the Substrate node and recommended hardware. @@ -55,149 +50,11 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `529` // Estimated: `3994` - // Minimum execution time: 15_950_000 picoseconds. - Weight::from_parts(16_481_000, 3994) + // Minimum execution time: 14_491_000 picoseconds. + Weight::from_parts(15_063_000, 3994) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } - fn add_liquidity() -> Weight { - // Proof Size summary in bytes: - // Measured: `0` - // Estimated: `0` - // Minimum execution time: 2_484_000 picoseconds. - Weight::from_parts(2_675_000, 0) - } - /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) - /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::Positions` (r:1 w:1) - /// Proof: `Swap::Positions` (`max_values`: None, `max_size`: Some(140), added: 2615, mode: `MaxEncodedLen`) - /// Storage: `Swap::FeeGlobalTao` (r:1 w:0) - /// Proof: `Swap::FeeGlobalTao` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:0) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::Ticks` (r:2 w:2) - /// Proof: `Swap::Ticks` (`max_values`: None, `max_size`: Some(78), added: 2553, mode: `MaxEncodedLen`) - /// Storage: `Swap::FeeGlobalAlpha` (r:1 w:0) - /// Proof: `Swap::FeeGlobalAlpha` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:0) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:1) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) - /// Storage: `SubtensorModule::Owner` (r:1 w:0) - /// Proof: `SubtensorModule::Owner` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingHotkeys` (r:1 w:1) - /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::TotalHotkeyAlpha` (r:1 w:1) - /// Proof: `SubtensorModule::TotalHotkeyAlpha` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::Alpha` (r:1 w:0) - /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::AlphaV2` (r:1 w:1) - /// Proof: `SubtensorModule::AlphaV2` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::TotalHotkeyShares` (r:1 w:0) - /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:1 w:1) - /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) - fn remove_liquidity() -> Weight { - // Proof Size summary in bytes: - // Measured: `1600` - // Estimated: `6096` - // Minimum execution time: 2_535_000 picoseconds. - Weight::from_parts(2_535_000, 6096) - } - /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) - /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubtokenEnabled` (r:1 w:0) - /// Proof: `SubtensorModule::SubtokenEnabled` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::EnabledUserLiquidity` (r:1 w:0) - /// Proof: `Swap::EnabledUserLiquidity` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::Positions` (r:1 w:1) - /// Proof: `Swap::Positions` (`max_values`: None, `max_size`: Some(140), added: 2615, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:0) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:1) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) - /// Storage: `Swap::FeeGlobalTao` (r:1 w:0) - /// Proof: `Swap::FeeGlobalTao` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:0) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::Ticks` (r:2 w:2) - /// Proof: `Swap::Ticks` (`max_values`: None, `max_size`: Some(78), added: 2553, mode: `MaxEncodedLen`) - /// Storage: `Swap::FeeGlobalAlpha` (r:1 w:0) - /// Proof: `Swap::FeeGlobalAlpha` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `SubtensorModule::Owner` (r:1 w:0) - /// Proof: `SubtensorModule::Owner` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingHotkeys` (r:1 w:1) - /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::TotalHotkeyAlpha` (r:1 w:1) - /// Proof: `SubtensorModule::TotalHotkeyAlpha` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::Alpha` (r:1 w:0) - /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::AlphaV2` (r:1 w:1) - /// Proof: `SubtensorModule::AlphaV2` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::TotalHotkeyShares` (r:1 w:0) - /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:1 w:1) - /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) - fn modify_position() -> Weight { - // Proof Size summary in bytes: - // Measured: `1645` - // Estimated: `6096` - // Minimum execution time: 2_484_000 picoseconds. - Weight::from_parts(2_484_000, 6096) - } - /// Storage: `Swap::EnabledUserLiquidity` (r:128 w:128) - /// Proof: `Swap::EnabledUserLiquidity` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::SwapV3Initialized` (r:128 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::Positions` (r:256 w:128) - /// Proof: `Swap::Positions` (`max_values`: None, `max_size`: Some(140), added: 2615, mode: `MaxEncodedLen`) - /// Storage: `SubtensorModule::ValidatorTrust` (r:128 w:0) - /// Proof: `SubtensorModule::ValidatorTrust` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::ValidatorPermit` (r:128 w:0) - /// Proof: `SubtensorModule::ValidatorPermit` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::FeeGlobalTao` (r:128 w:0) - /// Proof: `Swap::FeeGlobalTao` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:128 w:0) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::Ticks` (r:256 w:256) - /// Proof: `Swap::Ticks` (`max_values`: None, `max_size`: Some(78), added: 2553, mode: `MaxEncodedLen`) - /// Storage: `Swap::FeeGlobalAlpha` (r:128 w:0) - /// Proof: `Swap::FeeGlobalAlpha` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:128 w:0) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:128 w:128) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) - /// Storage: `SubtensorModule::SubnetTAO` (r:128 w:128) - /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaIn` (r:128 w:128) - /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) - fn disable_lp() -> Weight { - // Proof Size summary in bytes: - // Measured: `32696` - // Estimated: `670430` - // Minimum execution time: 795_151_000 picoseconds. - Weight::from_parts(795_151_000, 670430) - .saturating_add(T::DbWeight::get().reads(128_u64)) - .saturating_add(T::DbWeight::get().writes(128_u64)) - } - /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) - /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) - fn toggle_user_liquidity() -> Weight { - // Proof Size summary in bytes: - // Measured: `538` - // Estimated: `4003` - // Minimum execution time: 13_054_000 picoseconds. - Weight::from_parts(13_335_000, 4003) - .saturating_add(T::DbWeight::get().reads(1_u64)) - } } // For backwards compatibility and tests. @@ -210,147 +67,9 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `529` // Estimated: `3994` - // Minimum execution time: 15_950_000 picoseconds. - Weight::from_parts(16_481_000, 3994) + // Minimum execution time: 14_491_000 picoseconds. + Weight::from_parts(15_063_000, 3994) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } - fn add_liquidity() -> Weight { - // Proof Size summary in bytes: - // Measured: `0` - // Estimated: `0` - // Minimum execution time: 2_484_000 picoseconds. - Weight::from_parts(2_675_000, 0) - } - /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) - /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::Positions` (r:1 w:1) - /// Proof: `Swap::Positions` (`max_values`: None, `max_size`: Some(140), added: 2615, mode: `MaxEncodedLen`) - /// Storage: `Swap::FeeGlobalTao` (r:1 w:0) - /// Proof: `Swap::FeeGlobalTao` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:0) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::Ticks` (r:2 w:2) - /// Proof: `Swap::Ticks` (`max_values`: None, `max_size`: Some(78), added: 2553, mode: `MaxEncodedLen`) - /// Storage: `Swap::FeeGlobalAlpha` (r:1 w:0) - /// Proof: `Swap::FeeGlobalAlpha` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:0) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:1) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) - /// Storage: `SubtensorModule::Owner` (r:1 w:0) - /// Proof: `SubtensorModule::Owner` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingHotkeys` (r:1 w:1) - /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::TotalHotkeyAlpha` (r:1 w:1) - /// Proof: `SubtensorModule::TotalHotkeyAlpha` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::Alpha` (r:1 w:0) - /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::AlphaV2` (r:1 w:1) - /// Proof: `SubtensorModule::AlphaV2` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::TotalHotkeyShares` (r:1 w:0) - /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:1 w:1) - /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetTAO` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaIn` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) - fn remove_liquidity() -> Weight { - // Proof Size summary in bytes: - // Measured: `1600` - // Estimated: `6096` - // Minimum execution time: 2_535_000 picoseconds. - Weight::from_parts(2_535_000, 6096) - } - /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) - /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubtokenEnabled` (r:1 w:0) - /// Proof: `SubtensorModule::SubtokenEnabled` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::EnabledUserLiquidity` (r:1 w:0) - /// Proof: `Swap::EnabledUserLiquidity` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::Positions` (r:1 w:1) - /// Proof: `Swap::Positions` (`max_values`: None, `max_size`: Some(140), added: 2615, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:1 w:0) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:1 w:1) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) - /// Storage: `Swap::FeeGlobalTao` (r:1 w:0) - /// Proof: `Swap::FeeGlobalTao` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:1 w:0) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::Ticks` (r:2 w:2) - /// Proof: `Swap::Ticks` (`max_values`: None, `max_size`: Some(78), added: 2553, mode: `MaxEncodedLen`) - /// Storage: `Swap::FeeGlobalAlpha` (r:1 w:0) - /// Proof: `Swap::FeeGlobalAlpha` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `SubtensorModule::Owner` (r:1 w:0) - /// Proof: `SubtensorModule::Owner` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaOut` (r:1 w:1) - /// Proof: `SubtensorModule::SubnetAlphaOut` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::StakingHotkeys` (r:1 w:1) - /// Proof: `SubtensorModule::StakingHotkeys` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::TotalHotkeyAlpha` (r:1 w:1) - /// Proof: `SubtensorModule::TotalHotkeyAlpha` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::Alpha` (r:1 w:0) - /// Proof: `SubtensorModule::Alpha` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::AlphaV2` (r:1 w:1) - /// Proof: `SubtensorModule::AlphaV2` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::TotalHotkeyShares` (r:1 w:0) - /// Proof: `SubtensorModule::TotalHotkeyShares` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::TotalHotkeySharesV2` (r:1 w:1) - /// Proof: `SubtensorModule::TotalHotkeySharesV2` (`max_values`: None, `max_size`: None, mode: `Measured`) - fn modify_position() -> Weight { - // Proof Size summary in bytes: - // Measured: `1645` - // Estimated: `6096` - // Minimum execution time: 2_484_000 picoseconds. - Weight::from_parts(2_484_000, 6096) - } - /// Storage: `Swap::EnabledUserLiquidity` (r:128 w:128) - /// Proof: `Swap::EnabledUserLiquidity` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::SwapV3Initialized` (r:128 w:0) - /// Proof: `Swap::SwapV3Initialized` (`max_values`: None, `max_size`: Some(11), added: 2486, mode: `MaxEncodedLen`) - /// Storage: `Swap::Positions` (r:256 w:128) - /// Proof: `Swap::Positions` (`max_values`: None, `max_size`: Some(140), added: 2615, mode: `MaxEncodedLen`) - /// Storage: `SubtensorModule::ValidatorTrust` (r:128 w:0) - /// Proof: `SubtensorModule::ValidatorTrust` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::ValidatorPermit` (r:128 w:0) - /// Proof: `SubtensorModule::ValidatorPermit` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `Swap::FeeGlobalTao` (r:128 w:0) - /// Proof: `Swap::FeeGlobalTao` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentTick` (r:128 w:0) - /// Proof: `Swap::CurrentTick` (`max_values`: None, `max_size`: Some(14), added: 2489, mode: `MaxEncodedLen`) - /// Storage: `Swap::Ticks` (r:256 w:256) - /// Proof: `Swap::Ticks` (`max_values`: None, `max_size`: Some(78), added: 2553, mode: `MaxEncodedLen`) - /// Storage: `Swap::FeeGlobalAlpha` (r:128 w:0) - /// Proof: `Swap::FeeGlobalAlpha` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::AlphaSqrtPrice` (r:128 w:0) - /// Proof: `Swap::AlphaSqrtPrice` (`max_values`: None, `max_size`: Some(26), added: 2501, mode: `MaxEncodedLen`) - /// Storage: `Swap::CurrentLiquidity` (r:128 w:128) - /// Proof: `Swap::CurrentLiquidity` (`max_values`: None, `max_size`: Some(18), added: 2493, mode: `MaxEncodedLen`) - /// Storage: `SubtensorModule::SubnetTAO` (r:128 w:128) - /// Proof: `SubtensorModule::SubnetTAO` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `SubtensorModule::SubnetAlphaIn` (r:128 w:128) - /// Proof: `SubtensorModule::SubnetAlphaIn` (`max_values`: None, `max_size`: None, mode: `Measured`) - fn disable_lp() -> Weight { - // Proof Size summary in bytes: - // Measured: `32696` - // Estimated: `670430` - // Minimum execution time: 795_151_000 picoseconds. - Weight::from_parts(795_151_000, 670430) - .saturating_add(RocksDbWeight::get().reads(128_u64)) - .saturating_add(RocksDbWeight::get().writes(128_u64)) - } - /// Storage: `SubtensorModule::NetworksAdded` (r:1 w:0) - /// Proof: `SubtensorModule::NetworksAdded` (`max_values`: None, `max_size`: None, mode: `Measured`) - fn toggle_user_liquidity() -> Weight { - // Proof Size summary in bytes: - // Measured: `538` - // Estimated: `4003` - // Minimum execution time: 13_054_000 picoseconds. - Weight::from_parts(13_335_000, 4003) - .saturating_add(RocksDbWeight::get().reads(1_u64)) - } } diff --git a/pallets/transaction-fee/Cargo.toml b/pallets/transaction-fee/Cargo.toml index 9e19bf2758..22f89b5f4c 100644 --- a/pallets/transaction-fee/Cargo.toml +++ b/pallets/transaction-fee/Cargo.toml @@ -91,4 +91,5 @@ runtime-benchmarks = [ "pallet-subtensor/runtime-benchmarks", "pallet-subtensor-swap/runtime-benchmarks", "subtensor-runtime-common/runtime-benchmarks", + "subtensor-swap-interface/runtime-benchmarks", ] diff --git a/pallets/transaction-fee/src/lib.rs b/pallets/transaction-fee/src/lib.rs index eab3006d79..fbbe2cd805 100644 --- a/pallets/transaction-fee/src/lib.rs +++ b/pallets/transaction-fee/src/lib.rs @@ -200,7 +200,7 @@ where ) .map(|tao_amount| (alpha_fee, tao_amount, *netuid)) .map_err(|err| { - log::error!("Error withdrawing transaction fee in alpha: {err:?}"); + log::warn!("Error withdrawing transaction fee in alpha: {err:?}"); InvalidTransaction::Payment.into() }) } else { diff --git a/pallets/transaction-fee/src/tests/mock.rs b/pallets/transaction-fee/src/tests/mock.rs index 343decb8a8..703ed27ba2 100644 --- a/pallets/transaction-fee/src/tests/mock.rs +++ b/pallets/transaction-fee/src/tests/mock.rs @@ -6,12 +6,10 @@ use crate::TransactionFeeHandler; use frame_support::pallet_prelude::Zero; use frame_support::{ PalletId, assert_ok, derive_impl, parameter_types, - traits::{Everything, Hooks, InherentBuilder, PrivilegeCmp}, + traits::{Everything, Hooks, PrivilegeCmp}, weights::IdentityFee, }; -use frame_system::{ - self as system, EnsureRoot, RawOrigin, limits, offchain::CreateTransactionBase, -}; +use frame_system::{self as system, EnsureRoot, RawOrigin, limits}; pub use pallet_subtensor::*; pub use sp_core::U256; use sp_core::{ConstU64, H256}; @@ -195,6 +193,10 @@ parameter_types! { pub const InitialMaxBurn: TaoBalance = TaoBalance::new(1_000_000_000); pub const MinBurnUpperBound: TaoBalance = TaoBalance::new(1_000_000_000); // 1 TAO pub const MaxBurnLowerBound: TaoBalance = TaoBalance::new(100_000_000); // 0.1 TAO + pub const MinTempo: u16 = pallet_subtensor::MIN_TEMPO; + pub const MaxTempo: u16 = pallet_subtensor::MAX_TEMPO; + pub const MinActivityCutoffFactorMilli: u32 = pallet_subtensor::MIN_ACTIVITY_CUTOFF_FACTOR_MILLI; + pub const MaxActivityCutoffFactorMilli: u32 = pallet_subtensor::MAX_ACTIVITY_CUTOFF_FACTOR_MILLI; pub const InitialValidatorPruneLen: u64 = 0; pub const InitialScalingLawPower: u16 = 50; pub const InitialMaxAllowedValidators: u16 = 100; @@ -233,6 +235,7 @@ parameter_types! { pub const EvmKeyAssociateRateLimit: u64 = 0; pub const SubtensorPalletId: PalletId = PalletId(*b"subtensr"); pub const BurnAccountId: PalletId = PalletId(*b"burntnsr"); + pub const MaxEpochsPerBlock: u32 = 32; } impl pallet_subtensor::Config for Test { @@ -281,6 +284,10 @@ impl pallet_subtensor::Config for Test { type InitialMinStake = InitialMinStake; type MinBurnUpperBound = MinBurnUpperBound; type MaxBurnLowerBound = MaxBurnLowerBound; + type MinTempo = MinTempo; + type MaxTempo = MaxTempo; + type MinActivityCutoffFactorMilli = MinActivityCutoffFactorMilli; + type MaxActivityCutoffFactorMilli = MaxActivityCutoffFactorMilli; type InitialRAORecycledForRegistration = InitialRAORecycledForRegistration; type InitialNetworkImmunityPeriod = InitialNetworkImmunityPeriod; type InitialNetworkMinLockCost = InitialNetworkMinLockCost; @@ -312,6 +319,7 @@ impl pallet_subtensor::Config for Test { type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; type BurnAccountId = BurnAccountId; + type MaxEpochsPerBlock = MaxEpochsPerBlock; type WeightInfo = (); } @@ -415,7 +423,6 @@ impl pallet_alpha_assets::Config for Test {} 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(1_000_000).unwrap(); } @@ -427,7 +434,6 @@ impl pallet_subtensor_swap::Config for Test { type TaoReserve = pallet_subtensor::TaoBalanceReserve; type AlphaReserve = pallet_subtensor::AlphaBalanceReserve; type MaxFeeRate = SwapMaxFeeRate; - type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; type MinimumReserve = SwapMinimumReserve; type WeightInfo = (); @@ -523,39 +529,12 @@ where type RuntimeCall = RuntimeCall; } -impl frame_system::offchain::CreateInherent for Test +impl frame_system::offchain::CreateBare 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 { - let extra: TransactionExtensions = ( - frame_system::CheckNonZeroSender::::new(), - frame_system::CheckWeight::::new(), - pallet_transaction_payment::ChargeTransactionPayment::::from(0.into()), - ); - - Some(UncheckedExtrinsic::new_signed( - call, - nonce.into(), - (), - extra, - )) + UncheckedExtrinsic::new_bare(call) } } @@ -592,8 +571,7 @@ pub fn register_ok_neuron( // Ensure reserves exist for swap/burn path, but do NOT clobber reserves if the test already set them. let reserve: u64 = 1_000_000_000_000; let tao_reserve = SubnetTAO::::get(netuid); - let alpha_reserve = - SubnetAlphaIn::::get(netuid) + SubnetAlphaInProvided::::get(netuid); + let alpha_reserve = SubnetAlphaIn::::get(netuid); if tao_reserve.is_zero() && alpha_reserve.is_zero() { setup_reserves(netuid, reserve.into(), reserve.into()); @@ -822,10 +800,6 @@ pub fn setup_subnets(sncount: u16, neurons: u16) -> TestSetup { } } -pub(crate) fn remove_stake_rate_limit_for_tests(hotkey: &U256, coldkey: &U256, netuid: NetUid) { - StakingOperationRateLimiter::::remove((hotkey, coldkey, netuid)); -} - #[allow(dead_code)] pub fn setup_stake( netuid: subtensor_runtime_common::NetUid, @@ -845,7 +819,6 @@ pub fn setup_stake( netuid, stake_amount.into(), )); - remove_stake_rate_limit_for_tests(hotkey, coldkey, netuid); } pub(crate) fn quote_remove_stake_after_alpha_fee( diff --git a/pallets/transaction-fee/src/tests/mod.rs b/pallets/transaction-fee/src/tests/mod.rs index 8203070d76..f452634cfe 100644 --- a/pallets/transaction-fee/src/tests/mod.rs +++ b/pallets/transaction-fee/src/tests/mod.rs @@ -5,12 +5,10 @@ use frame_support::dispatch::GetDispatchInfo; use frame_support::pallet_prelude::Zero; use frame_support::traits::Currency; use frame_support::{assert_err, assert_ok}; -use pallet_subtensor_swap::AlphaSqrtPrice; use sp_runtime::{ traits::{DispatchTransaction, TransactionExtension, TxBaseImplication}, transaction_validity::{InvalidTransaction, TransactionValidityError}, }; -use substrate_fixed::types::U64F64; use subtensor_runtime_common::AlphaBalance; use mock::*; @@ -709,7 +707,9 @@ fn test_remove_stake_edge_alpha() { assert_ok!(result); // Lower Alpha price to 0.0001 so that there is not enough alpha to cover tx fees - AlphaSqrtPrice::::insert(sn.subnets[0].netuid, U64F64::from_num(0.01)); + SubnetTAO::::insert(sn.subnets[0].netuid, TaoBalance::from(1_000_000)); + SubnetAlphaIn::::insert(sn.subnets[0].netuid, AlphaBalance::from(10_000_000_000_u64)); + let result_low_alpha_price = ext.validate( RuntimeOrigin::signed(sn.coldkey).into(), &call.clone(), @@ -1540,7 +1540,6 @@ fn test_add_stake_fees_go_to_block_builder() { let (_, swap_fee) = mock::swap_tao_to_alpha(sn.subnets[0].netuid, stake_amount.into()); add_balance_to_coldkey_account(&sn.coldkey, (stake_amount * 10).into()); - remove_stake_rate_limit_for_tests(&sn.hotkeys[0], &sn.coldkey, sn.subnets[0].netuid); // Stake let balance_before = Balances::free_balance(sn.coldkey); diff --git a/pallets/utility/src/weights.rs b/pallets/utility/src/weights.rs index 0287a464fe..4fe14ea89b 100644 --- a/pallets/utility/src/weights.rs +++ b/pallets/utility/src/weights.rs @@ -2,9 +2,9 @@ //! Autogenerated weights for `pallet_subtensor_utility` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 49.1.0 -//! DATE: 2026-05-31, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-06-15, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `runnervm3jyl0`, CPU: `AMD EPYC 9V74 80-Core Processor` +//! HOSTNAME: `runnervm1li68`, CPU: `AMD EPYC 9V74 80-Core Processor` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` // Executed Command: @@ -22,7 +22,7 @@ // --no-storage-info // --no-min-squares // --no-median-slopes -// --output=/tmp/tmp.wsE00cetaq +// --output=/tmp/tmp.dful3SGU9S // --template=/home/runner/work/subtensor/subtensor/.maintain/frame-weight-template.hbs #![cfg_attr(rustfmt, rustfmt_skip)] @@ -57,10 +57,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `518` // Estimated: `3983` - // Minimum execution time: 4_006_000 picoseconds. - Weight::from_parts(12_108_520, 3983) - // Standard Error: 3_458 - .saturating_add(Weight::from_parts(5_277_918, 0).saturating_mul(c.into())) + // Minimum execution time: 3_655_000 picoseconds. + Weight::from_parts(13_879_015, 3983) + // Standard Error: 2_223 + .saturating_add(Weight::from_parts(5_280_856, 0).saturating_mul(c.into())) .saturating_add(T::DbWeight::get().reads(2_u64)) } /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) @@ -71,8 +71,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `518` // Estimated: `3983` - // Minimum execution time: 13_480_000 picoseconds. - Weight::from_parts(13_830_000, 3983) + // Minimum execution time: 13_380_000 picoseconds. + Weight::from_parts(13_880_000, 3983) .saturating_add(T::DbWeight::get().reads(2_u64)) } /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) @@ -85,17 +85,17 @@ impl WeightInfo for SubstrateWeight { // Measured: `518` // Estimated: `3983` // Minimum execution time: 3_825_000 picoseconds. - Weight::from_parts(15_244_012, 3983) - // Standard Error: 2_052 - .saturating_add(Weight::from_parts(5_505_817, 0).saturating_mul(c.into())) + Weight::from_parts(9_466_022, 3983) + // Standard Error: 1_996 + .saturating_add(Weight::from_parts(5_530_123, 0).saturating_mul(c.into())) .saturating_add(T::DbWeight::get().reads(2_u64)) } fn dispatch_as() -> Weight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 5_398_000 picoseconds. - Weight::from_parts(5_768_000, 0) + // Minimum execution time: 5_508_000 picoseconds. + Weight::from_parts(5_809_000, 0) } /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -106,18 +106,18 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `518` // Estimated: `3983` - // Minimum execution time: 3_896_000 picoseconds. - Weight::from_parts(1_067_442, 3983) - // Standard Error: 4_142 - .saturating_add(Weight::from_parts(5_312_644, 0).saturating_mul(c.into())) + // Minimum execution time: 3_736_000 picoseconds. + Weight::from_parts(15_189_579, 3983) + // Standard Error: 1_690 + .saturating_add(Weight::from_parts(5_270_917, 0).saturating_mul(c.into())) .saturating_add(T::DbWeight::get().reads(2_u64)) } fn dispatch_as_fallible() -> Weight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 5_498_000 picoseconds. - Weight::from_parts(5_708_000, 0) + // Minimum execution time: 5_338_000 picoseconds. + Weight::from_parts(5_719_000, 0) } /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -127,8 +127,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `518` // Estimated: `3983` - // Minimum execution time: 18_757_000 picoseconds. - Weight::from_parts(19_459_000, 3983) + // Minimum execution time: 18_498_000 picoseconds. + Weight::from_parts(19_339_000, 3983) .saturating_add(T::DbWeight::get().reads(2_u64)) } } @@ -144,10 +144,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `518` // Estimated: `3983` - // Minimum execution time: 4_006_000 picoseconds. - Weight::from_parts(12_108_520, 3983) - // Standard Error: 3_458 - .saturating_add(Weight::from_parts(5_277_918, 0).saturating_mul(c.into())) + // Minimum execution time: 3_655_000 picoseconds. + Weight::from_parts(13_879_015, 3983) + // Standard Error: 2_223 + .saturating_add(Weight::from_parts(5_280_856, 0).saturating_mul(c.into())) .saturating_add(RocksDbWeight::get().reads(2_u64)) } /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) @@ -158,8 +158,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `518` // Estimated: `3983` - // Minimum execution time: 13_480_000 picoseconds. - Weight::from_parts(13_830_000, 3983) + // Minimum execution time: 13_380_000 picoseconds. + Weight::from_parts(13_880_000, 3983) .saturating_add(RocksDbWeight::get().reads(2_u64)) } /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) @@ -172,17 +172,17 @@ impl WeightInfo for () { // Measured: `518` // Estimated: `3983` // Minimum execution time: 3_825_000 picoseconds. - Weight::from_parts(15_244_012, 3983) - // Standard Error: 2_052 - .saturating_add(Weight::from_parts(5_505_817, 0).saturating_mul(c.into())) + Weight::from_parts(9_466_022, 3983) + // Standard Error: 1_996 + .saturating_add(Weight::from_parts(5_530_123, 0).saturating_mul(c.into())) .saturating_add(RocksDbWeight::get().reads(2_u64)) } fn dispatch_as() -> Weight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 5_398_000 picoseconds. - Weight::from_parts(5_768_000, 0) + // Minimum execution time: 5_508_000 picoseconds. + Weight::from_parts(5_809_000, 0) } /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -193,18 +193,18 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `518` // Estimated: `3983` - // Minimum execution time: 3_896_000 picoseconds. - Weight::from_parts(1_067_442, 3983) - // Standard Error: 4_142 - .saturating_add(Weight::from_parts(5_312_644, 0).saturating_mul(c.into())) + // Minimum execution time: 3_736_000 picoseconds. + Weight::from_parts(15_189_579, 3983) + // Standard Error: 1_690 + .saturating_add(Weight::from_parts(5_270_917, 0).saturating_mul(c.into())) .saturating_add(RocksDbWeight::get().reads(2_u64)) } fn dispatch_as_fallible() -> Weight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 5_498_000 picoseconds. - Weight::from_parts(5_708_000, 0) + // Minimum execution time: 5_338_000 picoseconds. + Weight::from_parts(5_719_000, 0) } /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -214,8 +214,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `518` // Estimated: `3983` - // Minimum execution time: 18_757_000 picoseconds. - Weight::from_parts(19_459_000, 3983) + // Minimum execution time: 18_498_000 picoseconds. + Weight::from_parts(19_339_000, 3983) .saturating_add(RocksDbWeight::get().reads(2_u64)) } } diff --git a/precompiles/Cargo.toml b/precompiles/Cargo.toml index c896ecb731..dd5e20dfd0 100644 --- a/precompiles/Cargo.toml +++ b/precompiles/Cargo.toml @@ -99,6 +99,7 @@ runtime-benchmarks = [ "pallet-timestamp/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "subtensor-runtime-common/runtime-benchmarks", + "subtensor-swap-interface/runtime-benchmarks" ] [dev-dependencies] diff --git a/precompiles/src/alpha.rs b/precompiles/src/alpha.rs index b183c5ec23..9840c42575 100644 --- a/precompiles/src/alpha.rs +++ b/precompiles/src/alpha.rs @@ -1,16 +1,16 @@ use core::marker::PhantomData; +use crate::PrecompileExt; use fp_evm::{ExitError, PrecompileFailure}; use pallet_evm::{BalanceConverter, PrecompileHandle, SubstrateBalance}; use precompile_utils::EvmResult; +use sp_runtime::{SaturatedConversion, Vec}; + +use crate::PrecompileHandleExt; use sp_core::U256; -use sp_std::vec::Vec; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::U64F64; use subtensor_runtime_common::{NetUid, Token}; use subtensor_swap_interface::{Order, SwapHandler}; - -use crate::PrecompileExt; - pub struct AlphaPrecompile(PhantomData); impl PrecompileExt for AlphaPrecompile @@ -34,10 +34,12 @@ where { #[precompile::public("getAlphaPrice(uint16)")] #[precompile::view] - fn get_alpha_price(_handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_alpha_price(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + // SubnetMechanism + SubnetAlphaIn + SubnetTAO + SwapBalancer reads + handle.record_db_reads::(4)?; let current_alpha_price = as SwapHandler>::current_alpha_price(netuid.into()); - let price = current_alpha_price.saturating_mul(U96F32::from_num(1_000_000_000)); + let price = current_alpha_price.saturating_mul(U64F64::from_num(1_000_000_000)); let price: SubstrateBalance = price.saturating_to_num::().into(); let price_eth = ::BalanceConverter::into_evm_balance(price) .map(|amount| amount.into_u256()) @@ -48,10 +50,12 @@ where #[precompile::public("getMovingAlphaPrice(uint16)")] #[precompile::view] - fn get_moving_alpha_price(_handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { - let moving_alpha_price: U96F32 = + fn get_moving_alpha_price(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + // SubnetMechanism + SubnetMovingPrice reads + handle.record_db_reads::(2)?; + let moving_alpha_price: U64F64 = pallet_subtensor::Pallet::::get_moving_alpha_price(netuid.into()); - let price = moving_alpha_price.saturating_mul(U96F32::from_num(1_000_000_000)); + let price = moving_alpha_price.saturating_mul(U64F64::from_num(1_000_000_000)); let price: SubstrateBalance = price.saturating_to_num::().into(); let price_eth = ::BalanceConverter::into_evm_balance(price) .map(|amount| amount.into_u256()) @@ -62,38 +66,45 @@ where #[precompile::public("getTaoInPool(uint16)")] #[precompile::view] - fn get_tao_in_pool(_handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_tao_in_pool(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::SubnetTAO::::get(NetUid::from(netuid)).to_u64()) } #[precompile::public("getAlphaInPool(uint16)")] #[precompile::view] - fn get_alpha_in_pool(_handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_alpha_in_pool(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::SubnetAlphaIn::::get(NetUid::from(netuid)).into()) } #[precompile::public("getAlphaOutPool(uint16)")] #[precompile::view] - fn get_alpha_out_pool(_handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_alpha_out_pool(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::SubnetAlphaOut::::get(NetUid::from(netuid)).into()) } #[precompile::public("getAlphaIssuance(uint16)")] #[precompile::view] - fn get_alpha_issuance(_handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_alpha_issuance(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + // SubnetAlphaIn + SubnetAlphaOut reads + handle.record_db_reads::(2)?; Ok(pallet_subtensor::Pallet::::get_alpha_issuance(netuid.into()).into()) } #[precompile::public("getTaoWeight()")] #[precompile::view] - fn get_tao_weight(_handle: &mut impl PrecompileHandle) -> EvmResult { + fn get_tao_weight(handle: &mut impl PrecompileHandle) -> EvmResult { + handle.record_db_reads::(1)?; let tao_weight = pallet_subtensor::TaoWeight::::get(); Ok(U256::from(tao_weight)) } #[precompile::public("getCKBurn()")] #[precompile::view] - fn get_ck_burn(_handle: &mut impl PrecompileHandle) -> EvmResult { + fn get_ck_burn(handle: &mut impl PrecompileHandle) -> EvmResult { + handle.record_db_reads::(1)?; let ck_burn = pallet_subtensor::CKBurn::::get(); Ok(U256::from(ck_burn)) } @@ -101,10 +112,13 @@ where #[precompile::public("simSwapTaoForAlpha(uint16,uint64)")] #[precompile::view] fn sim_swap_tao_for_alpha( - _handle: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, netuid: u16, tao: u64, ) -> EvmResult { + // SubnetMechanism + swap simulation reads + handle.record_db_reads::(9)?; + let order = pallet_subtensor::GetAlphaForTao::::with_amount(tao); let swap_result = as SwapHandler>::sim_swap(netuid.into(), order) @@ -117,10 +131,13 @@ where #[precompile::public("simSwapAlphaForTao(uint16,uint64)")] #[precompile::view] fn sim_swap_alpha_for_tao( - _handle: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, netuid: u16, alpha: u64, ) -> EvmResult { + // SubnetMechanism + swap simulation reads + handle.record_db_reads::(9)?; + let order = pallet_subtensor::GetTaoForAlpha::::with_amount(alpha); let swap_result = as SwapHandler>::sim_swap(netuid.into(), order) @@ -132,7 +149,8 @@ where #[precompile::public("getSubnetMechanism(uint16)")] #[precompile::view] - fn get_subnet_mechanism(_handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_subnet_mechanism(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::SubnetMechanism::::get(NetUid::from( netuid, ))) @@ -147,9 +165,10 @@ where #[precompile::public("getEMAPriceHalvingBlocks(uint16)")] #[precompile::view] fn get_ema_price_halving_blocks( - _handle: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, netuid: u16, ) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::EMAPriceHalvingBlocks::::get( NetUid::from(netuid), )) @@ -157,7 +176,8 @@ where #[precompile::public("getSubnetVolume(uint16)")] #[precompile::view] - fn get_subnet_volume(_handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_subnet_volume(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(U256::from(pallet_subtensor::SubnetVolume::::get( NetUid::from(netuid), ))) @@ -165,7 +185,8 @@ where #[precompile::public("getTaoInEmission(uint16)")] #[precompile::view] - fn get_tao_in_emission(_handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_tao_in_emission(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(U256::from( pallet_subtensor::SubnetTaoInEmission::::get(NetUid::from(netuid)).to_u64(), )) @@ -173,7 +194,8 @@ where #[precompile::public("getAlphaInEmission(uint16)")] #[precompile::view] - fn get_alpha_in_emission(_handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_alpha_in_emission(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(U256::from( pallet_subtensor::SubnetAlphaInEmission::::get(NetUid::from(netuid)).to_u64(), )) @@ -181,7 +203,8 @@ where #[precompile::public("getAlphaOutEmission(uint16)")] #[precompile::view] - fn get_alpha_out_emission(_handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_alpha_out_emission(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(U256::from( pallet_subtensor::SubnetAlphaOutEmission::::get(NetUid::from(netuid)).to_u64(), )) @@ -189,23 +212,38 @@ where #[precompile::public("getSumAlphaPrice()")] #[precompile::view] - fn get_sum_alpha_price(_handle: &mut impl PrecompileHandle) -> EvmResult { + fn get_sum_alpha_price(handle: &mut impl PrecompileHandle) -> EvmResult { + // NetworksAdded iteration + current_alpha_price reads + handle.record_db_reads::(1)?; + let subnet_limit = pallet_subtensor::SubnetLimit::::get().saturated_into::(); + + handle.record_db_reads::(subnet_limit)?; + + let mut sum_alpha_price: U64F64 = U64F64::from_num(0); let netuids = pallet_subtensor::NetworksAdded::::iter() .filter(|(netuid, _)| *netuid != NetUid::ROOT) + .map(|(netuid, _)| netuid) .collect::>(); - let mut sum_alpha_price: U96F32 = U96F32::from_num(0); - for (netuid, _) in netuids { - let price = as SwapHandler>::current_alpha_price( - netuid.into(), - ); + // NetworksAdded entry + current_alpha_price reads + handle.record_db_reads::( + netuids + .len() + .saturated_into::() + .saturating_mul(5) + .saturating_sub(subnet_limit), + )?; + + for netuid in netuids.iter() { + let price = + as SwapHandler>::current_alpha_price(*netuid); - if price < U96F32::from_num(1) { + if price < U64F64::from_num(1) { sum_alpha_price = sum_alpha_price.saturating_add(price); } } - let price = sum_alpha_price.saturating_mul(U96F32::from_num(1_000_000_000)); + let price = sum_alpha_price.saturating_mul(U64F64::from_num(1_000_000_000)); let price: SubstrateBalance = price.saturating_to_num::().into(); let price_eth = ::BalanceConverter::into_evm_balance(price) .map(|amount| amount.into_u256()) @@ -311,8 +349,8 @@ mod tests { let moving_alpha_price = pallet_subtensor::Pallet::::get_moving_alpha_price(dynamic_netuid); - assert!(alpha_price > U96F32::from_num(1)); - assert!(moving_alpha_price > U96F32::from_num(1)); + assert!(alpha_price > U64F64::from_num(1)); + assert!(moving_alpha_price > U64F64::from_num(1)); assert_static_call( &precompiles, @@ -457,7 +495,7 @@ mod tests { let caller = addr_from_index(1); let precompile_addr = addr_from_index(AlphaPrecompile::::INDEX); - let mut sum_alpha_price = U96F32::from_num(0); + let mut sum_alpha_price = U64F64::from_num(0); for (netuid, _) in pallet_subtensor::NetworksAdded::::iter() { if netuid.is_root() { continue; @@ -466,12 +504,12 @@ mod tests { as SwapHandler>::current_alpha_price( netuid, ); - if price < U96F32::from_num(1) { + if price < U64F64::from_num(1) { sum_alpha_price += price; } } - assert!(sum_alpha_price > U96F32::from_num(0)); + assert!(sum_alpha_price > U64F64::from_num(0)); assert_static_call( &precompiles, diff --git a/precompiles/src/crowdloan.rs b/precompiles/src/crowdloan.rs index c474ab9405..1c66d941ca 100644 --- a/precompiles/src/crowdloan.rs +++ b/precompiles/src/crowdloan.rs @@ -75,9 +75,10 @@ where #[precompile::public("getCrowdloan(uint32)")] #[precompile::view] fn get_crowdloan( - _handle: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, crowdloan_id: u32, ) -> EvmResult { + handle.record_db_reads::(1)?; let crowdloan = pallet_crowdloan::Crowdloans::::get(crowdloan_id).ok_or( PrecompileFailure::Error { exit_status: ExitError::Other("Crowdloan not found".into()), @@ -105,10 +106,11 @@ where #[precompile::public("getContribution(uint32,bytes32)")] #[precompile::view] fn get_contribution( - _handle: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, crowdloan_id: u32, coldkey: H256, ) -> EvmResult { + handle.record_db_reads::(1)?; let coldkey = R::AccountId::from(coldkey.0); let contribution = pallet_crowdloan::Contributions::::get(crowdloan_id, coldkey).ok_or( PrecompileFailure::Error { diff --git a/precompiles/src/extensions.rs b/precompiles/src/extensions.rs index 4a7c418c86..b98fcfb515 100644 --- a/precompiles/src/extensions.rs +++ b/precompiles/src/extensions.rs @@ -12,6 +12,7 @@ use pallet_evm::{ }; use pallet_subtensor::SubtensorTransactionExtension; use precompile_utils::EvmResult; +use precompile_utils::prelude::RuntimeHelper; use scale_info::TypeInfo; use sp_core::{H160, U256, blake2_256}; use sp_runtime::{ @@ -34,6 +35,23 @@ pub(crate) trait PrecompileHandleExt: PrecompileHandle { ::AddressMapping::into_account_id(self.context().caller) } + fn record_db_reads(&mut self, reads: u64) -> EvmResult<()> + where + R: frame_system::Config + pallet_evm::Config, + { + self.record_cost(RuntimeHelper::::db_read_gas_cost().saturating_mul(reads))?; + Ok(()) + } + + fn record_db_writes(&mut self, writes: u64) -> EvmResult<()> + where + R: frame_system::Config + pallet_evm::Config, + { + self.record_cost(RuntimeHelper::::db_write_gas_cost().saturating_mul(writes))?; + + Ok(()) + } + fn try_convert_apparent_value(&self) -> EvmResult where R: pallet_evm::Config, diff --git a/precompiles/src/leasing.rs b/precompiles/src/leasing.rs index 005782c776..5ebf03cb3c 100644 --- a/precompiles/src/leasing.rs +++ b/precompiles/src/leasing.rs @@ -73,7 +73,8 @@ where { #[precompile::public("getLease(uint32)")] #[precompile::view] - fn get_lease(_handle: &mut impl PrecompileHandle, lease_id: u32) -> EvmResult { + fn get_lease(handle: &mut impl PrecompileHandle, lease_id: u32) -> EvmResult { + handle.record_db_reads::(1)?; let lease = pallet_subtensor::SubnetLeases::::get(lease_id).ok_or(PrecompileFailure::Error { exit_status: ExitError::Other("Lease not found".into()), @@ -97,10 +98,11 @@ where #[precompile::public("getContributorShare(uint32,bytes32)")] #[precompile::view] fn get_contributor_share( - _handle: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, lease_id: u32, contributor: H256, ) -> EvmResult<(u128, u128)> { + handle.record_db_reads::(1)?; let contributor = R::AccountId::from(contributor.0); let share = pallet_subtensor::SubnetLeaseShares::::get(lease_id, contributor); @@ -109,7 +111,8 @@ where #[precompile::public("getLeaseIdForSubnet(uint16)")] #[precompile::view] - fn get_lease_id_for_subnet(_handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_lease_id_for_subnet(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; let lease_id = pallet_subtensor::SubnetUidToLeaseId::::get(NetUid::from(netuid)).ok_or( PrecompileFailure::Error { exit_status: ExitError::Other("Lease not found for netuid".into()), diff --git a/precompiles/src/lib.rs b/precompiles/src/lib.rs index 39815a6946..cf54934d95 100644 --- a/precompiles/src/lib.rs +++ b/precompiles/src/lib.rs @@ -4,12 +4,22 @@ extern crate alloc; use core::marker::PhantomData; +use crate::extensions::*; +pub use address_mapping::AddressMappingPrecompile; +pub use alpha::AlphaPrecompile; +pub use balance_transfer::BalanceTransferPrecompile; +pub use crowdloan::CrowdloanPrecompile; +pub use ed25519::Ed25519Verify; +pub use extensions::PrecompileExt; use fp_evm::{ExitError, PrecompileFailure}; use frame_support::traits::IsSubType; use frame_support::{ dispatch::{DispatchInfo, GetDispatchInfo, PostDispatchInfo}, pallet_prelude::Decode, }; +pub use leasing::LeasingPrecompile; +pub use metagraph::MetagraphPrecompile; +pub use neuron::NeuronPrecompile; use pallet_admin_utils::PrecompileEnum; use pallet_evm::{ AddressMapping, IsPrecompileResult, Precompile, PrecompileHandle, PrecompileResult, @@ -21,26 +31,14 @@ use pallet_evm_precompile_modexp::Modexp; use pallet_evm_precompile_sha3fips::Sha3FIPS256; use pallet_evm_precompile_simple::{ECRecover, ECRecoverPublicKey, Identity, Ripemd160, Sha256}; use pallet_subtensor_proxy as pallet_proxy; +pub use proxy::ProxyPrecompile; use sp_core::{H160, U256, crypto::ByteArray}; use sp_runtime::traits::{AsSystemOriginSigner, Dispatchable, StaticLookup}; -use subtensor_runtime_common::ProxyType; - -use crate::extensions::*; - -pub use address_mapping::AddressMappingPrecompile; -pub use alpha::AlphaPrecompile; -pub use balance_transfer::BalanceTransferPrecompile; -pub use crowdloan::CrowdloanPrecompile; -pub use ed25519::Ed25519Verify; -pub use extensions::PrecompileExt; -pub use leasing::LeasingPrecompile; -pub use metagraph::MetagraphPrecompile; -pub use neuron::NeuronPrecompile; -pub use proxy::ProxyPrecompile; pub use sr25519::Sr25519Verify; pub use staking::{StakingPrecompile, StakingPrecompileV2}; pub use storage_query::StorageQueryPrecompile; pub use subnet::SubnetPrecompile; +use subtensor_runtime_common::ProxyType; pub use uid_lookup::UidLookupPrecompile; pub use voting_power::VotingPowerPrecompile; diff --git a/precompiles/src/metagraph.rs b/precompiles/src/metagraph.rs index 4cffb76a4f..ec8086a87e 100644 --- a/precompiles/src/metagraph.rs +++ b/precompiles/src/metagraph.rs @@ -8,12 +8,13 @@ use sp_core::{ByteArray, H256}; use subtensor_runtime_common::{NetUid, Token}; use crate::PrecompileExt; +use crate::PrecompileHandleExt; pub struct MetagraphPrecompile(PhantomData); impl PrecompileExt for MetagraphPrecompile where - R: frame_system::Config + pallet_subtensor::Config, + R: frame_system::Config + pallet_subtensor::Config + pallet_evm::Config, R::AccountId: From<[u8; 32]> + ByteArray, { const INDEX: u64 = 2050; @@ -22,12 +23,13 @@ where #[precompile_utils::precompile] impl MetagraphPrecompile where - R: frame_system::Config + pallet_subtensor::Config, + R: frame_system::Config + pallet_subtensor::Config + pallet_evm::Config, R::AccountId: ByteArray, { #[precompile::public("getUidCount(uint16)")] #[precompile::view] - fn get_uid_count(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_uid_count(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::SubnetworkN::::get(NetUid::from( netuid, ))) @@ -35,7 +37,9 @@ where #[precompile::public("getStake(uint16,uint16)")] #[precompile::view] - fn get_stake(_: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + fn get_stake(handle: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + // Keys + TotalHotkeyAlpha reads + handle.record_db_reads::(2)?; let hotkey = pallet_subtensor::Pallet::::get_hotkey_for_net_and_uid(netuid.into(), uid) .map_err(|_| PrecompileFailure::Error { exit_status: ExitError::InvalidRange, @@ -60,7 +64,8 @@ where #[precompile::public("getConsensus(uint16,uint16)")] #[precompile::view] - fn get_consensus(_: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + fn get_consensus(handle: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::Pallet::::get_consensus_for_uid( netuid.into(), uid, @@ -69,7 +74,8 @@ where #[precompile::public("getIncentive(uint16,uint16)")] #[precompile::view] - fn get_incentive(_: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + fn get_incentive(handle: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::Pallet::::get_incentive_for_uid( netuid.into(), uid, @@ -78,7 +84,8 @@ where #[precompile::public("getDividends(uint16,uint16)")] #[precompile::view] - fn get_dividends(_: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + fn get_dividends(handle: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::Pallet::::get_dividends_for_uid( netuid.into(), uid, @@ -87,13 +94,15 @@ where #[precompile::public("getEmission(uint16,uint16)")] #[precompile::view] - fn get_emission(_: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + fn get_emission(handle: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::Pallet::::get_emission_for_uid(netuid.into(), uid).into()) } #[precompile::public("getVtrust(uint16,uint16)")] #[precompile::view] - fn get_vtrust(_: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + fn get_vtrust(handle: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::Pallet::::get_validator_trust_for_uid( netuid.into(), uid, @@ -103,10 +112,11 @@ where #[precompile::public("getValidatorStatus(uint16,uint16)")] #[precompile::view] fn get_validator_status( - _: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, netuid: u16, uid: u16, ) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::Pallet::::get_validator_permit_for_uid( netuid.into(), uid, @@ -115,7 +125,12 @@ where #[precompile::public("getLastUpdate(uint16,uint16)")] #[precompile::view] - fn get_last_update(_: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + fn get_last_update( + handle: &mut impl PrecompileHandle, + netuid: u16, + uid: u16, + ) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::Pallet::::get_last_update_for_uid( netuid.into(), uid, @@ -124,7 +139,8 @@ where #[precompile::public("getIsActive(uint16,uint16)")] #[precompile::view] - fn get_is_active(_: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + fn get_is_active(handle: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::Pallet::::get_active_for_uid( netuid.into(), uid, @@ -133,7 +149,9 @@ where #[precompile::public("getAxon(uint16,uint16)")] #[precompile::view] - fn get_axon(_: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + fn get_axon(handle: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + // Keys + Axons reads + handle.record_db_reads::(2)?; let hotkey = pallet_subtensor::Pallet::::get_hotkey_for_net_and_uid(netuid.into(), uid) .map_err(|_| PrecompileFailure::Error { exit_status: ExitError::Other("hotkey not found".into()), @@ -144,7 +162,8 @@ where #[precompile::public("getHotkey(uint16,uint16)")] #[precompile::view] - fn get_hotkey(_: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + fn get_hotkey(handle: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + handle.record_db_reads::(1)?; pallet_subtensor::Pallet::::get_hotkey_for_net_and_uid(netuid.into(), uid) .map(|acc| H256::from_slice(acc.as_slice())) .map_err(|_| PrecompileFailure::Error { @@ -154,7 +173,9 @@ where #[precompile::public("getColdkey(uint16,uint16)")] #[precompile::view] - fn get_coldkey(_: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + fn get_coldkey(handle: &mut impl PrecompileHandle, netuid: u16, uid: u16) -> EvmResult { + // Keys + Owner reads + handle.record_db_reads::(2)?; let hotkey = pallet_subtensor::Pallet::::get_hotkey_for_net_and_uid(netuid.into(), uid) .map_err(|_| PrecompileFailure::Error { exit_status: ExitError::InvalidRange, diff --git a/precompiles/src/mock.rs b/precompiles/src/mock.rs index 037e02d864..cb7275e47c 100644 --- a/precompiles/src/mock.rs +++ b/precompiles/src/mock.rs @@ -7,10 +7,10 @@ use core::{marker::PhantomData, num::NonZeroU64}; use fp_evm::{Context, PrecompileResult}; use frame_support::{ PalletId, derive_impl, parameter_types, - traits::{Everything, InherentBuilder, PrivilegeCmp}, + traits::{Everything, PrivilegeCmp}, weights::Weight, }; -use frame_system::{EnsureRoot, limits, offchain::CreateTransactionBase}; +use frame_system::{EnsureRoot, limits}; use pallet_evm::{ AddressMapping, BalanceConverter, EnsureAddressNever, EnsureAddressRoot, EvmBalance, PrecompileHandle, PrecompileSet, SubstrateBalance, @@ -22,7 +22,7 @@ use sp_runtime::{ testing::TestXt, traits::{BlakeTwo256, ConstU32, IdentityLookup}, }; -use substrate_fixed::types::U96F32; +use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AuthorshipInfo, NetUid, ProxyType, TaoBalance}; use crate::PrecompileExt; @@ -74,7 +74,6 @@ parameter_types! { pub const MaxContributors: u32 = 10; pub const SwapProtocolId: PalletId = PalletId(*b"ten/swap"); pub const SwapMaxFeeRate: u16 = 10000; - pub const SwapMaxPositions: u32 = 100; pub const SwapMinimumLiquidity: u64 = 1_000; pub const SwapMinimumReserve: NonZeroU64 = NonZeroU64::new(1_000_000).unwrap(); pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * @@ -117,6 +116,10 @@ parameter_types! { pub const InitialMaxBurn: TaoBalance = TaoBalance::new(1_000_000_000); pub const MinBurnUpperBound: TaoBalance = TaoBalance::new(1_000_000_000); pub const MaxBurnLowerBound: TaoBalance = TaoBalance::new(100_000_000); + pub const MinTempo: u16 = pallet_subtensor::MIN_TEMPO; + pub const MaxTempo: u16 = pallet_subtensor::MAX_TEMPO; + pub const MinActivityCutoffFactorMilli: u32 = pallet_subtensor::MIN_ACTIVITY_CUTOFF_FACTOR_MILLI; + pub const MaxActivityCutoffFactorMilli: u32 = pallet_subtensor::MAX_ACTIVITY_CUTOFF_FACTOR_MILLI; pub const InitialValidatorPruneLen: u64 = 0; pub const InitialScalingLawPower: u16 = 50; pub const InitialMaxAllowedValidators: u16 = 100; @@ -154,6 +157,7 @@ parameter_types! { pub const EvmKeyAssociateRateLimit: u64 = 0; pub const SubtensorPalletId: PalletId = PalletId(*b"subtensr"); pub const BurnAccountId: PalletId = PalletId(*b"burntnsr"); + pub const MaxEpochsPerBlock: u32 = 32; } #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] @@ -286,7 +290,6 @@ impl pallet_subtensor_swap::Config for Runtime { type TaoReserve = pallet_subtensor::TaoBalanceReserve; type AlphaReserve = pallet_subtensor::AlphaBalanceReserve; type MaxFeeRate = SwapMaxFeeRate; - type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; type MinimumReserve = SwapMinimumReserve; type WeightInfo = (); @@ -370,7 +373,7 @@ impl frame_system::offchain::SigningTypes for Runtime { type Signature = test_crypto::Signature; } -impl CreateTransactionBase for Runtime +impl frame_system::offchain::CreateTransactionBase for Runtime where RuntimeCall: From, { @@ -378,28 +381,12 @@ where type RuntimeCall = RuntimeCall; } -impl frame_system::offchain::CreateInherent for Runtime +impl frame_system::offchain::CreateBare for Runtime where RuntimeCall: From, { fn create_bare(call: Self::RuntimeCall) -> Self::Extrinsic { - UncheckedExtrinsic::new_inherent(call) - } -} - -impl frame_system::offchain::CreateSignedTransaction for Runtime -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, (), ())) + UncheckedExtrinsic::new_bare(call) } } @@ -461,6 +448,10 @@ impl pallet_subtensor::Config for Runtime { type InitialMinStake = InitialMinStake; type MinBurnUpperBound = MinBurnUpperBound; type MaxBurnLowerBound = MaxBurnLowerBound; + type MinTempo = MinTempo; + type MaxTempo = MaxTempo; + type MinActivityCutoffFactorMilli = MinActivityCutoffFactorMilli; + type MaxActivityCutoffFactorMilli = MaxActivityCutoffFactorMilli; type InitialRAORecycledForRegistration = InitialRAORecycledForRegistration; type InitialNetworkImmunityPeriod = InitialNetworkImmunityPeriod; type InitialNetworkMinLockCost = InitialNetworkMinLockCost; @@ -492,6 +483,7 @@ impl pallet_subtensor::Config for Runtime { type AuthorshipProvider = MockAuthorshipProvider; type SubtensorPalletId = SubtensorPalletId; type BurnAccountId = BurnAccountId; + type MaxEpochsPerBlock = MaxEpochsPerBlock; type WeightInfo = (); } @@ -622,8 +614,8 @@ pub(crate) fn selector_u32(signature: &str) -> u32 { u32::from_be_bytes([hash[0], hash[1], hash[2], hash[3]]) } -pub(crate) fn alpha_price_to_evm(price: U96F32) -> U256 { - let scaled_price = (price * U96F32::from_num(EVM_DECIMALS_FACTOR)).to_num::(); +pub(crate) fn alpha_price_to_evm(price: U64F64) -> U256 { + let scaled_price = (price * U64F64::from_num(EVM_DECIMALS_FACTOR)).to_num::(); ::BalanceConverter::into_evm_balance(scaled_price.into()) .expect("runtime balance conversion should work for alpha price") .into_u256() diff --git a/precompiles/src/neuron.rs b/precompiles/src/neuron.rs index 1397baf272..b0dd1ea720 100644 --- a/precompiles/src/neuron.rs +++ b/precompiles/src/neuron.rs @@ -260,8 +260,8 @@ mod tests { use super::*; use crate::PrecompileExt; use crate::mock::{ - AccountId, Runtime, System, addr_from_index, execute_precompile, mapped_account, - new_test_ext, precompiles, selector_u32, + AccountId, Runtime, addr_from_index, execute_precompile, mapped_account, new_test_ext, + precompiles, selector_u32, }; use precompile_utils::solidity::encode_with_selector; use precompile_utils::testing::PrecompileTesterExt; @@ -303,7 +303,7 @@ mod tests { pallet_subtensor::Pallet::::set_burn(netuid, REGISTRATION_BURN.into()); pallet_subtensor::Pallet::::set_max_allowed_uids(netuid, 4096); pallet_subtensor::Pallet::::set_weights_set_rate_limit(netuid, 0); - pallet_subtensor::Pallet::::set_tempo(netuid, TEMPO); + pallet_subtensor::Pallet::::set_tempo_unchecked(netuid, TEMPO); pallet_subtensor::Pallet::::set_commit_reveal_weights_enabled(netuid, true); pallet_subtensor::Pallet::::set_reveal_period(netuid, REVEAL_PERIOD) .expect("reveal period setup should succeed"); @@ -455,15 +455,21 @@ mod tests { &caller_account, ) .expect("weight commit should exist before reveal"); - let (_, _, first_reveal_block, _) = commits + // CR-v2 tuple layout: (hash, commit_epoch, commit_block, _unused). + let (_, commit_epoch, _, _) = commits .front() .copied() .expect("weight commit queue should contain the committed hash"); - System::set_block_number(u64::from( - u32::try_from(first_reveal_block) - .expect("first reveal block should fit in runtime block number"), - )); + // Put the subnet into the exact epoch in which the commit is revealable: + // `current_epoch == commit_epoch + reveal_period`. Pin `LastEpochBlock` and + // `PendingEpochAt` so `should_run_epoch` is false and the look-ahead does + // not advance past the reveal epoch. + let reveal_epoch = commit_epoch.saturating_add(REVEAL_PERIOD); + pallet_subtensor::SubnetEpochIndex::::insert(netuid, reveal_epoch); + let cur_block = pallet_subtensor::Pallet::::get_current_block_as_u64(); + pallet_subtensor::LastEpochBlock::::insert(netuid, cur_block); + pallet_subtensor::PendingEpochAt::::insert(netuid, 0u64); pallet_subtensor::Pallet::::set_stake_threshold(1); let rejected = execute_precompile( diff --git a/precompiles/src/proxy.rs b/precompiles/src/proxy.rs index 3312b67194..78d59f5ce2 100644 --- a/precompiles/src/proxy.rs +++ b/precompiles/src/proxy.rs @@ -268,9 +268,10 @@ where #[precompile::public("getProxies(bytes32)")] #[precompile::view] pub fn get_proxies( - _handle: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, account_id: H256, ) -> EvmResult> { + handle.record_db_reads::(1)?; let account_id = R::AccountId::from(account_id.0.into()); let proxies = pallet_proxy::pallet::Pallet::::proxies(account_id); diff --git a/precompiles/src/solidity/subnet.abi b/precompiles/src/solidity/subnet.abi index 4531f59246..60e8b49906 100644 --- a/precompiles/src/solidity/subnet.abi +++ b/precompiles/src/solidity/subnet.abi @@ -18,6 +18,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "netuid", + "type": "uint16" + } + ], + "name": "getActivityCutoffFactor", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -592,6 +611,24 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "netuid", + "type": "uint16" + }, + { + "internalType": "uint32", + "name": "factorMilli", + "type": "uint32" + } + ], + "name": "setActivityCutoffFactor", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { @@ -1028,8 +1065,5 @@ "outputs": [], "stateMutability": "payable", "type": "function" - }, - { - "inputs" } ] diff --git a/precompiles/src/solidity/subnet.sol b/precompiles/src/solidity/subnet.sol index 4e78708d62..c454781cb5 100644 --- a/precompiles/src/solidity/subnet.sol +++ b/precompiles/src/solidity/subnet.sol @@ -113,6 +113,15 @@ interface ISubnet { uint16 activityCutoff ) external payable; + function getActivityCutoffFactor( + uint16 netuid + ) external view returns (uint32); + + function setActivityCutoffFactor( + uint16 netuid, + uint32 factorMilli + ) external payable; + function getNetworkRegistrationAllowed( uint16 netuid ) external view returns (bool); diff --git a/precompiles/src/staking.rs b/precompiles/src/staking.rs index 28e043f07b..554115ddf0 100644 --- a/precompiles/src/staking.rs +++ b/precompiles/src/staking.rs @@ -43,7 +43,7 @@ use pallet_evm::{ }; use pallet_subtensor_proxy as pallet_proxy; use precompile_utils::EvmResult; -use precompile_utils::prelude::{Address, RuntimeHelper, revert}; +use precompile_utils::prelude::{Address, revert}; use sp_core::{H160, H256, U256}; use sp_runtime::traits::{AsSystemOriginSigner, Dispatchable, StaticLookup, UniqueSaturatedInto}; use sp_std::vec; @@ -296,9 +296,11 @@ where #[precompile::public("getTotalColdkeyStake(bytes32)")] #[precompile::view] fn get_total_coldkey_stake( - _handle: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, coldkey: H256, ) -> EvmResult { + // StakingHotkeys + per-hotkey stake reads + handle.record_db_reads::(2)?; let coldkey = R::AccountId::from(coldkey.0); let stake = pallet_subtensor::Pallet::::get_total_stake_for_coldkey(&coldkey); @@ -307,10 +309,9 @@ where #[precompile::public("getTotalHotkeyStake(bytes32)")] #[precompile::view] - fn get_total_hotkey_stake( - _handle: &mut impl PrecompileHandle, - hotkey: H256, - ) -> EvmResult { + fn get_total_hotkey_stake(handle: &mut impl PrecompileHandle, hotkey: H256) -> EvmResult { + // Per-subnet stake + alpha price reads + handle.record_db_reads::(2)?; let hotkey = R::AccountId::from(hotkey.0); let stake = pallet_subtensor::Pallet::::get_total_stake_for_hotkey(&hotkey); @@ -320,11 +321,13 @@ where #[precompile::public("getStake(bytes32,bytes32,uint256)")] #[precompile::view] fn get_stake( - _: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, hotkey: H256, coldkey: H256, netuid: U256, ) -> EvmResult { + // Alpha share pool reads + handle.record_db_reads::(2)?; let hotkey = R::AccountId::from(hotkey.0); let coldkey = R::AccountId::from(coldkey.0); let netuid = try_u16_from_u256(netuid)?; @@ -340,7 +343,7 @@ where #[precompile::public("getAlphaStakedValidators(bytes32,uint256)")] #[precompile::view] fn get_alpha_staked_validators( - _handle: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, hotkey: H256, netuid: U256, ) -> EvmResult> { @@ -350,6 +353,7 @@ where for (coldkey, netuid_in_alpha, _) in pallet_subtensor::Pallet::::alpha_iter_single_prefix(&hotkey) { + handle.record_db_reads::(1)?; if netuid == netuid_in_alpha { let key: [u8; 32] = coldkey.into(); coldkeys.push(key.into()); @@ -362,10 +366,11 @@ where #[precompile::public("getTotalAlphaStaked(bytes32,uint256)")] #[precompile::view] fn get_total_alpha_staked( - _handle: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, hotkey: H256, netuid: U256, ) -> EvmResult { + handle.record_db_reads::(2)?; let hotkey = R::AccountId::from(hotkey.0); let netuid = try_u16_from_u256(netuid)?; let stake = @@ -376,7 +381,9 @@ where #[precompile::public("getNominatorMinRequiredStake()")] #[precompile::view] - fn get_nominator_min_required_stake(_handle: &mut impl PrecompileHandle) -> EvmResult { + fn get_nominator_min_required_stake(handle: &mut impl PrecompileHandle) -> EvmResult { + // NominatorMinRequiredStake + DefaultMinStake reads + handle.record_db_reads::(2)?; let stake = pallet_subtensor::Pallet::::get_nominator_min_required_stake(); Ok(stake.into()) @@ -467,10 +474,12 @@ where #[precompile::public("getTotalColdkeyStakeOnSubnet(bytes32,uint256)")] #[precompile::view] fn get_total_coldkey_stake_on_subnet( - _handle: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, coldkey: H256, netuid: U256, ) -> EvmResult { + // StakingHotkeys + per-hotkey stake reads + handle.record_db_reads::(2)?; let coldkey = R::AccountId::from(coldkey.0); let netuid = try_u16_from_u256(netuid)?; let stake = pallet_subtensor::Pallet::::get_total_stake_for_coldkey_on_subnet( @@ -496,8 +505,8 @@ where amount_alpha: U256, ) -> EvmResult<()> { // AllowancesStorage write + RegisteredSubnetCounter read - handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; - handle.record_cost(RuntimeHelper::::db_write_gas_cost())?; + handle.record_db_reads::(1)?; + handle.record_db_writes::(1)?; let approver = handle.context().caller; let spender = spender_address.0; @@ -522,8 +531,7 @@ where origin_netuid: U256, ) -> EvmResult { // AllowancesStorage read + RegisteredSubnetCounter read - handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; - handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; + handle.record_db_reads::(2)?; let spender = spender_address.0; let netuid = try_u16_from_u256(origin_netuid)?; @@ -547,9 +555,8 @@ where } // AllowancesStorage read + write + RegisteredSubnetCounter read - handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; - handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; - handle.record_cost(RuntimeHelper::::db_write_gas_cost())?; + handle.record_db_reads::(2)?; + handle.record_db_writes::(1)?; let approver = handle.context().caller; let spender = spender_address.0; @@ -578,9 +585,8 @@ where } // AllowancesStorage read + write + RegisteredSubnetCounter read - handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; - handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; - handle.record_cost(RuntimeHelper::::db_write_gas_cost())?; + handle.record_db_reads::(2)?; + handle.record_db_writes::(1)?; let approver = handle.context().caller; let spender = spender_address.0; @@ -613,9 +619,8 @@ where } // AllowancesStorage read + write + RegisteredSubnetCounter read - handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; - handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; - handle.record_cost(RuntimeHelper::::db_write_gas_cost())?; + handle.record_db_reads::(2)?; + handle.record_db_writes::(1)?; let counter = Self::current_subnet_counter(netuid); let approval_key = (spender, netuid, counter); @@ -780,9 +785,11 @@ where #[precompile::public("getTotalColdkeyStake(bytes32)")] #[precompile::view] fn get_total_coldkey_stake( - _handle: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, coldkey: H256, ) -> EvmResult { + // StakingHotkeys + per-hotkey stake reads + handle.record_db_reads::(2)?; let coldkey = R::AccountId::from(coldkey.0); // get total stake of coldkey @@ -799,10 +806,9 @@ where #[precompile::public("getTotalHotkeyStake(bytes32)")] #[precompile::view] - fn get_total_hotkey_stake( - _handle: &mut impl PrecompileHandle, - hotkey: H256, - ) -> EvmResult { + fn get_total_hotkey_stake(handle: &mut impl PrecompileHandle, hotkey: H256) -> EvmResult { + // Per-subnet stake + alpha price reads + handle.record_db_reads::(2)?; let hotkey = R::AccountId::from(hotkey.0); // get total stake of hotkey @@ -820,11 +826,13 @@ where #[precompile::public("getStake(bytes32,bytes32,uint256)")] #[precompile::view] fn get_stake( - _: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, hotkey: H256, coldkey: H256, netuid: U256, ) -> EvmResult { + // Alpha share pool reads + handle.record_db_reads::(2)?; let hotkey = R::AccountId::from(hotkey.0); let coldkey = R::AccountId::from(coldkey.0); let netuid = try_u16_from_u256(netuid)?; @@ -1070,11 +1078,6 @@ mod tests { fund_account(&source_account, COLDKEY_BALANCE); add_stake_v2(source, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); - pallet_subtensor::StakingOperationRateLimiter::::remove(( - hotkey.clone(), - source_account.clone(), - netuid, - )); ( netuid, @@ -1269,11 +1272,6 @@ mod tests { fund_account(&caller_account, COLDKEY_BALANCE); add_stake_v1(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); - pallet_subtensor::StakingOperationRateLimiter::::remove(( - hotkey.clone(), - caller_account.clone(), - netuid, - )); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompile::::INDEX); @@ -1309,11 +1307,6 @@ mod tests { fund_account(&caller_account, COLDKEY_BALANCE); add_stake_v2(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO); - pallet_subtensor::StakingOperationRateLimiter::::remove(( - hotkey.clone(), - caller_account.clone(), - netuid, - )); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(StakingPrecompileV2::::INDEX); @@ -1402,11 +1395,6 @@ mod tests { ), ) .execute_returns(()); - pallet_subtensor::StakingOperationRateLimiter::::remove(( - hotkey.clone(), - caller_account.clone(), - netuid, - )); let stake_before = stake_for(&hotkey, &caller_account, netuid); precompiles @@ -1458,11 +1446,6 @@ mod tests { ), ) .execute_returns(()); - pallet_subtensor::StakingOperationRateLimiter::::remove(( - hotkey.clone(), - caller_account.clone(), - netuid, - )); assert!(stake_for(&hotkey, &caller_account, netuid) > 0); precompiles @@ -1512,11 +1495,6 @@ mod tests { ), ) .execute_returns(()); - pallet_subtensor::StakingOperationRateLimiter::::remove(( - hotkey.clone(), - caller_account.clone(), - netuid, - )); assert!(stake_for(&hotkey, &caller_account, netuid) > 0); precompiles diff --git a/precompiles/src/subnet.rs b/precompiles/src/subnet.rs index b89d972eea..9992bd1cf3 100644 --- a/precompiles/src/subnet.rs +++ b/precompiles/src/subnet.rs @@ -161,9 +161,22 @@ where ) } + #[precompile::public("getNetworkRegistrationBlock(uint16)")] + #[precompile::view] + fn get_network_registration_block( + handle: &mut impl PrecompileHandle, + netuid: u16, + ) -> EvmResult { + handle.record_db_reads::(1)?; + Ok(pallet_subtensor::NetworkRegisteredAt::::get( + NetUid::from(netuid), + )) + } + #[precompile::public("getServingRateLimit(uint16)")] #[precompile::view] - fn get_serving_rate_limit(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_serving_rate_limit(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::ServingRateLimit::::get(NetUid::from( netuid, ))) @@ -189,7 +202,8 @@ where #[precompile::public("getMinDifficulty(uint16)")] #[precompile::view] - fn get_min_difficulty(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_min_difficulty(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::MinDifficulty::::get(NetUid::from( netuid, ))) @@ -215,7 +229,8 @@ where #[precompile::public("getMaxDifficulty(uint16)")] #[precompile::view] - fn get_max_difficulty(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_max_difficulty(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::MaxDifficulty::::get(NetUid::from( netuid, ))) @@ -241,7 +256,8 @@ where #[precompile::public("getWeightsVersionKey(uint16)")] #[precompile::view] - fn get_weights_version_key(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_weights_version_key(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::WeightsVersionKey::::get(NetUid::from( netuid, ))) @@ -267,7 +283,11 @@ where #[precompile::public("getWeightsSetRateLimit(uint16)")] #[precompile::view] - fn get_weights_set_rate_limit(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_weights_set_rate_limit( + handle: &mut impl PrecompileHandle, + netuid: u16, + ) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::WeightsSetRateLimit::::get( NetUid::from(netuid), )) @@ -286,7 +306,8 @@ where #[precompile::public("getAdjustmentAlpha(uint16)")] #[precompile::view] - fn get_adjustment_alpha(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_adjustment_alpha(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::AdjustmentAlpha::::get(NetUid::from( netuid, ))) @@ -320,7 +341,8 @@ where #[precompile::public("getImmunityPeriod(uint16)")] #[precompile::view] - fn get_immunity_period(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_immunity_period(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::ImmunityPeriod::::get(NetUid::from( netuid, ))) @@ -346,7 +368,8 @@ where #[precompile::public("getMinAllowedWeights(uint16)")] #[precompile::view] - fn get_min_allowed_weights(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_min_allowed_weights(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::MinAllowedWeights::::get(NetUid::from( netuid, ))) @@ -372,7 +395,8 @@ where #[precompile::public("getKappa(uint16)")] #[precompile::view] - fn get_kappa(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_kappa(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::Kappa::::get(NetUid::from(netuid))) } @@ -392,13 +416,18 @@ where #[precompile::public("getRho(uint16)")] #[precompile::view] - fn get_rho(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_rho(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::Rho::::get(NetUid::from(netuid))) } #[precompile::public("getAlphaSigmoidSteepness(uint16)")] #[precompile::view] - fn get_alpha_sigmoid_steepness(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_alpha_sigmoid_steepness( + handle: &mut impl PrecompileHandle, + netuid: u16, + ) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::AlphaSigmoidSteepness::::get(NetUid::from(netuid)) as u16) } @@ -436,7 +465,8 @@ where #[precompile::public("getActivityCutoff(uint16)")] #[precompile::view] - fn get_activity_cutoff(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_activity_cutoff(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::ActivityCutoff::::get(NetUid::from( netuid, ))) @@ -460,12 +490,39 @@ where ) } + #[precompile::public("getActivityCutoffFactor(uint16)")] + #[precompile::view] + fn get_activity_cutoff_factor(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + Ok(pallet_subtensor::ActivityCutoffFactorMilli::::get( + NetUid::from(netuid), + )) + } + + #[precompile::public("setActivityCutoffFactor(uint16,uint32)")] + #[precompile::payable] + fn set_activity_cutoff_factor( + handle: &mut impl PrecompileHandle, + netuid: u16, + factor_milli: u32, + ) -> EvmResult<()> { + let call = pallet_subtensor::Call::::set_activity_cutoff_factor { + netuid: netuid.into(), + factor_milli, + }; + + handle.try_dispatch_runtime_call::( + call, + RawOrigin::Signed(handle.caller_account_id::()), + ) + } + #[precompile::public("getNetworkRegistrationAllowed(uint16)")] #[precompile::view] fn get_network_registration_allowed( - _: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, netuid: u16, ) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::NetworkRegistrationAllowed::::get( NetUid::from(netuid), )) @@ -492,9 +549,10 @@ where #[precompile::public("getNetworkPowRegistrationAllowed(uint16)")] #[precompile::view] fn get_network_pow_registration_allowed( - _: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, netuid: u16, ) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::NetworkPowRegistrationAllowed::::get( NetUid::from(netuid), )) @@ -520,7 +578,8 @@ where #[precompile::public("getMinBurn(uint16)")] #[precompile::view] - fn get_min_burn(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_min_burn(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::MinBurn::::get(NetUid::from(netuid)).to_u64()) } @@ -537,7 +596,8 @@ where #[precompile::public("getMaxBurn(uint16)")] #[precompile::view] - fn get_max_burn(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_max_burn(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::MaxBurn::::get(NetUid::from(netuid)).to_u64()) } @@ -554,7 +614,8 @@ where #[precompile::public("getDifficulty(uint16)")] #[precompile::view] - fn get_difficulty(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_difficulty(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::Difficulty::::get(NetUid::from(netuid))) } @@ -578,7 +639,8 @@ where #[precompile::public("getBondsMovingAverage(uint16)")] #[precompile::view] - fn get_bonds_moving_average(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_bonds_moving_average(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::BondsMovingAverage::::get( NetUid::from(netuid), )) @@ -605,9 +667,10 @@ where #[precompile::public("getCommitRevealWeightsEnabled(uint16)")] #[precompile::view] fn get_commit_reveal_weights_enabled( - _: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, netuid: u16, ) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::CommitRevealWeightsEnabled::::get( NetUid::from(netuid), )) @@ -633,7 +696,11 @@ where #[precompile::public("getLiquidAlphaEnabled(uint16)")] #[precompile::view] - fn get_liquid_alpha_enabled(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_liquid_alpha_enabled( + handle: &mut impl PrecompileHandle, + netuid: u16, + ) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::LiquidAlphaOn::::get(NetUid::from( netuid, ))) @@ -659,13 +726,15 @@ where #[precompile::public("getYuma3Enabled(uint16)")] #[precompile::view] - fn get_yuma3_enabled(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_yuma3_enabled(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::Yuma3On::::get(NetUid::from(netuid))) } #[precompile::public("getBondsResetEnabled(uint16)")] #[precompile::view] - fn get_bonds_reset_enabled(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_bonds_reset_enabled(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::BondsResetOn::::get(NetUid::from( netuid, ))) @@ -709,7 +778,8 @@ where #[precompile::public("getAlphaValues(uint16)")] #[precompile::view] - fn get_alpha_values(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult<(u16, u16)> { + fn get_alpha_values(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult<(u16, u16)> { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::AlphaValues::::get(NetUid::from( netuid, ))) @@ -738,9 +808,10 @@ where #[precompile::public("getCommitRevealWeightsInterval(uint16)")] #[precompile::view] fn get_commit_reveal_weights_interval( - _: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, netuid: u16, ) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::RevealPeriodEpochs::::get( NetUid::from(netuid), )) @@ -1111,6 +1182,32 @@ mod tests { U256::from(activity_cutoff), ); + let factor_milli: u32 = 1_500; + precompiles + .prepare_test( + caller, + precompile_addr, + encode_with_selector( + selector_u32("setActivityCutoffFactor(uint16,uint32)"), + (TEST_NETUID_U16, factor_milli), + ), + ) + .execute_returns(()); + assert_eq!( + pallet_subtensor::ActivityCutoffFactorMilli::::get(netuid), + factor_milli + ); + assert_static_call( + &precompiles, + caller, + precompile_addr, + encode_with_selector( + selector_u32("getActivityCutoffFactor(uint16)"), + (TEST_NETUID_U16,), + ), + U256::from(factor_milli), + ); + precompiles .prepare_test( caller, @@ -1225,4 +1322,28 @@ mod tests { ); }); } + + #[test] + fn subnet_precompile_gets_network_registered_block() { + new_test_ext().execute_with(|| { + let caller = addr_from_index(0x5003); + let netuid = setup_owner_subnet(caller); + let precompiles = precompiles::>(); + let precompile_addr = addr_from_index(SubnetPrecompile::::INDEX); + + let registration_block: u64 = 42; + pallet_subtensor::NetworkRegisteredAt::::insert(netuid, registration_block); + + assert_static_call( + &precompiles, + caller, + precompile_addr, + encode_with_selector( + selector_u32("getNetworkRegistrationBlock(uint16)"), + (TEST_NETUID_U16,), + ), + U256::from(registration_block), + ); + }); + } } diff --git a/precompiles/src/uid_lookup.rs b/precompiles/src/uid_lookup.rs index 5d87973368..dc65501ba1 100644 --- a/precompiles/src/uid_lookup.rs +++ b/precompiles/src/uid_lookup.rs @@ -6,7 +6,7 @@ use precompile_utils::{EvmResult, prelude::Address}; use sp_runtime::traits::{Dispatchable, StaticLookup}; use sp_std::vec::Vec; -use crate::PrecompileExt; +use crate::{PrecompileExt, PrecompileHandleExt}; pub struct UidLookupPrecompile(PhantomData); @@ -39,11 +39,12 @@ where #[precompile::public("uidLookup(uint16,address,uint16)")] #[precompile::view] fn uid_lookup( - _handle: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, netuid: u16, evm_address: Address, limit: u16, ) -> EvmResult> { + handle.record_db_reads::(u64::from(limit))?; Ok(pallet_subtensor::Pallet::::uid_lookup( netuid.into(), evm_address.0, diff --git a/precompiles/src/voting_power.rs b/precompiles/src/voting_power.rs index af7896dac1..4cad7fcb89 100644 --- a/precompiles/src/voting_power.rs +++ b/precompiles/src/voting_power.rs @@ -6,6 +6,7 @@ use sp_core::{ByteArray, H256, U256}; use subtensor_runtime_common::NetUid; use crate::PrecompileExt; +use crate::PrecompileHandleExt; /// VotingPower precompile for smart contract access to validator voting power. /// @@ -15,7 +16,7 @@ pub struct VotingPowerPrecompile(PhantomData); impl PrecompileExt for VotingPowerPrecompile where - R: frame_system::Config + pallet_subtensor::Config, + R: frame_system::Config + pallet_subtensor::Config + pallet_evm::Config, R::AccountId: From<[u8; 32]> + ByteArray, { const INDEX: u64 = 2061; @@ -24,7 +25,7 @@ where #[precompile_utils::precompile] impl VotingPowerPrecompile where - R: frame_system::Config + pallet_subtensor::Config, + R: frame_system::Config + pallet_subtensor::Config + pallet_evm::Config, R::AccountId: From<[u8; 32]>, { /// Get voting power for a hotkey on a subnet. @@ -44,10 +45,11 @@ where #[precompile::public("getVotingPower(uint16,bytes32)")] #[precompile::view] fn get_voting_power( - _: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, netuid: u16, hotkey: H256, ) -> EvmResult { + handle.record_db_reads::(1)?; let hotkey = R::AccountId::from(hotkey.0); let voting_power = pallet_subtensor::VotingPower::::get(NetUid::from(netuid), &hotkey); Ok(U256::from(voting_power)) @@ -63,9 +65,10 @@ where #[precompile::public("isVotingPowerTrackingEnabled(uint16)")] #[precompile::view] fn is_voting_power_tracking_enabled( - _: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, netuid: u16, ) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::VotingPowerTrackingEnabled::::get( NetUid::from(netuid), )) @@ -84,9 +87,10 @@ where #[precompile::public("getVotingPowerDisableAtBlock(uint16)")] #[precompile::view] fn get_voting_power_disable_at_block( - _: &mut impl PrecompileHandle, + handle: &mut impl PrecompileHandle, netuid: u16, ) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::VotingPowerDisableAtBlock::::get( NetUid::from(netuid), )) @@ -104,7 +108,11 @@ where /// * `u64` - The alpha value (with 18 decimal precision) #[precompile::public("getVotingPowerEmaAlpha(uint16)")] #[precompile::view] - fn get_voting_power_ema_alpha(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + fn get_voting_power_ema_alpha( + handle: &mut impl PrecompileHandle, + netuid: u16, + ) -> EvmResult { + handle.record_db_reads::(1)?; Ok(pallet_subtensor::VotingPowerEmaAlpha::::get( NetUid::from(netuid), )) @@ -122,10 +130,14 @@ where /// * `u256` - The total voting power across all validators #[precompile::public("getTotalVotingPower(uint16)")] #[precompile::view] - fn get_total_voting_power(_: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { - let total: u64 = pallet_subtensor::VotingPower::::iter_prefix(NetUid::from(netuid)) - .map(|(_, voting_power)| voting_power) - .fold(0u64, |acc, vp| acc.saturating_add(vp)); + fn get_total_voting_power(handle: &mut impl PrecompileHandle, netuid: u16) -> EvmResult { + let mut total: u64 = 0; + for (_, voting_power) in + pallet_subtensor::VotingPower::::iter_prefix(NetUid::from(netuid)) + { + handle.record_db_reads::(1)?; + total = total.saturating_add(voting_power); + } Ok(U256::from(total)) } } diff --git a/pallets/swap-interface/Cargo.toml b/primitives/swap-interface/Cargo.toml similarity index 81% rename from pallets/swap-interface/Cargo.toml rename to primitives/swap-interface/Cargo.toml index e4392c6d67..5d4020edc2 100644 --- a/pallets/swap-interface/Cargo.toml +++ b/primitives/swap-interface/Cargo.toml @@ -16,6 +16,10 @@ workspace = true [features] default = ["std"] +runtime-benchmarks = [ + "frame-support/runtime-benchmarks", + "subtensor-runtime-common/runtime-benchmarks", +] std = [ "codec/std", "frame-support/std", diff --git a/primitives/swap-interface/src/lib.rs b/primitives/swap-interface/src/lib.rs new file mode 100644 index 0000000000..9980604707 --- /dev/null +++ b/primitives/swap-interface/src/lib.rs @@ -0,0 +1,234 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(clippy::too_many_arguments)] +use core::ops::Neg; + +use frame_support::pallet_prelude::*; +use substrate_fixed::types::U64F64; +use subtensor_macros::freeze_struct; +use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token}; + +pub use order::*; + +mod order; + +pub trait SwapEngine: DefaultPriceLimit { + fn swap( + netuid: NetUid, + order: O, + price_limit: TaoBalance, + drop_fees: bool, + should_rollback: bool, + ) -> Result, DispatchError>; +} + +pub trait SwapHandler { + fn swap( + netuid: NetUid, + order: O, + price_limit: TaoBalance, + drop_fees: bool, + should_rollback: bool, + ) -> Result, DispatchError> + where + Self: SwapEngine; + fn sim_swap( + netuid: NetUid, + order: O, + ) -> Result, DispatchError> + where + Self: SwapEngine; + + fn approx_fee_amount(netuid: NetUid, amount: T) -> T; + fn current_alpha_price(netuid: NetUid) -> U64F64; + fn max_price() -> C; + fn min_price() -> C; + fn adjust_protocol_liquidity( + netuid: NetUid, + tao_delta: TaoBalance, + alpha_delta: AlphaBalance, + ) -> (TaoBalance, AlphaBalance); + fn clear_protocol_liquidity(netuid: NetUid) -> DispatchResult; + fn init_swap(netuid: NetUid, maybe_price: Option); + fn get_alpha_amount_for_tao(netuid: NetUid, tao_amount: TaoBalance) -> AlphaBalance; +} + +/// Combined swap + balance execution interface for limit orders. +/// +/// Wraps the complete buy/sell operation: AMM state update (via `SwapHandler`), +/// pool reserve accounting, and user balance changes (TAO free balance / +/// alpha staking). Implemented by `pallet_subtensor::Pallet` using +/// `stake_into_subnet` / `unstake_from_subnet`. +pub trait OrderSwapInterface { + /// Buy alpha with TAO: debit `tao_amount` from `coldkey`'s free balance, + /// credit resulting alpha as stake at `hotkey` on `netuid`. + /// + /// When `validate` is `true` the implementation enforces subnet + /// existence, hotkey registration, minimum stake amount, sufficient + /// coldkey balance, and sets the staking rate-limit flag for `(hotkey, + /// coldkey, netuid)` after a successful stake. Pass `false` for internal + /// pallet-intermediary swaps that must bypass these user-facing guards. + /// Buy alpha with TAO: debit `tao_amount` from `coldkey`'s free balance, + /// credit resulting alpha as stake at `hotkey` on `netuid`. + /// + /// **Implementations MUST be transactional** (wrap in + /// `frame_support::storage::with_transaction` or annotate with + /// `#[frame_support::transactional]`). The implementation debits the + /// caller's balance before the pool swap; if the swap fails the debit + /// must be rolled back to leave the caller's state unchanged. + fn buy_alpha( + coldkey: &AccountId, + hotkey: &AccountId, + netuid: NetUid, + tao_amount: TaoBalance, + limit_price: TaoBalance, + validate: bool, + ) -> Result; + + /// Sell alpha for TAO: remove `alpha_amount` from `coldkey`'s stake at + /// `hotkey` on `netuid`, credit resulting TAO to `coldkey`'s free balance. + /// + /// When `validate` is `true` the implementation enforces subnet + /// existence, hotkey registration, minimum stake amount, sufficient alpha + /// balance, and checks that the staking rate-limit flag is not set for + /// `(hotkey, coldkey, netuid)` (i.e. the account did not stake this + /// block). Pass `false` for internal pallet-intermediary swaps. + /// Sell alpha for TAO: remove `alpha_amount` from `coldkey`'s stake at + /// `hotkey` on `netuid`, credit resulting TAO to `coldkey`'s free balance. + /// + /// **Implementations MUST be transactional** (wrap in + /// `frame_support::storage::with_transaction` or annotate with + /// `#[frame_support::transactional]`). The implementation decrements the + /// caller's stake before the pool swap; if the swap fails the decrement + /// must be rolled back to leave the caller's state unchanged. + fn sell_alpha( + coldkey: &AccountId, + hotkey: &AccountId, + netuid: NetUid, + alpha_amount: AlphaBalance, + limit_price: TaoBalance, + validate: bool, + ) -> Result; + + /// Current spot price: TAO per alpha, same scale as + /// `SwapHandler::current_alpha_price`. + fn current_alpha_price(netuid: NetUid) -> U64F64; + + /// Transfer `amount` TAO from `from`'s free balance to `to`'s free balance. + /// + /// Used by the batch executor to collect TAO from buy-order signers into + /// the pallet intermediary account and to distribute TAO to sell-order + /// signers after internal matching. + fn transfer_tao(from: &AccountId, to: &AccountId, amount: TaoBalance) -> DispatchResult; + + /// Move `amount` staked alpha directly between two (coldkey, hotkey) pairs + /// on `netuid` **without going through the AMM pool**. + /// + /// This is a pure stake-accounting transfer used for internal order + /// matching in `execute_batched_orders`: it lets the pallet collect alpha + /// from sell-order signers into its intermediary account, and later + /// distribute alpha to buy-order signers, all without touching the pool. + /// + /// When `validate_sender` is `true`, the sender side is validated before + /// the transfer: subnet existence, subtoken enabled, minimum stake amount, + /// and the staking rate-limit flag for `(from_hotkey, from_coldkey, + /// netuid)` is checked — the transfer is rejected if `from_coldkey` + /// already staked this block. + /// + /// When `validate_receiver` is `true`, the staking rate-limit flag for + /// `(to_hotkey, to_coldkey, netuid)` is set after the transfer, marking + /// that `to_coldkey` has received stake this block. + /// + /// The two flags are intentionally separate so that each call site can + /// opt into only the half it needs: + /// - Collecting alpha from users into the pallet intermediary: + /// `validate_sender: true, validate_receiver: false` — validates the + /// user but does not rate-limit the intermediary account. + /// - Distributing alpha from the pallet intermediary to buyers: + /// `validate_sender: false, validate_receiver: true` — skips checking + /// the intermediary (which would fail) and rate-limits the buyer. + fn transfer_staked_alpha( + from_coldkey: &AccountId, + from_hotkey: &AccountId, + to_coldkey: &AccountId, + to_hotkey: &AccountId, + netuid: NetUid, + amount: AlphaBalance, + validate_sender: bool, + validate_receiver: bool, + ) -> DispatchResult; + + /// Set up a subnet for benchmark execution. + /// + /// Called once per benchmark before any orders are built. Implementations + /// should initialise the subnet (registers it, enables the subtoken, seeds + /// pool reserves) so that price queries and swaps succeed. + /// The default is a no-op; override in runtime implementations. + #[cfg(feature = "runtime-benchmarks")] + fn set_up_netuid_for_benchmark(_netuid: NetUid) {} + + /// Register `hotkey` as owned by `coldkey`. + /// + /// Called during `on_genesis` and `on_runtime_upgrade` to claim ownership of + /// the pallet's hotkey before any external actor can register it. Safe to call + /// multiple times — is a no-op if the hotkey account already exists. + fn register_pallet_hotkey(coldkey: &AccountId, hotkey: &AccountId) -> DispatchResult; + + /// Returns `true` if `coldkey` is the registered owner of `hotkey`. + fn pallet_hotkey_registered(coldkey: &AccountId, hotkey: &AccountId) -> bool; + + /// Set up accounts for benchmark execution. + /// + /// Called once per order before the benchmarked extrinsic runs. Implementations + /// should fund `coldkey` with sufficient TAO (and alpha for sell orders) and + /// register `hotkey` on the relevant subnet so that swap operations succeed. + /// The default is a no-op; override in runtime implementations. + #[cfg(feature = "runtime-benchmarks")] + fn set_up_acc_for_benchmark(_hotkey: &AccountId, _coldkey: &AccountId) {} +} + +pub trait DefaultPriceLimit +where + PaidIn: Token, + PaidOut: Token, +{ + fn default_price_limit() -> C; +} + +/// Externally used swap result (for RPC) +#[freeze_struct("6a03533fc53ccfb8")] +#[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] +pub struct SwapResult +where + PaidIn: Token, + PaidOut: Token, +{ + pub amount_paid_in: PaidIn, + pub amount_paid_out: PaidOut, + pub fee_paid: PaidIn, + pub fee_to_block_author: PaidIn, +} + +impl SwapResult +where + PaidIn: Token, + PaidOut: Token, +{ + pub fn paid_in_reserve_delta(&self) -> i128 { + self.amount_paid_in.to_u64() as i128 + } + + pub fn paid_in_reserve_delta_i64(&self) -> i64 { + self.paid_in_reserve_delta() + .clamp(i64::MIN as i128, i64::MAX as i128) as i64 + } + + pub fn paid_out_reserve_delta(&self) -> i128 { + (self.amount_paid_out.to_u64() as i128).neg() + } + + pub fn paid_out_reserve_delta_i64(&self) -> i64 { + (self.amount_paid_out.to_u64() as i128) + .neg() + .clamp(i64::MIN as i128, i64::MAX as i128) as i64 + } +} diff --git a/pallets/swap-interface/src/order.rs b/primitives/swap-interface/src/order.rs similarity index 84% rename from pallets/swap-interface/src/order.rs rename to primitives/swap-interface/src/order.rs index b4075e9781..7b9970f123 100644 --- a/pallets/swap-interface/src/order.rs +++ b/primitives/swap-interface/src/order.rs @@ -11,7 +11,7 @@ pub trait Order: Clone { fn with_amount(amount: impl Into) -> Self; fn amount(&self) -> Self::PaidIn; - fn is_beyond_price_limit(&self, alpha_sqrt_price: U64F64, limit_sqrt_price: U64F64) -> bool; + fn is_beyond_price_limit(&self, current_price: U64F64, limit_price: U64F64) -> bool; } #[derive(Clone, Default)] @@ -45,8 +45,8 @@ where self.amount } - fn is_beyond_price_limit(&self, alpha_sqrt_price: U64F64, limit_sqrt_price: U64F64) -> bool { - alpha_sqrt_price < limit_sqrt_price + fn is_beyond_price_limit(&self, current_price: U64F64, limit_price: U64F64) -> bool { + current_price < limit_price } } @@ -81,7 +81,7 @@ where self.amount } - fn is_beyond_price_limit(&self, alpha_sqrt_price: U64F64, limit_sqrt_price: U64F64) -> bool { - alpha_sqrt_price > limit_sqrt_price + fn is_beyond_price_limit(&self, current_price: U64F64, limit_price: U64F64) -> bool { + current_price > limit_price } } diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 48269f5eb5..946e797f21 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -61,6 +61,7 @@ sp-authority-discovery.workspace = true subtensor-runtime-common.workspace = true subtensor-precompiles.workspace = true sp-weights.workspace = true +sp-io.workspace = true # Temporary sudo pallet-sudo.workspace = true @@ -88,9 +89,6 @@ pallet-transaction-payment-rpc-runtime-api.workspace = true frame-benchmarking = { workspace = true, optional = true } frame-system-benchmarking = { workspace = true, optional = true } -# Identity registry pallet for registering project info -pallet-registry.workspace = true - # Metadata commitment pallet pallet-commitments.workspace = true @@ -151,6 +149,9 @@ ark-serialize = { workspace = true, features = ["derive"] } # Crowdloan pallet-crowdloan.workspace = true +# Limit Orders +pallet-limit-orders.workspace = true + # Mev Shield pallet-shield.workspace = true stp-shield.workspace = true @@ -159,8 +160,8 @@ ethereum.workspace = true [dev-dependencies] frame-metadata.workspace = true -sp-io.workspace = true sp-tracing.workspace = true +sp-keyring.workspace = true precompile-utils = { workspace = true, features = ["testing"] } [build-dependencies] @@ -214,12 +215,12 @@ std = [ "sp-transaction-pool/std", "sp-version/std", "substrate-wasm-builder", - "pallet-registry/std", "pallet-admin-utils/std", "subtensor-custom-rpc-runtime-api/std", "subtensor-transaction-fee/std", "serde_json/std", "sp-io/std", + "sp-keyring/std", "sp-tracing/std", "log/std", "safe-math/std", @@ -227,6 +228,7 @@ std = [ "sp-genesis-builder/std", "subtensor-precompiles/std", "subtensor-runtime-common/std", + "pallet-limit-orders/std", "pallet-crowdloan/std", "pallet-babe/std", "pallet-session/std", @@ -296,7 +298,6 @@ runtime-benchmarks = [ "pallet-safe-mode/runtime-benchmarks", "pallet-subtensor/runtime-benchmarks", "pallet-subtensor-proxy/runtime-benchmarks", - "pallet-registry/runtime-benchmarks", "pallet-commitments/runtime-benchmarks", "pallet-admin-utils/runtime-benchmarks", "pallet-multisig/runtime-benchmarks", @@ -330,7 +331,9 @@ runtime-benchmarks = [ "pallet-shield/runtime-benchmarks", "subtensor-runtime-common/runtime-benchmarks", - "subtensor-chain-extensions/runtime-benchmarks" + "subtensor-chain-extensions/runtime-benchmarks", + "pallet-limit-orders/runtime-benchmarks", + "subtensor-swap-interface/runtime-benchmarks", ] try-runtime = [ "frame-try-runtime/try-runtime", @@ -354,7 +357,6 @@ try-runtime = [ "sp-runtime/try-runtime", "pallet-admin-utils/try-runtime", "pallet-commitments/try-runtime", - "pallet-registry/try-runtime", "pallet-crowdloan/try-runtime", "pallet-babe/try-runtime", "pallet-session/try-runtime", diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 32d629a761..f18065f75c 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -18,6 +18,7 @@ pub mod transaction_payment_wrapper; extern crate alloc; +use alloc::collections::BTreeMap; use codec::{Compact, Decode, Encode}; use ethereum::AuthorizationList; use frame_support::{ @@ -30,7 +31,6 @@ use frame_support::{ use frame_system::{EnsureRoot, EnsureRootWithSuccess, EnsureSigned}; use pallet_commitments::{CanCommit, OnMetadataCommitment}; use pallet_grandpa::{AuthorityId as GrandpaId, fg_primitives}; -use pallet_registry::CanRegisterIdentity; pub use pallet_shield; use pallet_subtensor::rpc_info::{ delegate_info::DelegateInfo, @@ -38,7 +38,7 @@ use pallet_subtensor::rpc_info::{ metagraph::{Metagraph, SelectiveMetagraph}, neuron_info::{NeuronInfo, NeuronInfoLite}, show_subnet::SubnetState, - stake_info::StakeInfo, + stake_info::{StakeAvailability, StakeInfo}, subnet_info::{ SubnetHyperparams, SubnetHyperparamsV2, SubnetHyperparamsV3, SubnetInfo, SubnetInfov2, }, @@ -57,13 +57,11 @@ use sp_core::{ H160, H256, OpaqueMetadata, U256, crypto::{ByteArray, KeyTypeId}, }; -use sp_runtime::Cow; -use sp_runtime::generic::Era; use sp_runtime::{ - AccountId32, ApplyExtrinsicResult, ConsensusEngineId, Percent, generic, impl_opaque_keys, + AccountId32, ApplyExtrinsicResult, ConsensusEngineId, Cow, Percent, generic, impl_opaque_keys, traits::{ - AccountIdLookup, BlakeTwo256, Block as BlockT, DispatchInfoOf, Dispatchable, One, - PostDispatchInfoOf, UniqueSaturatedInto, Verify, + AccountIdConversion, AccountIdLookup, BlakeTwo256, Block as BlockT, DispatchInfoOf, + Dispatchable, One, PostDispatchInfoOf, UniqueSaturatedInto, Verify, }, transaction_validity::{ TransactionPriority, TransactionSource, TransactionValidity, TransactionValidityError, @@ -75,7 +73,7 @@ use sp_std::prelude::*; use sp_version::NativeVersion; use sp_version::RuntimeVersion; use stp_shield::ShieldedTransaction; -use substrate_fixed::types::{U64F64, U96F32}; +use substrate_fixed::types::U64F64; use subtensor_precompiles::Precompiles; use subtensor_runtime_common::{AlphaBalance, AuthorshipInfo, TaoBalance, time::*, *}; use subtensor_swap_interface::{Order, SwapHandler}; @@ -185,47 +183,6 @@ impl frame_system::offchain::CreateBare> for Runtime } } -impl frame_system::offchain::CreateSignedTransaction> for Runtime { - fn create_signed_transaction< - S: frame_system::offchain::AppCrypto, - >( - call: RuntimeCall, - public: Self::Public, - account: Self::AccountId, - nonce: Self::Nonce, - ) -> Option { - use sp_runtime::traits::StaticLookup; - - let address = ::Lookup::unlookup(account.clone()); - let extra: TxExtension = ( - ( - frame_system::CheckNonZeroSender::::new(), - frame_system::CheckSpecVersion::::new(), - frame_system::CheckTxVersion::::new(), - frame_system::CheckGenesis::::new(), - check_mortality::CheckMortality::::from(Era::Immortal), - check_nonce::CheckNonce::::from(nonce).into(), - frame_system::CheckWeight::::new(), - ), - ( - ChargeTransactionPaymentWrapper::new(TaoBalance::new(0)), - SudoTransactionExtension::::new(), - pallet_shield::CheckShieldedTxValidity::::new(), - pallet_subtensor::SubtensorTransactionExtension::::new(), - pallet_drand::drand_priority::DrandPriority::::new(), - ), - frame_metadata_hash_extension::CheckMetadataHash::::new(true), - ); - - let raw_payload = SignedPayload::new(call.clone(), extra.clone()).ok()?; - let signature = raw_payload.using_encoded(|payload| S::sign(payload, public))?; - - Some(UncheckedExtrinsic::new_signed( - call, address, signature, extra, - )) - } -} - // Subtensor module pub use pallet_scheduler; pub use pallet_subtensor; @@ -277,7 +234,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 419, + spec_version: 420, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -915,43 +872,6 @@ impl pallet_preimage::Config for Runtime { >; } -pub struct AllowIdentityReg; - -impl CanRegisterIdentity for AllowIdentityReg { - #[cfg(not(feature = "runtime-benchmarks"))] - fn can_register(address: &AccountId, identified: &AccountId) -> bool { - if address != identified { - SubtensorModule::coldkey_owns_hotkey(address, identified) - && SubtensorModule::is_hotkey_registered_on_network(NetUid::ROOT, identified) - } else { - SubtensorModule::is_subnet_owner(address) - } - } - - #[cfg(feature = "runtime-benchmarks")] - fn can_register(_: &AccountId, _: &AccountId) -> bool { - true - } -} - -// Configure registry pallet. -parameter_types! { - pub const MaxAdditionalFields: u32 = 1; - pub const InitialDeposit: Balance = TaoBalance::new(100_000_000); // 0.1 TAO - pub const FieldDeposit: Balance = TaoBalance::new(100_000_000); // 0.1 TAO -} - -impl pallet_registry::Config for Runtime { - type RuntimeHoldReason = RuntimeHoldReason; - type Currency = Balances; - type CanRegister = AllowIdentityReg; - type WeightInfo = pallet_registry::weights::SubstrateWeight; - - type MaxAdditionalFields = MaxAdditionalFields; - type InitialDeposit = InitialDeposit; - type FieldDeposit = FieldDeposit; -} - parameter_types! { pub const MaxCommitFieldsInner: u32 = 3; pub const CommitmentInitialDeposit: Balance = TaoBalance::ZERO; // Free @@ -1076,6 +996,12 @@ parameter_types! { pub const SubtensorInitialMaxBurn: TaoBalance = TaoBalance::new(100_000_000_000); // 100 tao pub const MinBurnUpperBound: TaoBalance = TaoBalance::new(1_000_000_000); // 1 TAO pub const MaxBurnLowerBound: TaoBalance = TaoBalance::new(100_000_000); // 0.1 TAO + pub const SubtensorMinTempo: u16 = pallet_subtensor::MIN_TEMPO; + pub const SubtensorMaxTempo: u16 = pallet_subtensor::MAX_TEMPO; + pub const SubtensorMinActivityCutoffFactorMilli: u32 = + pallet_subtensor::MIN_ACTIVITY_CUTOFF_FACTOR_MILLI; + pub const SubtensorMaxActivityCutoffFactorMilli: u32 = + pallet_subtensor::MAX_ACTIVITY_CUTOFF_FACTOR_MILLI; pub const SubtensorInitialTxRateLimit: u64 = 1000; pub const SubtensorInitialTxDelegateTakeRateLimit: u64 = 216000; // 30 days at 12 seconds per block pub const SubtensorInitialTxChildKeyTakeRateLimit: u64 = INITIAL_CHILDKEY_TAKE_RATELIMIT; @@ -1106,6 +1032,7 @@ parameter_types! { pub const EvmKeyAssociateRateLimit: u64 = EVM_KEY_ASSOCIATE_RATELIMIT; pub const SubtensorPalletId: PalletId = PalletId(*b"subtensr"); pub const BurnAccountId: PalletId = PalletId(*b"burntnsr"); + pub const SubtensorMaxEpochsPerBlock: u32 = prod_or_fast!(2, 32); } impl pallet_subtensor::Config for Runtime { @@ -1150,6 +1077,10 @@ impl pallet_subtensor::Config for Runtime { type InitialMinStake = SubtensorInitialMinStake; type MinBurnUpperBound = MinBurnUpperBound; type MaxBurnLowerBound = MaxBurnLowerBound; + type MinTempo = SubtensorMinTempo; + type MaxTempo = SubtensorMaxTempo; + type MinActivityCutoffFactorMilli = SubtensorMinActivityCutoffFactorMilli; + type MaxActivityCutoffFactorMilli = SubtensorMaxActivityCutoffFactorMilli; type InitialTxRateLimit = SubtensorInitialTxRateLimit; type InitialTxDelegateTakeRateLimit = SubtensorInitialTxDelegateTakeRateLimit; type InitialTxChildKeyTakeRateLimit = SubtensorInitialTxChildKeyTakeRateLimit; @@ -1185,13 +1116,13 @@ impl pallet_subtensor::Config for Runtime { type AuthorshipProvider = BlockAuthorFromAura; type SubtensorPalletId = SubtensorPalletId; type BurnAccountId = BurnAccountId; + type MaxEpochsPerBlock = SubtensorMaxEpochsPerBlock; type WeightInfo = pallet_subtensor::weights::SubstrateWeight; } 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 = unsafe { NonZeroU64::new_unchecked(1_000_000) }; } @@ -1203,10 +1134,8 @@ impl pallet_subtensor_swap::Config for Runtime { type TaoReserve = pallet_subtensor::TaoBalanceReserve; type AlphaReserve = pallet_subtensor::AlphaBalanceReserve; type MaxFeeRate = SwapMaxFeeRate; - type MaxPositions = SwapMaxPositions; type MinimumLiquidity = SwapMinimumLiquidity; type MinimumReserve = SwapMinimumReserve; - // TODO: set measured weights when the pallet been benchmarked and the type is generated type WeightInfo = pallet_subtensor_swap::weights::SubstrateWeight; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = SwapBenchmarkHelper; @@ -1542,6 +1471,42 @@ impl pallet_crowdloan::Config for Runtime { type MaxContributors = MaxContributors; } +// Limit Orders +parameter_types! { + pub const LimitOrdersPalletId: PalletId = PalletId(*b"bt/limit"); + pub const LimitOrdersMaxOrdersPerBatch: u32 = 100; +} + +pub struct LimitOrdersPalletHotkey; +impl Get for LimitOrdersPalletHotkey { + fn get() -> AccountId { + PalletId(*b"bt/lmhky").into_account_truncating() + } +} + +#[cfg(feature = "runtime-benchmarks")] +pub struct LimitOrdersUnixTime; + +#[cfg(feature = "runtime-benchmarks")] +impl frame_support::traits::UnixTime for LimitOrdersUnixTime { + fn now() -> core::time::Duration { + core::time::Duration::from_millis(pallet_timestamp::Pallet::::get()) + } +} + +impl pallet_limit_orders::Config for Runtime { + type SwapInterface = SubtensorModule; + #[cfg(feature = "runtime-benchmarks")] + type TimeProvider = LimitOrdersUnixTime; + #[cfg(not(feature = "runtime-benchmarks"))] + type TimeProvider = Timestamp; + type MaxOrdersPerBatch = LimitOrdersMaxOrdersPerBatch; + type PalletId = LimitOrdersPalletId; + type PalletHotkey = LimitOrdersPalletHotkey; + type WeightInfo = pallet_limit_orders::weights::SubstrateWeight; + type ChainId = ConfigurableChainId; +} + fn contracts_schedule() -> pallet_contracts::Schedule { pallet_contracts::Schedule { limits: pallet_contracts::Limits { @@ -1646,7 +1611,7 @@ construct_runtime!( Preimage: pallet_preimage = 14, Scheduler: pallet_scheduler = 15, Proxy: pallet_proxy = 16, - Registry: pallet_registry = 17, + // pallet_registry was 17 Commitments: pallet_commitments = 18, AdminUtils: pallet_admin_utils = 19, SafeMode: pallet_safe_mode = 20, @@ -1664,6 +1629,7 @@ construct_runtime!( Contracts: pallet_contracts = 29, MevShield: pallet_shield = 30, AlphaAssets: pallet_alpha_assets = 31, + LimitOrders: pallet_limit_orders = 32, } ); @@ -1702,6 +1668,7 @@ type Migrations = ( pallet_subtensor::migrations::migrate_init_total_issuance::initialise_total_issuance::Migration< Runtime, >, + migrations::PalletRegistryCleanupMigration, ); // Unchecked extrinsic type as expected by this runtime. @@ -1739,7 +1706,6 @@ mod benches { [pallet_balances, Balances] [pallet_timestamp, Timestamp] [pallet_sudo, Sudo] - [pallet_registry, Registry] [pallet_commitments, Commitments] [pallet_admin_utils, AdminUtils] [pallet_subtensor, SubtensorModule] @@ -1749,6 +1715,7 @@ mod benches { [pallet_shield, MevShield] [pallet_subtensor_proxy, Proxy] [pallet_subtensor_utility, Utility] + [pallet_limit_orders, LimitOrders] ); } @@ -2559,6 +2526,10 @@ impl_runtime_apis! { fn get_subnet_account_id(netuid: NetUid) -> Option { SubtensorModule::get_subnet_account_id(netuid) } + + fn get_next_epoch_start_block(netuid: NetUid) -> Option { + SubtensorModule::get_next_epoch_start_block(netuid) + } } impl subtensor_custom_rpc_runtime_api::StakeInfoRuntimeApi for Runtime { @@ -2574,6 +2545,10 @@ impl_runtime_apis! { SubtensorModule::get_stake_info_for_hotkey_coldkey_netuid( hotkey_account, coldkey_account, netuid ) } + fn get_stake_availability_for_coldkeys( coldkey_accounts: Vec, netuids: Option> ) -> BTreeMap> { + SubtensorModule::get_stake_availability_for_coldkeys( coldkey_accounts, netuids ) + } + fn get_stake_fee( origin: Option<(AccountId32, NetUid)>, origin_coldkey_account: AccountId32, destination: Option<(AccountId32, NetUid)>, destination_coldkey_account: AccountId32, amount: u64 ) -> u64 { SubtensorModule::get_stake_fee( origin, origin_coldkey_account, destination, destination_coldkey_account, amount ) } @@ -2669,7 +2644,7 @@ impl_runtime_apis! { impl pallet_subtensor_swap_runtime_api::SwapRuntimeApi for Runtime { fn current_alpha_price(netuid: NetUid) -> u64 { pallet_subtensor_swap::Pallet::::current_price(netuid.into()) - .saturating_mul(U96F32::from_num(1_000_000_000)) + .saturating_mul(U64F64::from_num(1_000_000_000)) .saturating_to_num() } @@ -2688,7 +2663,7 @@ impl_runtime_apis! { fn sim_swap_tao_for_alpha(netuid: NetUid, tao: TaoBalance) -> SimSwapResult { let price = pallet_subtensor_swap::Pallet::::current_price(netuid.into()); let tao_u64: u64 = tao.into(); - let no_slippage_alpha = U96F32::saturating_from_num(tao_u64).safe_div(price).saturating_to_num::(); + let no_slippage_alpha = U64F64::saturating_from_num(tao_u64).safe_div(price).saturating_to_num::(); let order = pallet_subtensor::GetAlphaForTao::::with_amount(tao); // fee_to_block_author is included in sr.fee_paid, so it is absent in this calculation pallet_subtensor_swap::Pallet::::sim_swap( @@ -2718,7 +2693,7 @@ impl_runtime_apis! { fn sim_swap_alpha_for_tao(netuid: NetUid, alpha: AlphaBalance) -> SimSwapResult { let price = pallet_subtensor_swap::Pallet::::current_price(netuid.into()); let alpha_u64: u64 = alpha.into(); - let no_slippage_tao = U96F32::saturating_from_num(alpha_u64).saturating_mul(price).saturating_to_num::(); + let no_slippage_tao = U64F64::saturating_from_num(alpha_u64).saturating_mul(price).saturating_to_num::(); let order = pallet_subtensor::GetTaoForAlpha::::with_amount(alpha); // fee_to_block_author is included in sr.fee_paid, so it is absent in this calculation pallet_subtensor_swap::Pallet::::sim_swap( diff --git a/runtime/src/migrations/mod.rs b/runtime/src/migrations/mod.rs index ecc48efcdb..d0fdf7f3da 100644 --- a/runtime/src/migrations/mod.rs +++ b/runtime/src/migrations/mod.rs @@ -1 +1,3 @@ -//! Export migrations from here. +mod pallet_registry_cleanup_migration; + +pub use pallet_registry_cleanup_migration::*; diff --git a/runtime/src/migrations/pallet_registry_cleanup_migration.rs b/runtime/src/migrations/pallet_registry_cleanup_migration.rs new file mode 100644 index 0000000000..9a98ce77c4 --- /dev/null +++ b/runtime/src/migrations/pallet_registry_cleanup_migration.rs @@ -0,0 +1,539 @@ +use crate::{Runtime, RuntimeHoldReason}; +use alloc::string::String; +#[cfg(feature = "try-runtime")] +use alloc::vec::Vec; +#[cfg(feature = "try-runtime")] +use codec::{Decode, Encode}; +use deprecated::RegistryHoldReason as OldRegistryHoldReason; +use deprecated::RuntimeHoldReason as OldRuntimeHoldReason; +use frame_support::{ + BoundedVec, + pallet_prelude::Zero, + storage::unhashed, + traits::{OnRuntimeUpgrade, StoredMap, tokens::IdAmount}, + weights::Weight, +}; +use sp_io::hashing::twox_128; +use sp_runtime::Saturating; + +type DbWeightOf = ::DbWeight; +#[cfg(feature = "try-runtime")] +type AccountIdOf = ::AccountId; +type BalanceOf = ::Balance; +type AccountStoreOf = ::AccountStore; + +const MIGRATION_NAME: &[u8] = b"pallet_registry_cleanup_migration"; +const REGISTRY_PALLET_NAME: &[u8] = b"Registry"; +#[cfg(test)] +const REGISTRY_IDENTITY_OF_STORAGE_NAME: &[u8] = b"IdentityOf"; + +mod deprecated { + use super::BalanceOf; + use crate::Runtime; + use codec::Decode; + use frame_support::{ + BoundedVec, + traits::{ConstU32, tokens::IdAmount}, + }; + + #[cfg_attr(test, derive(codec::Encode))] + #[derive(Decode, Copy, Clone, Eq, PartialEq, Debug)] + pub(super) enum RegistryHoldReason { + #[codec(index = 0)] + RegistryIdentity, + } + + #[cfg_attr(test, derive(codec::Encode))] + #[derive(Decode, Copy, Clone, Eq, PartialEq, Debug)] + pub(super) enum RuntimeHoldReason { + #[codec(index = 14)] + Preimage(pallet_preimage::HoldReason), + #[codec(index = 17)] + Registry(RegistryHoldReason), + #[codec(index = 20)] + SafeMode(pallet_safe_mode::HoldReason), + #[codec(index = 29)] + Contracts(pallet_contracts::HoldReason), + } + + // Aggregated variant count across all pallets defining a + // composite HoldReason when the pallet was removed. + pub(super) const VARIANT_COUNT: u32 = 5; + + pub(super) type Holds = + BoundedVec>, ConstU32>; +} + +pub struct PalletRegistryCleanupMigration; + +impl OnRuntimeUpgrade for PalletRegistryCleanupMigration { + fn on_runtime_upgrade() -> Weight { + let migration_name = MIGRATION_NAME.to_vec(); + let mut weight = Weight::zero(); + + if pallet_subtensor::HasMigrationRun::::get(&migration_name) { + log::info!( + "Migration '{}' has already run. Skipping.", + String::from_utf8_lossy(&migration_name) + ); + return weight; + } + + log::info!( + "Running migration '{}'", + String::from_utf8_lossy(&migration_name) + ); + + pallet_balances::Holds::::translate::( + |account_id, old_holds| { + weight.saturating_accrue(DbWeightOf::::get().reads_writes(1, 1)); + let mut current_holds = BoundedVec::new(); + let mut unlocked_amount = BalanceOf::::zero(); + + // Translate old holds to new holds and keep track of cleaned up amount. + for hold in old_holds { + match map_reason(hold.id) { + Some(id) => { + if current_holds + .try_push(IdAmount { + id, + amount: hold.amount, + }) + .is_err() + { + log::error!( + "too many balance holds after migration for account {:?}", + account_id + ); + } + } + None => { + unlocked_amount = unlocked_amount.saturating_add(hold.amount); + } + } + } + + // Unlock the balance if there is any. + if !unlocked_amount.is_zero() { + weight.saturating_accrue(DbWeightOf::::get().reads_writes(1, 1)); + if let Err(error) = AccountStoreOf::::mutate(&account_id, |account| { + account.reserved = account.reserved.saturating_sub(unlocked_amount); + account.free = account.free.saturating_add(unlocked_amount); + }) { + log::error!( + "failed to unlock balance during holds migration: {:?}", + error + ); + } + } + + (!current_holds.is_empty()).then_some(current_holds) + }, + ); + + let registry_prefix = twox_128(REGISTRY_PALLET_NAME); + let result = unhashed::clear_prefix(®istry_prefix, Some(u32::MAX), None); + weight.saturating_accrue( + DbWeightOf::::get().reads_writes(result.loops as u64, result.unique as u64), + ); + log::info!( + "Removed {} entries from Registry pallet storage.", + result.unique + ); + + pallet_subtensor::HasMigrationRun::::insert(&migration_name, true); + weight = weight.saturating_add(DbWeightOf::::get().writes(1)); + + log::info!( + "Migration '{}' completed successfully.", + String::from_utf8_lossy(&migration_name) + ); + + weight + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, sp_runtime::TryRuntimeError> { + let mut affected_accounts = Vec::new(); + + for account_id in pallet_balances::Holds::::iter_keys() { + let old_holds = decode_deprecated_holds(&account_id)?; + let mut unlocked_amount = BalanceOf::::zero(); + + for hold in old_holds { + if matches!(hold.id, OldRuntimeHoldReason::Registry(_)) { + unlocked_amount = unlocked_amount.saturating_add(hold.amount); + } + } + + if !unlocked_amount.is_zero() { + let account = AccountStoreOf::::get(&account_id); + affected_accounts.push(AffectedAccount { + account_id, + free: account.free, + reserved: account.reserved, + unlocked: unlocked_amount, + }); + } + } + + let state = PreUpgradeState { + total_issuance: pallet_balances::TotalIssuance::::get(), + affected_accounts, + }; + + Ok(state.encode()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(state: Vec) -> Result<(), sp_runtime::TryRuntimeError> { + let state = PreUpgradeState::decode(&mut state.as_slice()) + .map_err(|_| "failed to decode registry cleanup pre-upgrade state")?; + + if !pallet_subtensor::HasMigrationRun::::get(MIGRATION_NAME.to_vec()) { + return Err("registry cleanup migration marker was not set".into()); + } + + if pallet_balances::TotalIssuance::::get() != state.total_issuance { + return Err("registry cleanup migration changed total issuance".into()); + } + + for affected_account in state.affected_accounts { + let account = AccountStoreOf::::get(&affected_account.account_id); + let expected_free = affected_account + .free + .saturating_add(affected_account.unlocked); + let expected_reserved = affected_account + .reserved + .saturating_sub(affected_account.unlocked); + + if account.free != expected_free { + return Err("registry cleanup migration did not unlock free balance".into()); + } + + if account.reserved != expected_reserved { + return Err("registry cleanup migration did not reduce reserved balance".into()); + } + } + + for account_id in pallet_balances::Holds::::iter_keys() { + pallet_balances::Holds::::try_get(&account_id) + .map_err(|_| "failed to decode migrated balances holds")?; + } + + let registry_prefix = twox_128(REGISTRY_PALLET_NAME); + if unhashed::contains_prefixed_key(®istry_prefix) { + return Err("registry pallet storage was not cleared".into()); + } + + Ok(()) + } +} + +#[cfg(test)] +fn registry_storage_prefix(storage_name: &[u8]) -> Vec { + let mut prefix = twox_128(REGISTRY_PALLET_NAME).to_vec(); + prefix.extend_from_slice(&twox_128(storage_name)); + prefix +} + +fn map_reason(reason: OldRuntimeHoldReason) -> Option { + match reason { + OldRuntimeHoldReason::Preimage(reason) => Some(RuntimeHoldReason::Preimage(reason)), + OldRuntimeHoldReason::SafeMode(reason) => Some(RuntimeHoldReason::SafeMode(reason)), + OldRuntimeHoldReason::Contracts(reason) => Some(RuntimeHoldReason::Contracts(reason)), + OldRuntimeHoldReason::Registry(OldRegistryHoldReason::RegistryIdentity) => None, + } +} + +#[cfg(feature = "try-runtime")] +#[derive(Encode, Decode)] +struct PreUpgradeState { + total_issuance: BalanceOf, + affected_accounts: Vec, +} + +#[cfg(feature = "try-runtime")] +#[derive(Encode, Decode)] +struct AffectedAccount { + account_id: AccountIdOf, + free: BalanceOf, + reserved: BalanceOf, + unlocked: BalanceOf, +} + +#[cfg(feature = "try-runtime")] +fn decode_deprecated_holds( + account_id: &AccountIdOf, +) -> Result { + let key = pallet_balances::Holds::::hashed_key_for(account_id); + unhashed::get::(&key) + .ok_or("failed to decode deprecated balances holds".into()) +} + +#[cfg(test)] +#[allow(clippy::expect_used)] +mod tests { + use super::*; + use alloc::vec; + use codec::Encode; + use frame_support::{ + assert_ok, + storage::unhashed, + traits::{Currency, ReservableCurrency}, + }; + use sp_runtime::{AccountId32, BuildStorage}; + + fn new_test_ext() -> sp_io::TestExternalities { + let mut ext: sp_io::TestExternalities = crate::RuntimeGenesisConfig::default() + .build_storage() + .expect("runtime genesis storage should build") + .into(); + ext.execute_with(|| crate::System::set_block_number(1)); + ext + } + + fn account(seed: u8) -> AccountId32 { + AccountId32::new([seed; 32]) + } + + fn balance(amount: u64) -> BalanceOf { + amount.into() + } + + fn old_hold( + id: OldRuntimeHoldReason, + amount: u64, + ) -> IdAmount> { + IdAmount { + id, + amount: balance(amount), + } + } + + fn old_holds( + holds: alloc::vec::Vec>>, + ) -> deprecated::Holds { + holds + .try_into() + .expect("test old holds should fit the deprecated bound") + } + + fn holds_key(account_id: &AccountId32) -> alloc::vec::Vec { + pallet_balances::Holds::::hashed_key_for(account_id) + } + + fn insert_old_holds(account_id: &AccountId32, holds: deprecated::Holds) { + unhashed::put_raw(&holds_key(account_id), &holds.encode()); + } + + fn registry_identity_prefix() -> alloc::vec::Vec { + registry_storage_prefix(REGISTRY_IDENTITY_OF_STORAGE_NAME) + } + + fn insert_old_registry_identity_storage(suffix: &[u8]) -> alloc::vec::Vec { + let mut key = registry_identity_prefix(); + key.extend_from_slice(suffix); + unhashed::put_raw(&key, &[1]); + key + } + + fn insert_old_registry_storage_version() -> alloc::vec::Vec { + let key = registry_storage_prefix(b":__STORAGE_VERSION__:"); + unhashed::put_raw(&key, &[1]); + key + } + + #[test] + fn drops_registry_holds_and_unlocks_their_balance() { + new_test_ext().execute_with(|| { + let account_id = account(1); + + assert!(!pallet_subtensor::HasMigrationRun::::get( + MIGRATION_NAME.to_vec() + )); + + let _ = crate::Balances::make_free_balance_be(&account_id, balance(10_000)); + assert_ok!(crate::Balances::reserve(&account_id, balance(225))); + + insert_old_holds( + &account_id, + old_holds(vec![ + old_hold( + OldRuntimeHoldReason::Registry(OldRegistryHoldReason::RegistryIdentity), + 125, + ), + old_hold( + OldRuntimeHoldReason::Preimage(pallet_preimage::HoldReason::Preimage), + 75, + ), + old_hold( + OldRuntimeHoldReason::SafeMode(pallet_safe_mode::HoldReason::EnterOrExtend), + 25, + ), + ]), + ); + + let registry_identity_key = insert_old_registry_identity_storage(b"account-1"); + let registry_storage_version_key = insert_old_registry_storage_version(); + assert!(unhashed::contains_prefixed_key(&twox_128( + REGISTRY_PALLET_NAME + ))); + + let issuance_before = crate::Balances::total_issuance(); + + let weight = PalletRegistryCleanupMigration::on_runtime_upgrade(); + + let account = crate::System::account(&account_id).data; + assert!(!weight.is_zero()); + assert_eq!(account.free, balance(9_900)); + assert_eq!(account.reserved, balance(100)); + assert_eq!(crate::Balances::total_issuance(), issuance_before); + + let current_holds = pallet_balances::Holds::::get(&account_id); + assert_eq!(current_holds.len(), 2); + assert!(current_holds.contains(&IdAmount { + id: RuntimeHoldReason::Preimage(pallet_preimage::HoldReason::Preimage), + amount: balance(75), + })); + assert!(current_holds.contains(&IdAmount { + id: RuntimeHoldReason::SafeMode(pallet_safe_mode::HoldReason::EnterOrExtend), + amount: balance(25), + })); + + assert!(pallet_subtensor::HasMigrationRun::::get( + MIGRATION_NAME.to_vec() + )); + assert!(unhashed::get_raw(®istry_identity_key).is_none()); + assert!(unhashed::get_raw(®istry_storage_version_key).is_none()); + assert!(!unhashed::contains_prefixed_key(&twox_128( + REGISTRY_PALLET_NAME + ))); + + let second_weight = PalletRegistryCleanupMigration::on_runtime_upgrade(); + let account_after_second = crate::System::account(&account_id).data; + + assert!(second_weight.is_zero()); + assert_eq!(account_after_second.free, account.free); + assert_eq!(account_after_second.reserved, account.reserved); + assert_eq!(account_after_second.frozen, account.frozen); + assert_eq!( + pallet_balances::Holds::::get(&account_id), + current_holds + ); + }); + } + + #[test] + fn removes_holds_storage_when_only_registry_holds_remain() { + new_test_ext().execute_with(|| { + let account_id = account(2); + + let _ = crate::Balances::make_free_balance_be(&account_id, balance(10_000)); + assert_ok!(crate::Balances::reserve(&account_id, balance(125))); + + insert_old_holds( + &account_id, + old_holds(vec![old_hold( + OldRuntimeHoldReason::Registry(OldRegistryHoldReason::RegistryIdentity), + 125, + )]), + ); + + let storage_key = holds_key(&account_id); + let issuance_before = crate::Balances::total_issuance(); + + PalletRegistryCleanupMigration::on_runtime_upgrade(); + + let account = crate::System::account(&account_id).data; + assert_eq!(account.free, balance(10_000)); + assert_eq!(account.reserved, balance(0)); + assert_eq!(crate::Balances::total_issuance(), issuance_before); + assert!(pallet_balances::Holds::::get(&account_id).is_empty()); + assert!(unhashed::get_raw(&storage_key).is_none()); + }); + } + + #[test] + fn preserves_non_registry_holds_without_changing_balances() { + new_test_ext().execute_with(|| { + let account_id = account(3); + + let _ = crate::Balances::make_free_balance_be(&account_id, balance(10_000)); + assert_ok!(crate::Balances::reserve(&account_id, balance(100))); + + insert_old_holds( + &account_id, + old_holds(vec![ + old_hold( + OldRuntimeHoldReason::Preimage(pallet_preimage::HoldReason::Preimage), + 70, + ), + old_hold( + OldRuntimeHoldReason::Contracts( + pallet_contracts::HoldReason::StorageDepositReserve, + ), + 30, + ), + ]), + ); + + let issuance_before = crate::Balances::total_issuance(); + + PalletRegistryCleanupMigration::on_runtime_upgrade(); + + let account = crate::System::account(&account_id).data; + assert_eq!(account.free, balance(9_900)); + assert_eq!(account.reserved, balance(100)); + assert_eq!(crate::Balances::total_issuance(), issuance_before); + + let current_holds = pallet_balances::Holds::::get(&account_id); + assert_eq!(current_holds.len(), 2); + assert!(current_holds.contains(&IdAmount { + id: RuntimeHoldReason::Preimage(pallet_preimage::HoldReason::Preimage), + amount: balance(70), + })); + assert!(current_holds.contains(&IdAmount { + id: RuntimeHoldReason::Contracts( + pallet_contracts::HoldReason::StorageDepositReserve, + ), + amount: balance(30), + })); + }); + } + + #[cfg(feature = "try-runtime")] + #[test] + fn try_runtime_checks_validate_cleanup() { + new_test_ext().execute_with(|| { + let account_id = account(4); + + let _ = crate::Balances::make_free_balance_be(&account_id, balance(10_000)); + assert_ok!(crate::Balances::reserve(&account_id, balance(150))); + + insert_old_holds( + &account_id, + old_holds(vec![ + old_hold( + OldRuntimeHoldReason::Registry(OldRegistryHoldReason::RegistryIdentity), + 100, + ), + old_hold( + OldRuntimeHoldReason::Preimage(pallet_preimage::HoldReason::Preimage), + 50, + ), + ]), + ); + + insert_old_registry_identity_storage(b"account-4"); + + let state = PalletRegistryCleanupMigration::pre_upgrade() + .expect("pre-upgrade check should decode old holds"); + + PalletRegistryCleanupMigration::on_runtime_upgrade(); + + PalletRegistryCleanupMigration::post_upgrade(state) + .expect("post-upgrade check should validate migrated holds"); + }); + } +} diff --git a/runtime/tests/limit_orders.rs b/runtime/tests/limit_orders.rs new file mode 100644 index 0000000000..f68191fa29 --- /dev/null +++ b/runtime/tests/limit_orders.rs @@ -0,0 +1,2667 @@ +#![allow( + clippy::unwrap_used, + clippy::arithmetic_side_effects, + clippy::too_many_arguments +)] + +use codec::Encode; +use frame_support::{ + BoundedVec, PalletId, assert_noop, assert_ok, + traits::{ConstU32, Hooks}, +}; +use node_subtensor_runtime::{ + BuildStorage, LimitOrders, Runtime, RuntimeEvent, RuntimeGenesisConfig, RuntimeOrigin, + SubtensorModule, System, pallet_subtensor, +}; +use pallet_limit_orders::{ + HasMigrationRun, LimitOrdersEnabled, Order, OrderStatus, OrderType, Orders, SignedOrder, + VersionedOrder, +}; +use pallet_subtensor::{SubnetAlphaIn, SubnetMechanism, SubnetTAO}; +use sp_core::{Get, H256, Pair}; +use sp_keyring::Sr25519Keyring; +use sp_runtime::traits::AccountIdConversion; +use sp_runtime::{MultiSignature, Perbill}; +use subtensor_runtime_common::{AccountId, AlphaBalance, NetUid, TaoBalance, Token}; + +fn new_test_ext() -> sp_io::TestExternalities { + sp_tracing::try_init_simple(); + let mut ext: sp_io::TestExternalities = RuntimeGenesisConfig::default() + .build_storage() + .unwrap() + .into(); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +/// Initialise a subnet so that limit-order execution has a pool to interact with. +/// +/// We use the stable mechanism (mechanism_id = 0, the default), which swaps at a +/// fixed 1 TAO : 1 alpha rate without requiring pre-seeded AMM liquidity. +fn setup_subnet(netuid: NetUid) { + SubtensorModule::init_new_network(netuid, 0); + // Genesis forces netuid 1 to dynamic (mechanism_id = 1); override to stable + // (mechanism_id = 0) so that swaps are 1:1 with no AMM fees, matching the + // intent of every test that calls this helper. + pallet_subtensor::SubnetMechanism::::insert(netuid, 0u16); + pallet_subtensor::SubtokenEnabled::::insert(netuid, true); +} + +fn min_default_stake() -> TaoBalance { + pallet_subtensor::DefaultMinStake::::get() +} + +fn add_balance_to_coldkey_account(coldkey: &AccountId, tao: TaoBalance) { + let credit = SubtensorModule::mint_tao(tao); + let _ = SubtensorModule::spend_tao(coldkey, credit, tao); +} + +fn seed_subnet_tao(netuid: NetUid, amount: TaoBalance) { + let subnet_account = SubtensorModule::get_subnet_account_id(netuid).unwrap(); + add_balance_to_coldkey_account(&subnet_account, amount); +} + +fn fund_account(id: &AccountId) { + add_balance_to_coldkey_account(id, min_default_stake() * 10u64.into()); +} + +fn order_id(order: &VersionedOrder) -> H256 { + H256(sp_io::hashing::blake2_256(&order.encode())) +} + +fn make_order_batch( + orders: Vec>, +) -> BoundedVec, ::MaxOrdersPerBatch> +{ + orders.try_into().unwrap() +} + +fn setup_buyer_seller( + netuid: NetUid, + alice_id: &AccountId, + charlie_id: &AccountId, + bob_id: &AccountId, + dave_id: &AccountId, +) { + fund_account(alice_id); + let initial_alpha: AlphaBalance = (min_default_stake().to_u64() * 10u64).into(); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + dave_id, + bob_id, + netuid, + initial_alpha, + ); + seed_subnet_tao(netuid, TaoBalance::from(initial_alpha.to_u64())); + let _ = SubtensorModule::create_account_if_non_existent(alice_id, charlie_id); + let _ = SubtensorModule::create_account_if_non_existent(bob_id, dave_id); +} + +struct OrderParams { + order_type: OrderType, + amount: u64, + limit_price: u64, + expiry: u64, + fee_rate: Perbill, + fee_recipient: AccountId, + relayer: Option>>, + max_slippage: Option, + partial_fills_enabled: bool, +} + +/// Shared implementation: constructs and signs a `VersionedOrder::V1` from an +/// `OrderParams` and returns a `SignedOrder` with `partial_fill = None`. +/// All three public factory functions delegate here so that adding a new field +/// to `Order` requires updating only this function. +fn make_signed_order_inner( + keyring: Sr25519Keyring, + hotkey: AccountId, + netuid: NetUid, + params: OrderParams, +) -> SignedOrder { + let order = VersionedOrder::V1(Order { + signer: keyring.to_account_id(), + hotkey, + netuid, + order_type: params.order_type, + amount: params.amount, + limit_price: params.limit_price, + expiry: params.expiry, + fee_rate: params.fee_rate, + fee_recipient: params.fee_recipient, + relayer: params.relayer, + max_slippage: params.max_slippage, + partial_fills_enabled: params.partial_fills_enabled, + // chain_id 0 matches the default pallet_evm_chain_id genesis value in tests + chain_id: 0, + }); + let sig = keyring.pair().sign(&order.encode()); + SignedOrder { + order, + signature: MultiSignature::Sr25519(sig), + partial_fill: None, + } +} + +fn make_signed_order( + keyring: Sr25519Keyring, + hotkey: AccountId, + netuid: NetUid, + order_type: OrderType, + amount: u64, + limit_price: u64, + expiry: u64, + fee_rate: Perbill, + fee_recipient: AccountId, +) -> SignedOrder { + make_signed_order_inner( + keyring, + hotkey, + netuid, + OrderParams { + order_type, + amount, + limit_price, + expiry, + fee_rate, + fee_recipient, + relayer: None, + max_slippage: None, + partial_fills_enabled: false, + }, + ) +} + +/// Set up a dynamic-mechanism (Uniswap v3-style) subnet with equal TAO and +/// alpha reserves, giving an initial pool price of exactly 1.0 TAO/alpha. +/// +/// The stable mechanism (mechanism_id = 0) ignores the `price_limit` parameter +/// entirely and always executes at 1:1, so slippage enforcement can only be +/// tested against a dynamic subnet. +fn setup_dynamic_subnet(netuid: NetUid) { + SubtensorModule::init_new_network(netuid, 0); + // Override the mechanism to 1 (dynamic / Uniswap v3). + SubnetMechanism::::insert(netuid, 1u16); + pallet_subtensor::SubtokenEnabled::::insert(netuid, true); + // Equal reserves → price = tao_reserve / alpha_reserve = 1.0 + SubnetTAO::::insert(netuid, TaoBalance::from(1_000_000_000_000_u64)); + SubnetAlphaIn::::insert(netuid, AlphaBalance::from(1_000_000_000_000_u64)); + seed_subnet_tao(netuid, TaoBalance::from(1_000_000_000_000_u64)); +} + +/// Build a signed order with an explicit `max_slippage` value. +fn make_signed_order_with_slippage_rt( + keyring: Sr25519Keyring, + hotkey: AccountId, + netuid: NetUid, + order_type: OrderType, + amount: u64, + limit_price: u64, + expiry: u64, + fee_rate: Perbill, + fee_recipient: AccountId, + max_slippage: Option, +) -> SignedOrder { + make_signed_order_inner( + keyring, + hotkey, + netuid, + OrderParams { + order_type, + amount, + limit_price, + expiry, + fee_rate, + fee_recipient, + relayer: None, + max_slippage, + partial_fills_enabled: false, + }, + ) +} + +/// Build a `SignedOrder` with `partial_fills_enabled = true` and the relayer set +/// to `relayer`. The `partial_fill` field on the envelope is supplied separately +/// by each test so that the *same* `VersionedOrder` payload (and therefore the +/// same order-id) can be re-used across multiple submissions. +fn make_partial_fill_order( + keyring: Sr25519Keyring, + hotkey: AccountId, + netuid: NetUid, + order_type: OrderType, + amount: u64, + limit_price: u64, + expiry: u64, + fee_recipient: AccountId, + relayer: AccountId, + partial_fill: Option, +) -> SignedOrder { + let mut signed = make_signed_order_inner( + keyring, + hotkey, + netuid, + OrderParams { + order_type, + amount, + limit_price, + expiry, + fee_rate: Perbill::zero(), + fee_recipient, + relayer: Some(BoundedVec::try_from(vec![relayer]).unwrap()), + max_slippage: None, + partial_fills_enabled: true, + }, + ); + signed.partial_fill = partial_fill; + signed +} + +// ───────────────────────────────────────────────────────────────────────────── + +/// Signing and cancelling an order writes the order id to storage as Cancelled +/// and emits OrderCancelled. No subnet or balance setup required. +#[test] +fn cancel_order_works() { + new_test_ext().execute_with(|| { + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let fee_recipient = Sr25519Keyring::Charlie.to_account_id(); + + let order = VersionedOrder::V1(Order { + signer: alice_id.clone(), + hotkey: bob_id, + netuid: NetUid::from(1u16), + order_type: OrderType::LimitBuy, + amount: 1_000, + limit_price: u64::MAX, + expiry: u64::MAX, + fee_rate: Perbill::zero(), + fee_recipient, + relayer: None, + max_slippage: None, + partial_fills_enabled: false, + // chain_id 0 matches the default pallet_evm_chain_id genesis value in tests + chain_id: 0, + }); + let id = order_id(&order); + + assert_ok!(LimitOrders::cancel_order( + RuntimeOrigin::signed(alice_id), + order, + )); + + assert_eq!(Orders::::get(id), Some(OrderStatus::Cancelled)); + }); +} + +/// An order signed with an Ed25519 key is rejected at validation time even +/// though the signature itself is cryptographically valid. The order must not +/// appear in the Orders storage map after the batch runs. +#[test] +fn execute_orders_ed25519_signature_rejected() { + new_test_ext().execute_with(|| { + let alice_id = Sr25519Keyring::Alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let fee_recipient = Sr25519Keyring::Charlie.to_account_id(); + + let order = VersionedOrder::V1(Order { + signer: alice_id.clone(), + hotkey: bob_id, + netuid: NetUid::from(1u16), + order_type: OrderType::LimitBuy, + amount: 1_000, + limit_price: u64::MAX, + expiry: u64::MAX, + fee_rate: Perbill::zero(), + fee_recipient, + relayer: None, + max_slippage: None, + partial_fills_enabled: false, + // chain_id 0 matches the default pallet_evm_chain_id genesis value in tests + chain_id: 0, + }); + let id = order_id(&order); + + // Sign with ed25519 — valid signature, wrong scheme. + let ed_pair = sp_core::ed25519::Pair::from_legacy_string("//Alice", None); + let ed_sig = ed_pair.sign(&order.encode()); + let signed = SignedOrder { + order, + signature: MultiSignature::Ed25519(ed_sig), + partial_fill: None, + }; + + let orders = make_order_batch(vec![signed]); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(alice_id), + orders, + false, + )); + + // Order was silently skipped — nothing written to storage. + assert!(Orders::::get(id).is_none()); + }); +} + +/// An order carrying a wrong chain_id is silently skipped by `execute_orders` +/// (the per-order error path) and must not appear in the Orders storage map. +#[test] +fn execute_orders_chain_id_mismatch_rejected() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + setup_subnet(netuid); + + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let fee_recipient = Sr25519Keyring::Charlie.to_account_id(); + fund_account(&alice_id); + + // Build an order with a chain_id that doesn't match the runtime (0). + let order = VersionedOrder::V1(Order { + signer: alice_id.clone(), + hotkey: bob_id, + netuid, + order_type: OrderType::LimitBuy, + amount: 1_000, + limit_price: u64::MAX, + expiry: u64::MAX, + fee_rate: Perbill::zero(), + fee_recipient, + relayer: None, + max_slippage: None, + partial_fills_enabled: false, + chain_id: 9999, // wrong chain — should be rejected + }); + let id = order_id(&order); + let sig = alice.pair().sign(&order.encode()); + let signed = SignedOrder { + order, + signature: MultiSignature::Sr25519(sig), + partial_fill: None, + }; + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(alice_id), + make_order_batch(vec![signed]), + false, + )); + + // Order was silently skipped — nothing written to storage. + assert!(Orders::::get(id).is_none()); + }); +} + +/// A LimitBuy order whose price condition is satisfied executes against the pool, +/// marks the order as Fulfilled, and credits staked alpha to the buyer. +#[test] +fn limit_buy_order_executes_and_stakes_alpha() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + setup_subnet(netuid); + + // Fund Alice so buy_alpha can debit her balance. + fund_account(&alice_id); + + // Create the hot-key association. + let _ = SubtensorModule::create_account_if_non_existent(&alice_id, &bob_id); + + // limit_price = u64::MAX → current_price (1.0) ≤ MAX → condition always met. + let signed = make_signed_order( + alice, + bob_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), // default min stake units of TAO to spend + u64::MAX, // price ceiling — always satisfied + u64::MAX, // no expiry + Perbill::zero(), + charlie_id.clone(), + ); + let id = order_id(&signed.order); + + let orders = make_order_batch(vec![signed]); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie_id), + orders, + false, + )); + + // Order must be marked as executed. + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + + // Alice must now have staked alpha delegated through Bob on this subnet. + // AMM pool output has slight slippage even with the stable mechanism; check within 1%. + let staked = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&bob_id, &alice_id, netuid); + let expected_alpha = min_default_stake().to_u64(); + assert!( + staked >= AlphaBalance::from(expected_alpha * 99 / 100) + && staked <= AlphaBalance::from(expected_alpha), + "alice should hold approximately min_default_stake alpha after a LimitBuy order executes (got {staked:?})" + ); + }); +} + +/// A TakeProfit order whose price condition is satisfied executes against the pool, +/// marks the order as Fulfilled, and burns the seller's staked alpha position. +#[test] +fn take_profit_order_executes_and_unstakes_alpha() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + setup_subnet(netuid); + + // Create the hot-key association. + let _ = SubtensorModule::create_account_if_non_existent(&alice_id, &bob_id); + + // Seed Alice with staked alpha through Bob so she has something to sell. + let initial_alpha: AlphaBalance = (min_default_stake().to_u64() * 10u64).into(); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &bob_id, + &alice_id, + netuid, + initial_alpha, + ); + seed_subnet_tao(netuid, TaoBalance::from(initial_alpha.to_u64())); + + // limit_price = 0 → current_price (1.0) ≥ 0 → condition always met. + let signed = make_signed_order( + alice, + bob_id.clone(), + netuid, + OrderType::TakeProfit, + min_default_stake().into(), // sell min default alpha units + 0, // price floor — always satisfied + u64::MAX, + Perbill::zero(), + charlie_id.clone(), + ); + let id = order_id(&signed.order); + + let orders = make_order_batch(vec![signed]); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie_id), + orders, + false, + )); + + // Order must be marked as executed. + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + + // Alice's staked alpha must have decreased by exactly min_default_stake after the sell. + // Stable mechanism 1:1, zero fee: initial_alpha = min_default_stake * 10, + // sold min_default_stake alpha, so remaining = min_default_stake * 9. + let remaining = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&bob_id, &alice_id, netuid); + assert_eq!( + remaining, + AlphaBalance::from(min_default_stake().to_u64() * 9u64), + "alice's staked alpha should be min_default_stake*9 after a TakeProfit order executes" + ); + }); +} + +/// A StopLoss order whose price condition is satisfied (price ≤ limit_price) executes +/// against the pool, marks the order as Fulfilled, decreases the seller's staked alpha, +/// and credits free TAO to the seller. +#[test] +fn stop_loss_order_executes_and_unstakes_alpha() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + setup_subnet(netuid); + + // Create the hot-key association. + let _ = SubtensorModule::create_account_if_non_existent(&alice_id, &bob_id); + + // Seed Alice with staked alpha through Bob so she has something to sell. + let initial_alpha: AlphaBalance = (min_default_stake().to_u64() * 10u64).into(); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &bob_id, + &alice_id, + netuid, + initial_alpha, + ); + seed_subnet_tao(netuid, TaoBalance::from(initial_alpha.to_u64())); + + // limit_price = 1_000_000_000 (1.0 × 10⁹) → scaled_price (1_000_000_000) ≤ 1_000_000_000 + // → StopLoss condition always met. Stable mechanism ignores the AMM floor, so any + // value ≥ 1_000_000_000 works here. + let signed = make_signed_order( + alice, + bob_id.clone(), + netuid, + OrderType::StopLoss, + min_default_stake().into(), // sell min_default_stake alpha units + 1_000_000_000, // price ceiling in ×10⁹ scale (1.0) — always met + u64::MAX, + Perbill::zero(), + charlie_id.clone(), + ); + let id = order_id(&signed.order); + + let orders = make_order_batch(vec![signed]); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie_id), + orders, + false, + )); + + // Order must be marked as executed. + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + + // Alice's staked alpha must have decreased by exactly min_default_stake. + // Stable mechanism 1:1, zero fee: initial_alpha = min_default_stake * 10, + // sold min_default_stake alpha, so remaining = min_default_stake * 9. + let remaining = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&bob_id, &alice_id, netuid); + assert_eq!( + remaining, + AlphaBalance::from(min_default_stake().to_u64() * 9u64), + "alice's staked alpha should be min_default_stake*9 after a StopLoss order executes" + ); + + // Alice must have received TAO from the sale. Pool output has slight slippage; check within 1%. + let alice_tao = SubtensorModule::get_coldkey_balance(&alice_id); + let expected_tao = min_default_stake().to_u64(); + assert!( + alice_tao >= TaoBalance::from(expected_tao * 99 / 100) + && alice_tao <= TaoBalance::from(expected_tao), + "alice should receive approximately min_default_stake TAO after a StopLoss order executes (got {alice_tao:?})" + ); + }); +} + +// ── Batched execution ───────────────────────────────────────────────────────── + +/// Buy side (5 000 TAO) exceeds sell side (2 000 alpha ≈ 2 000 TAO at 1:1). +/// +/// Residual 3 000 TAO goes to the pool; buyers receive pool alpha + seller passthrough +/// alpha. Sellers receive the passthrough TAO that corresponds to their alpha. +/// +/// With the stable mechanism (1 TAO = 1 alpha): +/// • Alice (buyer 5 000 TAO) → 5 000 alpha staked to Dave +/// • Bob (seller 2 000 α) → 2 000 free TAO +#[test] +fn batched_buy_dominant_executes_correctly() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob = Sr25519Keyring::Bob; + let bob_id = bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + let dave_id = Sr25519Keyring::Dave.to_account_id(); + + setup_subnet(netuid); + + setup_buyer_seller(netuid, &alice_id, &charlie_id, &bob_id, &dave_id); + + let buy = make_signed_order( + alice, + charlie_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().to_u64() * 2u64, + u64::MAX, + u64::MAX, + Perbill::zero(), + charlie_id.clone(), + ); + let sell = make_signed_order( + bob, + dave_id.clone(), + netuid, + OrderType::TakeProfit, + min_default_stake().into(), + 0, + u64::MAX, + Perbill::zero(), + charlie_id.clone(), + ); + + let orders = make_order_batch(vec![buy, sell]); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie_id.clone()), + netuid, + orders, + )); + + // Alice spent TAO and must hold the resulting staked alpha. + // Buy-dominant: Alice buys min_default_stake*2 TAO, Bob sells min_default_stake alpha. + // total_sell_tao_equiv = min_default_stake (at 1:1). residual_buy = min_default_stake. + // pool returns min_default_stake alpha; plus Bob's passthrough = min_default_stake. + // Alice receives Bob's passthrough alpha + pool alpha for the residual TAO. + // Pool output has slight slippage; check within 1% of expected min_default_stake*2. + let alice_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &charlie_id, + &alice_id, + netuid, + ); + let expected_alice_alpha = min_default_stake().to_u64() * 2u64; + assert!( + alice_alpha >= AlphaBalance::from(expected_alice_alpha * 99 / 100) + && alice_alpha <= AlphaBalance::from(expected_alice_alpha), + "alice should hold approximately min_default_stake*2 alpha after buy-dominant batch (got {alice_alpha:?})" + ); + + // Bob sold alpha and must hold the resulting free TAO. + // In buy-dominant, total_tao = total_sell_tao_equiv = min_default_stake. + // Bob's gross_share = (min_default_stake * min_default_stake) / min_default_stake + // = min_default_stake (exact). Zero fee => net_share = min_default_stake. + let bob_tao = SubtensorModule::get_coldkey_balance(&bob_id); + assert_eq!( + bob_tao, + TaoBalance::from(min_default_stake().to_u64()), + "bob should hold exactly min_default_stake TAO after buy-dominant batch" + ); + }); +} + +/// Regression (real-storage rollback): the same fully-signed `LimitBuy` order +/// appearing twice in one batch must hard-fail with `DuplicateOrderInBatch` and +/// leave the signer's balances completely untouched. +/// +/// Balances are real substrate storage, so the +/// all-or-nothing rollback is faithfully observable: free TAO and staked alpha +/// must match their pre-call values exactly, and the order must never be +/// recorded in `Orders`. +#[test] +fn batched_full_fill_duplicate_rejected_and_rolled_back() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + let dave_id = Sr25519Keyring::Dave.to_account_id(); + + setup_subnet(netuid); + setup_buyer_seller(netuid, &alice_id, &charlie_id, &bob_id, &dave_id); + + // Open-relay (relayer: None) fully-signed LimitBuy from Alice, staking + // to her hotkey (charlie). + let order = make_signed_order( + alice, + charlie_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), + u64::MAX, + u64::MAX, + Perbill::zero(), + charlie_id.clone(), + ); + let id = order_id(&order.order); + + // Snapshot the signer's real balances before the call. + let alice_tao_before = SubtensorModule::get_coldkey_balance(&alice_id); + let alice_alpha_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &charlie_id, + &alice_id, + netuid, + ); + + // The same order twice in one batch — must hard-fail the whole batch. + let orders = make_order_batch(vec![order.clone(), order]); + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie_id.clone()), + netuid, + orders, + ), + pallet_limit_orders::Error::::DuplicateOrderInBatch + ); + + // Full rollback: balances unchanged and no order status recorded. + assert_eq!( + SubtensorModule::get_coldkey_balance(&alice_id), + alice_tao_before, + "signer's free TAO must be unchanged after a duplicate-order batch rollback" + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &charlie_id, + &alice_id, + netuid, + ), + alice_alpha_before, + "signer's staked alpha must be unchanged after a duplicate-order batch rollback" + ); + assert!( + Orders::::get(id).is_none(), + "no order status must be recorded when the batch is rolled back" + ); + }); +} + +/// Sell side (min_default_stake()*2 alpha ≈ min_default_stake()*2 TAO at 1:1) exceeds buy side (min_default_stake() TAO). +/// +/// Residual min_default_stake() alpha goes to the pool; sellers receive pool TAO + buyer +/// passthrough TAO. Buyers receive the passthrough alpha corresponding to their TAO. +/// +/// With the stable mechanism (1 TAO = 1 alpha): +/// • Alice (buyer min_default_stake() TAO) → alpha staked to Dave +/// • Bob (seller min_default_stake()*2 α) → min_default_stake()*2 free TAO +#[test] +fn batched_sell_dominant_executes_correctly() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob = Sr25519Keyring::Bob; + let bob_id = bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + let dave_id = Sr25519Keyring::Dave.to_account_id(); + + setup_subnet(netuid); + + setup_buyer_seller(netuid, &alice_id, &charlie_id, &bob_id, &dave_id); + + let buy = make_signed_order( + alice, + charlie_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), + u64::MAX, + u64::MAX, + Perbill::zero(), + charlie_id.clone(), + ); + let sell = make_signed_order( + bob, + dave_id.clone(), + netuid, + OrderType::TakeProfit, + min_default_stake().to_u64() * 2u64, + 0, + u64::MAX, + Perbill::zero(), + charlie_id.clone(), + ); + + let orders = make_order_batch(vec![buy, sell]); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie_id.clone()), + netuid, + orders, + )); + + // Alice spent TAO and must hold the resulting staked alpha. + // Sell-dominant: Alice buys min_default_stake TAO, Bob sells min_default_stake*2 alpha. + // total_buy_alpha_equiv = tao_to_alpha(min_default_stake, 1.0) = min_default_stake (exact). + // Alice's pro-rata share = (min_default_stake * min_default_stake) / min_default_stake + // = min_default_stake (exact, no floor rounding). + let alice_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &charlie_id, + &alice_id, + netuid, + ); + assert_eq!( + alice_alpha, + AlphaBalance::from(min_default_stake().to_u64()), + "alice should hold exactly min_default_stake alpha after sell-dominant batch" + ); + + // Bob receives Alice's passthrough TAO + pool TAO for the residual alpha. + // Pool output has slight slippage; check within 1% of expected min_default_stake*2. + let bob_tao = SubtensorModule::get_coldkey_balance(&bob_id); + let expected_bob_tao = min_default_stake().to_u64() * 2u64; + assert!( + bob_tao >= TaoBalance::from(expected_bob_tao * 99 / 100) + && bob_tao <= TaoBalance::from(expected_bob_tao), + "bob should hold approximately min_default_stake*2 TAO after sell-dominant batch (got {bob_tao:?})" + ); + }); +} + +#[test] +fn batched_fails_if_executing_below_minimum_on_sell() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob = Sr25519Keyring::Bob; + let bob_id = bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + let dave_id = Sr25519Keyring::Dave.to_account_id(); + + setup_subnet(netuid); + + setup_buyer_seller(netuid, &alice_id, &charlie_id, &bob_id, &dave_id); + + let buy = make_signed_order( + alice, + charlie_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), + u64::MAX, + u64::MAX, + Perbill::zero(), + charlie_id.clone(), + ); + let sell = make_signed_order( + bob, + dave_id.clone(), + netuid, + OrderType::TakeProfit, + 1u64, + 0, + u64::MAX, + Perbill::zero(), + charlie_id.clone(), + ); + + let orders = make_order_batch(vec![buy, sell]); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie_id.clone()), + netuid, + orders, + ), + pallet_subtensor::Error::::AmountTooLow + ); + }); +} + +#[test] +fn batched_fails_if_executing_without_hot_key_association() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob = Sr25519Keyring::Bob; + let bob_id = bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + let dave_id = Sr25519Keyring::Dave.to_account_id(); + + setup_subnet(netuid); + + // Create the hot-key association. Alice is not associating to charlie + + // Alice has free TAO to spend on a buy order. + fund_account(&alice_id); + + // Seed Bob with staked alph so he has something to sell. + let initial_alpha: AlphaBalance = (min_default_stake().to_u64() * 10u64).into(); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &dave_id, + &bob_id, + netuid, + initial_alpha, + ); + seed_subnet_tao(netuid, TaoBalance::from(initial_alpha.to_u64())); + + let buy = make_signed_order( + alice, + charlie_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), + u64::MAX, + u64::MAX, + Perbill::zero(), + charlie_id.clone(), + ); + let sell = make_signed_order( + bob, + dave_id.clone(), + netuid, + OrderType::TakeProfit, + min_default_stake().to_u64() * 2u64, + 0, + u64::MAX, + Perbill::zero(), + charlie_id.clone(), + ); + + let orders = make_order_batch(vec![buy, sell]); + + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie_id.clone()), + netuid, + orders, + ), + pallet_subtensor::Error::::HotKeyAccountNotExists + ); + }); +} + +/// `execute_batched_orders` fails when the target subnet does not exist. +/// The subnet is never initialised (no `setup_subnet`), so `buy_alpha` +/// returns `SubnetNotExists` during the pool-swap step. +#[test] +fn batched_fails_for_nonexistent_subnet() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(2u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + // Fund Alice so that `transfer_tao` succeeds; the subnet check happens + // later inside `buy_alpha`. + fund_account(&alice_id); + + let buy = make_signed_order( + alice, + bob_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), + u64::MAX, // price ceiling — always satisfied + u64::MAX, // no expiry + Perbill::zero(), + charlie_id.clone(), + ); + + let orders = make_order_batch(vec![buy]); + + assert_noop!( + LimitOrders::execute_batched_orders(RuntimeOrigin::signed(charlie_id), netuid, orders,), + pallet_subtensor::Error::::SubnetNotExists + ); + }); +} + +/// `execute_batched_orders` fails when the subnet exists but its subtoken is +/// not enabled. The order passes validation (price condition is met) and the +/// TAO transfer succeeds, but `buy_alpha` then returns `SubtokenDisabled`. +#[test] +fn batched_fails_if_subtoken_not_enabled() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + // Initialise the network but deliberately skip setting SubtokenEnabled. + SubtensorModule::init_new_network(netuid, 0); + + // Fund Alice so that the TAO transfer in `collect_assets` succeeds. + fund_account(&alice_id); + + let buy = make_signed_order( + alice, + bob_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), + u64::MAX, + u64::MAX, + Perbill::zero(), + charlie_id.clone(), + ); + + let orders = make_order_batch(vec![buy]); + + assert_noop!( + LimitOrders::execute_batched_orders(RuntimeOrigin::signed(charlie_id), netuid, orders,), + pallet_subtensor::Error::::SubtokenDisabled + ); + }); +} + +/// An order whose `expiry` is in the past causes `execute_batched_orders` to +/// fail with `OrderExpired`. +#[test] +fn batched_fails_for_expired_order() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + setup_subnet(netuid); + + // Advance the runtime timestamp so that `now_ms` exceeds the order's expiry. + // `pallet_timestamp::Now` stores milliseconds; set it to 100_000 ms. + pallet_timestamp::Now::::put(100_000u64); + + // Build an order that expired at 50_000 ms — already in the past. + let signed = make_signed_order( + alice, + bob_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), + u64::MAX, + 50_000, // expiry in ms — before current timestamp of 100_000 + Perbill::zero(), + charlie_id.clone(), + ); + + let orders = make_order_batch(vec![signed]); + + assert_noop!( + LimitOrders::execute_batched_orders(RuntimeOrigin::signed(charlie_id), netuid, orders,), + pallet_limit_orders::Error::::OrderExpired + ); + }); +} + +/// An order whose price condition is not met causes `execute_batched_orders` to +/// fail with `PriceConditionNotMet`. A `LimitBuy` with `limit_price = 0` +/// requires `current_price <= 0`; since the stable mechanism prices alpha at +/// 1.0 TAO the condition is never met. +#[test] +fn batched_fails_if_price_condition_not_met() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + setup_subnet(netuid); + + // limit_price = 0 requires current_price <= 0, but current_price ~= 1.0 → fails. + let signed = make_signed_order( + alice, + bob_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), + 0, // price ceiling of 0 — never satisfied + u64::MAX, // no expiry + Perbill::zero(), + charlie_id.clone(), + ); + + let orders = make_order_batch(vec![signed]); + + assert_noop!( + LimitOrders::execute_batched_orders(RuntimeOrigin::signed(charlie_id), netuid, orders,), + pallet_limit_orders::Error::::PriceConditionNotMet + ); + }); +} + +/// `execute_batched_orders` fails immediately with `RootNetUidNotAllowed` when +/// called with `netuid = 0` (the root network). +#[test] +fn batched_fails_for_root_netuid() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(0u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + // Fund Alice so the call gets past any balance checks before hitting the root guard. + fund_account(&alice_id); + + let buy = make_signed_order( + alice, + bob_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), + u64::MAX, // price ceiling — always satisfied + u64::MAX, // no expiry + Perbill::zero(), + charlie_id.clone(), + ); + + let orders = make_order_batch(vec![buy]); + + assert_noop!( + LimitOrders::execute_batched_orders(RuntimeOrigin::signed(charlie_id), netuid, orders,), + pallet_limit_orders::Error::::RootNetUidNotAllowed + ); + }); +} + +// ── execute_orders — silent-skip behaviour ──────────────────────────────────── + +/// `execute_orders` silently skips an expired order: the call returns `Ok` +/// and the order is NOT written to the `Orders` storage map. +#[test] +fn execute_orders_skips_expired_order() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + setup_subnet(netuid); + + // Advance the runtime timestamp so that `now_ms` exceeds the order's expiry. + pallet_timestamp::Now::::put(100_000u64); + + // Build an order that expired at 50_000 ms — already in the past. + let signed = make_signed_order( + alice, + bob_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), + u64::MAX, + 50_000, // expiry in ms — before current timestamp of 100_000 + Perbill::zero(), + charlie_id.clone(), + ); + let id = order_id(&signed.order); + + let orders = make_order_batch(vec![signed]); + + // The call must succeed even though the order is expired. + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie_id), + orders, + false, + )); + + // Expired order silently skipped — nothing written to storage. + assert!(Orders::::get(id).is_none()); + }); +} + +/// `execute_orders` processes a mixed batch: the valid order executes and is +/// stored as `Fulfilled`; the expired order is silently skipped and is NOT +/// written to storage. The call always returns `Ok`. +#[test] +fn execute_orders_valid_and_invalid_mixed() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob = Sr25519Keyring::Bob; + let bob_id = bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + setup_subnet(netuid); + + // Fund Alice so that her LimitBuy order can execute. + fund_account(&alice_id); + + // Create the hotkey association for Alice so buy_alpha succeeds. + let _ = SubtensorModule::create_account_if_non_existent(&alice_id, &bob_id); + + // Timestamp at 100_000 ms — Bob's order (expiry 50_000) will be expired. + pallet_timestamp::Now::::put(100_000u64); + + // Valid order: LimitBuy with price ceiling always satisfied and no expiry. + let valid = make_signed_order( + alice, + bob_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), + u64::MAX, // price ceiling — always satisfied + u64::MAX, // no expiry + Perbill::zero(), + charlie_id.clone(), + ); + // Invalid order: already expired. + let expired = make_signed_order( + bob, + alice_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), + u64::MAX, + 50_000, // expiry in ms — before current timestamp of 100_000 + Perbill::zero(), + charlie_id.clone(), + ); + let valid_id = order_id(&valid.order); + let expired_id = order_id(&expired.order); + + let orders = make_order_batch(vec![valid, expired]); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie_id), + orders, + false, + )); + + // Valid order executed — stored as Fulfilled. + assert_eq!( + Orders::::get(valid_id), + Some(OrderStatus::Fulfilled) + ); + // Expired order silently skipped — not written to storage. + assert!(Orders::::get(expired_id).is_none()); + }); +} + +// ── execute_orders — all-or-nothing (should_fail = true) ────────────────────── + +/// `execute_orders` with `should_fail = true` aborts the whole call as soon as +/// it hits a failing order. A single expired order makes the extrinsic return +/// `OrderExpired`, and nothing is written to the `Orders` storage map. +#[test] +fn execute_orders_should_fail_aborts_on_expired_order() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + setup_subnet(netuid); + + // Advance the runtime timestamp so that `now_ms` exceeds the order's expiry. + pallet_timestamp::Now::::put(100_000u64); + + // Build an order that expired at 50_000 ms — already in the past. + let signed = make_signed_order( + alice, + bob_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), + u64::MAX, + 50_000, // expiry in ms — before current timestamp of 100_000 + Perbill::zero(), + charlie_id.clone(), + ); + let id = order_id(&signed.order); + + let orders = make_order_batch(vec![signed]); + + // should_fail = true → the expired order surfaces its error to the caller + // and the whole call reverts (nothing written to storage). + assert_noop!( + LimitOrders::execute_orders(RuntimeOrigin::signed(charlie_id), orders, true), + pallet_limit_orders::Error::::OrderExpired + ); + + // Order was never stored — the call aborted. + assert!(Orders::::get(id).is_none()); + }); +} + +/// Contrast with `execute_orders_valid_and_invalid_mixed`: the SAME mixed batch +/// (a valid LimitBuy followed by an expired LimitBuy) submitted with +/// `should_fail = true` reverts the WHOLE batch. The valid order's stake and +/// balance effects are NOT applied — dispatchables are transactional. +#[test] +fn execute_orders_should_fail_reverts_valid_order_in_mixed_batch() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob = Sr25519Keyring::Bob; + let bob_id = bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + setup_subnet(netuid); + + // Fund Alice so that her LimitBuy order would execute (absent the abort). + fund_account(&alice_id); + + // Create the hotkey association for Alice so buy_alpha would succeed. + let _ = SubtensorModule::create_account_if_non_existent(&alice_id, &bob_id); + + // Snapshot Alice's balance and stake before submitting the batch. + let alice_balance_before = SubtensorModule::get_coldkey_balance(&alice_id); + let alice_stake_before = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&bob_id, &alice_id, netuid); + + // Timestamp at 100_000 ms — Bob's order (expiry 50_000) will be expired. + pallet_timestamp::Now::::put(100_000u64); + + // Valid order: LimitBuy with price ceiling always satisfied and no expiry. + let valid = make_signed_order( + alice, + bob_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), + u64::MAX, // price ceiling — always satisfied + u64::MAX, // no expiry + Perbill::zero(), + charlie_id.clone(), + ); + // Invalid order: already expired. It follows the valid order in the batch, + // so the valid order is executed first and must be rolled back on abort. + let expired = make_signed_order( + bob, + alice_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), + u64::MAX, + 50_000, // expiry in ms — before current timestamp of 100_000 + Perbill::zero(), + charlie_id.clone(), + ); + let valid_id = order_id(&valid.order); + let expired_id = order_id(&expired.order); + + let orders = make_order_batch(vec![valid, expired]); + + // should_fail = true → the expired order aborts the whole call and reverts + // the already-executed valid order. + assert_noop!( + LimitOrders::execute_orders(RuntimeOrigin::signed(charlie_id), orders, true), + pallet_limit_orders::Error::::OrderExpired + ); + + // Neither order is stored — the entire batch was rolled back. + assert!( + Orders::::get(valid_id).is_none(), + "valid order must be rolled back, not stored, when should_fail aborts" + ); + assert!(Orders::::get(expired_id).is_none()); + + // The valid order's effects must NOT have been applied: Alice's TAO balance + // and her staked alpha are exactly what they were before the call. + assert_eq!( + SubtensorModule::get_coldkey_balance(&alice_id), + alice_balance_before, + "alice's TAO must be unchanged after an aborted all-or-nothing batch" + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&bob_id, &alice_id, netuid), + alice_stake_before, + "alice's staked alpha must be unchanged after an aborted all-or-nothing batch" + ); + }); +} + +/// `execute_orders` silently skips an order whose signer has no hotkey +/// association: the call returns `Ok` and the order is NOT written to the +/// `Orders` storage map. +#[test] +fn execute_orders_skips_order_with_unassociated_hotkey() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + setup_subnet(netuid); + + // Fund Alice so that any balance check is not the reason for skipping. + fund_account(&alice_id); + + // Deliberately do NOT call create_account_if_non_existent — Alice has no + // hotkey association, so the order should be silently skipped. + + let signed = make_signed_order( + alice, + bob_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), + u64::MAX, // price ceiling — always satisfied + u64::MAX, // no expiry + Perbill::zero(), + charlie_id.clone(), + ); + let id = order_id(&signed.order); + + let orders = make_order_batch(vec![signed]); + + // The call must succeed even though the hotkey association is missing. + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie_id), + orders, + false, + )); + + // Order was silently skipped — nothing written to storage. + assert!(Orders::::get(id).is_none()); + }); +} + +/// `execute_orders` silently skips an order whose amount is below the minimum +/// stake threshold: the call returns `Ok` and the order is NOT written to the +/// `Orders` storage map. +#[test] +fn execute_orders_skips_order_below_minimum_stake() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + setup_subnet(netuid); + + // Fund Alice so that any balance check is not the reason for skipping. + fund_account(&alice_id); + + // Create the hotkey association so that is not the reason for skipping. + let _ = SubtensorModule::create_account_if_non_existent(&alice_id, &bob_id); + + // amount = 1 is well below min_default_stake(), triggering AmountTooLow. + let signed = make_signed_order( + alice, + bob_id.clone(), + netuid, + OrderType::LimitBuy, + 1u64, + u64::MAX, // price ceiling — always satisfied + u64::MAX, // no expiry + Perbill::zero(), + charlie_id.clone(), + ); + let id = order_id(&signed.order); + + let orders = make_order_batch(vec![signed]); + + // The call must succeed even though the amount is below the minimum. + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie_id), + orders, + false, + )); + + // Order was silently skipped — nothing written to storage. + assert!(Orders::::get(id).is_none()); + }); +} + +/// `execute_orders` silently skips an order targeting a subnet that does not +/// exist: the call returns `Ok` and the order is NOT written to the `Orders` +/// storage map. +#[test] +fn execute_orders_skips_order_for_nonexistent_subnet() { + new_test_ext().execute_with(|| { + // netuid 2 is not initialised — no setup_subnet call. + let netuid = NetUid::from(2u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + // Fund Alice so that any balance check is not the reason for skipping. + fund_account(&alice_id); + + // Create the hotkey association so that is not the reason for skipping. + let _ = SubtensorModule::create_account_if_non_existent(&alice_id, &bob_id); + + let signed = make_signed_order( + alice, + bob_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), + u64::MAX, // price ceiling — always satisfied + u64::MAX, // no expiry + Perbill::zero(), + charlie_id.clone(), + ); + let id = order_id(&signed.order); + + let orders = make_order_batch(vec![signed]); + + // The call must succeed even though the subnet does not exist. + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie_id), + orders, + false, + )); + + // Order was silently skipped — nothing written to storage. + assert!(Orders::::get(id).is_none()); + }); +} + +// ── Fee-correctness tests ───────────────────────────────────────────────────── + +/// `execute_orders` (non-batched) correctly forwards the buy-order fee to the +/// designated fee recipient and charges Alice exactly `amount` TAO in total. +/// +/// Fee mechanics for a non-batched LimitBuy: +/// fee_tao = fee_rate * tao_in (computed from input BEFORE swap, exact integer arithmetic) +/// tao_after_fee = tao_in - fee_tao (goes to the pool) +/// fee transferred directly from signer to fee_recipient via transfer_tao +/// +/// We use amount = min_default_stake() * 2 so that tao_after_fee = 90% * 2 * min_default_stake() +/// = 1.8 * min_default_stake() > min_default_stake(), satisfying the minimum-stake validation +/// inside buy_alpha. With fee_rate = 10%: +/// fee_tao = 10% * (min_default_stake() * 2) = min_default_stake() / 5 (exact integer result) +/// Alice pays min_default_stake()*2 total and has min_default_stake()*8 remaining. +/// Charlie (fee recipient) receives exactly fee_tao. +#[test] +fn execute_orders_fee_forwarded_to_recipient() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + setup_subnet(netuid); + + // Fund Alice with 10× min_default_stake so she can cover the order amount and a margin. + fund_account(&alice_id); + + // Create the hotkey association Alice → Bob. + let _ = SubtensorModule::create_account_if_non_existent(&alice_id, &bob_id); + + // Charlie starts with zero balance — verify before submitting. + assert_eq!( + SubtensorModule::get_coldkey_balance(&charlie_id), + TaoBalance::from(0u64), + "charlie should start with zero balance" + ); + + // Use 2× min_default_stake so tao_after_fee (90%) stays above the minimum-stake threshold. + let order_amount = min_default_stake().to_u64() * 2u64; + + // limit_price = u64::MAX → condition always met; fee_recipient = Charlie. + let signed = make_signed_order( + alice, + bob_id.clone(), + netuid, + OrderType::LimitBuy, + order_amount, + u64::MAX, // price ceiling — always satisfied + u64::MAX, // no expiry + Perbill::from_percent(10), + charlie_id.clone(), + ); + let id = order_id(&signed.order); + + let orders = make_order_batch(vec![signed]); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie_id.clone()), + orders, + false, + )); + + // Order must be marked as executed. + assert_eq!(Orders::::get(id), Some(OrderStatus::Fulfilled)); + + // Buy fee is computed from input: fee = 10% * order_amount. Exact integer arithmetic. + let expected_fee = Perbill::from_percent(10) * order_amount; + assert_eq!( + SubtensorModule::get_coldkey_balance(&charlie_id), + TaoBalance::from(expected_fee), + "charlie (fee recipient) should receive exactly the buy fee" + ); + + // Alice spent exactly order_amount TAO (fee is deducted from the order amount, + // not charged on top), so she has min_default_stake()*10 - order_amount remaining. + assert_eq!( + SubtensorModule::get_coldkey_balance(&alice_id), + min_default_stake() * 8u64.into(), + "alice should have min_default_stake()*8 TAO remaining after the order" + ); + + // Alice must have received staked alpha through Bob. The pool received + // tao_after_fee = order_amount - fee; check within 1% of that expected alpha. + let tao_after_fee = order_amount - expected_fee; + let staked = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&bob_id, &alice_id, netuid); + assert!( + staked >= AlphaBalance::from(tao_after_fee * 99 / 100) + && staked <= AlphaBalance::from(tao_after_fee), + "alice should hold approximately tao_after_fee alpha after the LimitBuy with fee (got {staked:?})" + ); + }); +} + +/// `execute_batched_orders` correctly forwards fees to a shared fee recipient (Eve) +/// when both a buy and a sell order designate the same recipient. +/// +/// Fee mechanics for batched orders: +/// Buy: fee = gross - net = fee_rate * gross (withheld from pool input, transferred from pallet). +/// Sell: fee = fee_rate * gross_share (withheld from TAO pool output, inherits slippage). +/// +/// The buy fee is exact; the sell fee is approximate (pool slippage). +#[test] +fn batched_fee_forwarded_to_recipient() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob = Sr25519Keyring::Bob; + let bob_id = bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + let dave_id = Sr25519Keyring::Dave.to_account_id(); + let eve_id = Sr25519Keyring::Eve.to_account_id(); + + setup_subnet(netuid); + + setup_buyer_seller(netuid, &alice_id, &charlie_id, &bob_id, &dave_id); + + // Eve (shared fee recipient) starts with zero balance. + assert_eq!( + SubtensorModule::get_coldkey_balance(&eve_id), + TaoBalance::from(0u64), + "eve should start with zero balance" + ); + + let buy = make_signed_order( + alice, + charlie_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), + u64::MAX, // price ceiling — always satisfied + u64::MAX, // no expiry + Perbill::from_percent(10), + eve_id.clone(), // fee goes to Eve + ); + let sell = make_signed_order( + bob, + dave_id.clone(), + netuid, + OrderType::TakeProfit, + min_default_stake().into(), + 0, // price floor — always satisfied + u64::MAX, // no expiry + Perbill::from_percent(10), + eve_id.clone(), // fee goes to Eve + ); + let buy_id = order_id(&buy.order); + let sell_id = order_id(&sell.order); + + let orders = make_order_batch(vec![buy, sell]); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie_id.clone()), + netuid, + orders, + )); + + // Both orders must be fulfilled. + assert_eq!(Orders::::get(buy_id), Some(OrderStatus::Fulfilled)); + assert_eq!( + Orders::::get(sell_id), + Some(OrderStatus::Fulfilled) + ); + + // Buy fee is exact: fee = 10% * min_default_stake(). + let buy_fee = Perbill::from_percent(10) * min_default_stake().to_u64(); + + // Sell fee is approximate (pool slippage). Lower bound: 10% of 99% of amount. + let sell_fee_lower_bound = + Perbill::from_percent(10) * (min_default_stake().to_u64() * 99 / 100); + + // Eve must have received at least buy_fee + sell_fee_lower_bound, + // and at most buy_fee + 10% * amount (upper bound on sell fee with no slippage). + let sell_fee_upper_bound = Perbill::from_percent(10) * min_default_stake().to_u64(); + let eve_balance = SubtensorModule::get_coldkey_balance(&eve_id); + assert!( + eve_balance >= TaoBalance::from(buy_fee + sell_fee_lower_bound) + && eve_balance <= TaoBalance::from(buy_fee + sell_fee_upper_bound), + "eve should receive combined buy+sell fee within tolerance (got {eve_balance:?})" + ); + }); +} + +/// `execute_batched_orders` routes fees to the correct recipient when two orders +/// in the same batch designate different fee recipients (Charlie for the buy, +/// Dave for the sell). +/// +/// Verifies that: +/// - Charlie receives exactly the buy fee (no pool slippage on input). +/// - Dave receives approximately the sell fee (within 1%, due to pool slippage). +/// - Neither recipient received both fees. +#[test] +fn batched_multiple_fee_recipients_each_receive_correct_amount() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob = Sr25519Keyring::Bob; + let bob_id = bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + let dave_id = Sr25519Keyring::Dave.to_account_id(); + + setup_subnet(netuid); + + setup_buyer_seller(netuid, &alice_id, &charlie_id, &bob_id, &dave_id); + + // Charlie and Dave start with zero free balance (they are hotkeys; no initial funding). + assert_eq!( + SubtensorModule::get_coldkey_balance(&charlie_id), + TaoBalance::from(0u64), + "charlie should start with zero balance" + ); + assert_eq!( + SubtensorModule::get_coldkey_balance(&dave_id), + TaoBalance::from(0u64), + "dave should start with zero balance" + ); + + // Alice: LimitBuy, fee goes to Charlie. + let buy = make_signed_order( + alice, + charlie_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), + u64::MAX, // price ceiling — always satisfied + u64::MAX, // no expiry + Perbill::from_percent(10), + charlie_id.clone(), // buy fee to Charlie + ); + // Bob: TakeProfit, fee goes to Dave. + let sell = make_signed_order( + bob, + dave_id.clone(), + netuid, + OrderType::TakeProfit, + min_default_stake().into(), + 0, // price floor — always satisfied + u64::MAX, // no expiry + Perbill::from_percent(10), + dave_id.clone(), // sell fee to Dave + ); + let buy_id = order_id(&buy.order); + let sell_id = order_id(&sell.order); + + let orders = make_order_batch(vec![buy, sell]); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie_id.clone()), + netuid, + orders, + )); + + // Both orders must be fulfilled. + assert_eq!(Orders::::get(buy_id), Some(OrderStatus::Fulfilled)); + assert_eq!( + Orders::::get(sell_id), + Some(OrderStatus::Fulfilled) + ); + + // Charlie receives exactly the buy fee: 10% * min_default_stake(). + let expected_buy_fee = Perbill::from_percent(10) * min_default_stake().to_u64(); + assert_eq!( + SubtensorModule::get_coldkey_balance(&charlie_id), + TaoBalance::from(expected_buy_fee), + "charlie (buy fee recipient) should receive exactly the buy fee" + ); + + // Dave receives approximately the sell fee (pool slippage ≤ 1%). + // Expected sell fee ≈ 10% of min_default_stake (the seller's gross TAO share). + let expected_sell_fee = Perbill::from_percent(10) * min_default_stake().to_u64(); + let sell_fee_lower_bound = + Perbill::from_percent(10) * (min_default_stake().to_u64() * 99 / 100); + let dave_balance = SubtensorModule::get_coldkey_balance(&dave_id); + assert!( + dave_balance >= TaoBalance::from(sell_fee_lower_bound) + && dave_balance <= TaoBalance::from(expected_sell_fee), + "dave (sell fee recipient) should receive approximately the sell fee within 1% (got {dave_balance:?})" + ); + + // Verify fees are separate: neither recipient received both fees. + // Charlie's balance is exactly buy_fee (not buy_fee + sell_fee). + let charlie_balance = SubtensorModule::get_coldkey_balance(&charlie_id); + assert!( + charlie_balance <= TaoBalance::from(expected_buy_fee), + "charlie should not have received the sell fee (got {charlie_balance:?})" + ); + // Dave's balance is ≤ sell_fee (not sell_fee + buy_fee). + assert!( + dave_balance <= TaoBalance::from(expected_sell_fee), + "dave should not have received the buy fee (got {dave_balance:?})" + ); + }); +} + +// ── max_slippage enforcement against the real dynamic-mechanism AMM ─────────── + +/// A StopLoss order whose price condition is met (`current_price ≤ limit_price`) +/// but whose `max_slippage`-derived floor exceeds the pool's actual price is +/// silently skipped by `execute_orders`. +/// +/// Setup: +/// Dynamic subnet, equal reserves → pool price = 1.0 (raw ratio, i.e. 1 rao/alpha). +/// limit_price = 2_000_000_000 (2.0 × 10⁹) → StopLoss trigger: 1.0 ≤ 2.0 ✓ +/// max_slippage = 10% → effective AMM floor = 2_000_000_000 − 10% × 2_000_000_000 = 1_800_000_000. +/// Pool price = 1_000_000_000 (1.0 × 10⁹) < 1_800_000_000 → PriceLimitExceeded. +/// `execute_orders` catches the error and skips the order (no storage write). +/// Because `sell_alpha` is `#[transactional]`, the stake decrement is rolled back. +#[test] +fn execute_orders_stoploss_max_slippage_exceeds_pool_price_skipped() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + setup_dynamic_subnet(netuid); + + // Alice needs staked alpha so the sell can debit her position. + let _ = SubtensorModule::create_account_if_non_existent(&alice_id, &bob_id); + let initial_alpha: AlphaBalance = (min_default_stake().to_u64() * 10u64).into(); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &bob_id, + &alice_id, + netuid, + initial_alpha, + ); + + // limit_price = 2_000_000_000 (2.0 × 10⁹): StopLoss triggers when price ≤ 2.0; pool is at 1.0 → met. + // max_slippage = 10% → effective AMM floor = 1_800_000_000. + // Pool price = 1_000_000_000 < 1_800_000_000 → PriceLimitExceeded → order skipped. + let signed = make_signed_order_with_slippage_rt( + alice, + bob_id.clone(), + netuid, + OrderType::StopLoss, + min_default_stake().into(), + 2_000_000_000, // trigger at price 2.0 × 10⁹; pool is at 1.0 — condition met + u64::MAX, + Perbill::zero(), + charlie_id.clone(), + Some(Perbill::from_percent(10)), + ); + let id = order_id(&signed.order); + + let orders = make_order_batch(vec![signed]); + + // execute_orders is best-effort: the call succeeds even though the order + // is rejected by the AMM. + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie_id), + orders, + false, + )); + + // Order must NOT have been written to storage — it was silently skipped. + assert!( + Orders::::get(id).is_none(), + "order should have been skipped, not stored" + ); + + // `try_execute_order` is #[transactional]: the stake decrement inside + // `unstake_from_subnet` is rolled back when the AMM rejects the swap, + // so alice's alpha is unchanged. + let remaining = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&bob_id, &alice_id, netuid); + assert_eq!( + remaining, initial_alpha, + "alice's staked alpha should be unchanged when the order is rolled back" + ); + }); +} + +/// Contrasting test: the same StopLoss order without `max_slippage` executes +/// successfully against the dynamic-mechanism pool. +/// +/// This confirms that the price condition alone is not the blocker and that +/// the previous test's skip is genuinely caused by the slippage floor. +#[test] +fn execute_orders_stoploss_no_slippage_executes_on_dynamic_subnet() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + setup_dynamic_subnet(netuid); + + let _ = SubtensorModule::create_account_if_non_existent(&alice_id, &bob_id); + let initial_alpha: AlphaBalance = (min_default_stake().to_u64() * 10u64).into(); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &bob_id, + &alice_id, + netuid, + initial_alpha, + ); + + // Same limit_price — trigger still met. max_slippage = None → floor = 0 + // → AMM limit = 0 → no floor constraint → pool executes the sell. + // + // Sell 5× min_default_stake: the dynamic AMM deducts a small fee (~0.05%) + // from the alpha input before swapping, so the TAO output is slightly below + // the sell amount. The `validate_remove_stake` sim-swap check verifies that + // the TAO equivalent is ≥ DefaultMinStake — selling 5× ensures the fee cannot + // drag the output below that floor even on a lightly-loaded pool. + let sell_amount = min_default_stake().to_u64() * 5; + let signed = make_signed_order_with_slippage_rt( + alice, + bob_id.clone(), + netuid, + OrderType::StopLoss, + sell_amount, + 2_000_000_000, + u64::MAX, + Perbill::zero(), + charlie_id.clone(), + None, + ); + let id = order_id(&signed.order); + + let orders = make_order_batch(vec![signed]); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie_id), + orders, + false, + )); + + // Order must be marked as fulfilled. + assert_eq!( + Orders::::get(id), + Some(OrderStatus::Fulfilled), + "order should be fulfilled when no slippage floor is set" + ); + + // Alice's staked alpha must have decreased by the sold amount (5× min_default_stake). + let remaining = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&bob_id, &alice_id, netuid); + assert_eq!( + remaining, + AlphaBalance::from(min_default_stake().to_u64() * 5u64), + "alice's staked alpha should decrease by 5×min_default_stake after StopLoss executes" + ); + }); +} + +// ── Partial fill tests ──────────────────────────────────────────────────────── + +/// A LimitBuy order with `partial_fills_enabled` is partially filled on the +/// first `execute_orders` call, then fully filled (Fulfilled) on a second call +/// carrying the remaining amount. +/// +/// The signed payload (`VersionedOrder`) is identical in both submissions so +/// both calls share the same order-id. Only `SignedOrder::partial_fill` changes. +#[test] +fn execute_orders_partial_fill_then_complete() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + setup_subnet(netuid); + + // Alice funds two fills: partial_amount + remaining_amount = order amount. + let order_amount = min_default_stake().to_u64() * 4u64; + let partial_amount = min_default_stake().to_u64() * 3u64; + let remaining_amount = order_amount - partial_amount; + + add_balance_to_coldkey_account(&alice_id, TaoBalance::from(order_amount * 2u64)); + + // Create the hotkey association Alice → Bob. + let _ = SubtensorModule::create_account_if_non_existent(&alice_id, &bob_id); + + // Build the base signed order — this exact payload is re-used for both submissions. + let first_signed = make_partial_fill_order( + alice, + bob_id.clone(), + netuid, + OrderType::LimitBuy, + order_amount, + u64::MAX, // price ceiling — always satisfied + u64::MAX, // no expiry + charlie_id.clone(), + charlie_id.clone(), // relayer = caller + Some(partial_amount), + ); + let id = order_id(&first_signed.order); + + // ── First submission: partial fill ──────────────────────────────────── + let orders = make_order_batch(vec![first_signed.clone()]); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie_id.clone()), + orders, + false, + )); + + // After the first execution the order must be partially filled. + assert_eq!( + Orders::::get(id), + Some(OrderStatus::PartiallyFilled(partial_amount)), + "order should be PartiallyFilled({partial_amount}) after first execution" + ); + + // ── Second submission: fill the remainder ───────────────────────────── + // Clone the order payload from the first signed order (same VersionedOrder, + // same order-id) but set partial_fill to the remaining amount. + let second_signed = SignedOrder { + order: first_signed.order.clone(), + signature: first_signed.signature.clone(), + partial_fill: Some(remaining_amount), + }; + + let orders2 = make_order_batch(vec![second_signed]); + + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie_id.clone()), + orders2, + false, + )); + + // After the second execution the order must be fulfilled. + assert_eq!( + Orders::::get(id), + Some(OrderStatus::Fulfilled), + "order should be Fulfilled after the remaining amount is filled" + ); + }); +} + +/// Same partial-fill-then-complete scenario exercised through +/// `execute_batched_orders`. +/// +/// The buy order is the only order in the batch both times, so the batch is +/// buy-dominant and routes all TAO through the pool. The signed payload is +/// identical between submissions; only `SignedOrder::partial_fill` changes. +#[test] +fn execute_batched_orders_partial_fill_then_complete() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + setup_subnet(netuid); + + let order_amount = min_default_stake().to_u64() * 4u64; + let partial_amount = min_default_stake().to_u64() * 3u64; + let remaining_amount = order_amount - partial_amount; + + add_balance_to_coldkey_account(&alice_id, TaoBalance::from(order_amount * 2u64)); + + // Create the hotkey association Alice → Bob. + let _ = SubtensorModule::create_account_if_non_existent(&alice_id, &bob_id); + + // Build the base signed order — identical payload reused in both batches. + let first_signed = make_partial_fill_order( + alice, + bob_id.clone(), + netuid, + OrderType::LimitBuy, + order_amount, + u64::MAX, // price ceiling — always satisfied + u64::MAX, // no expiry + charlie_id.clone(), + charlie_id.clone(), // relayer = caller + Some(partial_amount), + ); + let id = order_id(&first_signed.order); + + // ── First batch: partial fill ───────────────────────────────────────── + let orders = make_order_batch(vec![first_signed.clone()]); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie_id.clone()), + netuid, + orders, + )); + + assert_eq!( + Orders::::get(id), + Some(OrderStatus::PartiallyFilled(partial_amount)), + "order should be PartiallyFilled({partial_amount}) after first batch" + ); + + // ── Second batch: fill the remainder ────────────────────────────────── + let second_signed = SignedOrder { + order: first_signed.order.clone(), + signature: first_signed.signature.clone(), + partial_fill: Some(remaining_amount), + }; + + let orders2 = make_order_batch(vec![second_signed]); + + assert_ok!(LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie_id.clone()), + netuid, + orders2, + )); + + assert_eq!( + Orders::::get(id), + Some(OrderStatus::Fulfilled), + "order should be Fulfilled after the remaining amount is filled in the second batch" + ); + }); +} + +// ── sim-swap partial-fill guard ─────────────────────────────────────────────── + +/// A LimitBuy order with 1 ppb max_slippage is silently skipped by +/// `execute_orders` because the sim-swap detects that the AMM would only +/// consume a microscopic fraction of the input before the price ceiling is +/// breached (partial fill). +/// +/// Setup: dynamic subnet, equal 1T TAO / 1T alpha reserves → pool price = 1.0. +/// limit_price = 1_000_000_000 (1.0 × 10⁹): LimitBuy triggers when price ≤ 1.0 — met. +/// max_slippage = 1 ppb → ceiling = 1_000_000_001, barely above pool price. +/// Sending any real TAO amount immediately pushes the price above the ceiling, +/// so sim.amount_paid_in + sim.fee_paid < input_amount → SlippageTooHigh. +/// `execute_orders` is best-effort: it catches the error and skips the order. +#[test] +fn execute_orders_buy_tight_slippage_partial_fill_skipped() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + setup_dynamic_subnet(netuid); + + // Alice needs a hotkey association for the buy to validate. + let _ = SubtensorModule::create_account_if_non_existent(&alice_id, &bob_id); + // Alice needs TAO to fund the buy. + fund_account(&alice_id); + + let initial_balance = SubtensorModule::get_coldkey_balance(&alice_id); + + // limit_price = 1_000_000_000 (= 1.0 × 10⁹): LimitBuy trigger (spot ≤ 1.0) met. + // max_slippage = 1 ppb → price ceiling = 1_000_000_001, just above pool price. + // Any real TAO amount pushes the price above the ceiling → partial fill detected. + let signed = make_signed_order_with_slippage_rt( + alice, + bob_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), + 1_000_000_000, // price ceiling at exactly 1.0 × 10⁹ — trigger met + u64::MAX, + Perbill::zero(), + charlie_id.clone(), + Some(Perbill::from_parts(1)), // 1 ppb — ceiling barely above spot + ); + let id = order_id(&signed.order); + + let orders = make_order_batch(vec![signed]); + + // execute_orders is best-effort: the call succeeds even though the guard + // rejects the order due to partial fill. + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie_id), + orders, + false, + )); + + // Order must NOT have been written to storage — it was silently skipped. + assert!( + Orders::::get(id).is_none(), + "order should have been skipped, not stored" + ); + + // No funds should have been debited from Alice — the rollback guard + // prevents any state change when partial fill is detected. + let final_balance = SubtensorModule::get_coldkey_balance(&alice_id); + assert_eq!( + final_balance, initial_balance, + "alice's TAO balance should be unchanged when the order is rolled back" + ); + }); +} + +/// Same setup as `execute_orders_buy_tight_slippage_partial_fill_skipped` but +/// submitted via `execute_batched_orders`. The batch hard-fails with +/// `SlippageTooHigh` because batched execution is not best-effort. +#[test] +fn execute_batched_orders_buy_tight_slippage_partial_fill_fails() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + setup_dynamic_subnet(netuid); + + let _ = SubtensorModule::create_account_if_non_existent(&alice_id, &bob_id); + fund_account(&alice_id); + + // Identical order to the execute_orders variant above. + let signed = make_signed_order_with_slippage_rt( + alice, + bob_id.clone(), + netuid, + OrderType::LimitBuy, + min_default_stake().into(), + 1_000_000_000, + u64::MAX, + Perbill::zero(), + charlie_id.clone(), + Some(Perbill::from_parts(1)), + ); + + let orders = make_order_batch(vec![signed]); + + // Batched execution hard-fails: the partial-fill guard surfaces the error + // directly to the caller instead of silently skipping. + assert_noop!( + LimitOrders::execute_batched_orders(RuntimeOrigin::signed(charlie_id), netuid, orders,), + pallet_subtensor::Error::::SlippageTooHigh + ); + }); +} + +/// A TakeProfit order with 1 ppb max_slippage is silently skipped by +/// `execute_orders` because the sim-swap detects that selling any real alpha +/// amount immediately pushes the pool price below the 1 ppb floor. +/// +/// Setup: dynamic subnet, equal 1T TAO / 1T alpha reserves → pool price = 1.0. +/// limit_price = 1_000_000_000 (1.0 × 10⁹): TakeProfit triggers when price ≥ 1.0 — met. +/// max_slippage = 1 ppb → floor = 999_999_999, barely below pool price. +/// Selling any real alpha amount moves the price below the floor, +/// so sim.amount_paid_in + sim.fee_paid < input_amount → SlippageTooHigh. +/// `execute_orders` is best-effort: it catches the error and skips the order. +#[test] +fn execute_orders_sell_tight_slippage_partial_fill_skipped() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + setup_dynamic_subnet(netuid); + + // Alice needs a hotkey association and staked alpha for the sell. + let _ = SubtensorModule::create_account_if_non_existent(&alice_id, &bob_id); + let initial_alpha: AlphaBalance = (min_default_stake().to_u64() * 10u64).into(); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &bob_id, + &alice_id, + netuid, + initial_alpha, + ); + + // limit_price = 1_000_000_000 (= 1.0 × 10⁹): TakeProfit trigger (spot ≥ 1.0) met. + // max_slippage = 1 ppb → price floor = 999_999_999, just below pool price. + // Any real alpha sale pushes the price below the floor → partial fill detected. + let signed = make_signed_order_with_slippage_rt( + alice, + bob_id.clone(), + netuid, + OrderType::TakeProfit, + min_default_stake().into(), + 1_000_000_000, // price floor at exactly 1.0 × 10⁹ — trigger met + u64::MAX, + Perbill::zero(), + charlie_id.clone(), + Some(Perbill::from_parts(1)), // 1 ppb — floor barely below spot + ); + let id = order_id(&signed.order); + + let orders = make_order_batch(vec![signed]); + + // execute_orders is best-effort: the call succeeds even though the guard + // rejects the order due to partial fill. + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie_id), + orders, + false, + )); + + // Order must NOT have been written to storage — it was silently skipped. + assert!( + Orders::::get(id).is_none(), + "order should have been skipped, not stored" + ); + + // Alice's staked alpha must be unchanged — the rollback guard prevents + // any state change when partial fill is detected. + let remaining_alpha = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&bob_id, &alice_id, netuid); + assert_eq!( + remaining_alpha, initial_alpha, + "alice's staked alpha should be unchanged when the order is rolled back" + ); + }); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Migration integration tests +// ───────────────────────────────────────────────────────────────────────────── + +fn migration_key() -> BoundedVec> { + BoundedVec::truncate_from(b"migrate_register_pallet_hotkey".to_vec()) +} + +fn pallet_acct() -> AccountId { + PalletId(*b"bt/limit").into_account_truncating() +} + +fn pallet_hotkey() -> AccountId { + PalletId(*b"bt/lmhky").into_account_truncating() +} + +/// `on_runtime_upgrade` registers the pallet hotkey and marks the migration as run. +/// +/// Starting from the default genesis (which already registers the hotkey and +/// enables the pallet via `GenesisConfig::build`), the upgrade hook must: +/// - set `HasMigrationRun[migration_key]` to `true` +/// - leave `LimitOrdersEnabled` untouched (still `true`) +/// - leave the hotkey registration intact +#[test] +fn on_runtime_upgrade_marks_migration_run_without_touching_pallet_status() { + new_test_ext().execute_with(|| { + assert!(LimitOrdersEnabled::::get()); + assert!(!HasMigrationRun::::get(migration_key())); + assert!(SubtensorModule::coldkey_owns_hotkey( + &pallet_acct(), + &pallet_hotkey() + )); + + >::on_runtime_upgrade(); + + assert!( + HasMigrationRun::::get(migration_key()), + "migration must be marked as run" + ); + assert!( + LimitOrdersEnabled::::get(), + "upgrade must not change LimitOrdersEnabled" + ); + assert!(SubtensorModule::coldkey_owns_hotkey( + &pallet_acct(), + &pallet_hotkey() + )); + }); +} + +/// Running `on_runtime_upgrade` twice is a no-op on the second call. +#[test] +fn on_runtime_upgrade_is_idempotent() { + new_test_ext().execute_with(|| { + >::on_runtime_upgrade(); + assert!(HasMigrationRun::::get(migration_key())); + + // Second run must not change any state. + LimitOrdersEnabled::::set(false); + >::on_runtime_upgrade(); + + assert!( + !LimitOrdersEnabled::::get(), + "second upgrade must not touch LimitOrdersEnabled" + ); + }); +} + +// ── Conviction-lock protection ──────────────────────────────────────────────── + +/// A sell order whose alpha is fully conviction-locked is silently skipped by +/// `execute_orders` (best-effort path): the extrinsic returns `Ok`, the order +/// is never written to `Orders` storage, and the seller's staked alpha is +/// unchanged. +#[test] +fn individual_sell_order_skipped_when_alpha_is_conviction_locked() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + setup_subnet(netuid); + let _ = SubtensorModule::create_account_if_non_existent(&alice_id, &bob_id); + + // Give alice staked alpha through bob. + let initial_alpha: AlphaBalance = (min_default_stake().to_u64() * 3u64).into(); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &bob_id, + &alice_id, + netuid, + initial_alpha, + ); + seed_subnet_tao(netuid, TaoBalance::from(initial_alpha.to_u64())); + + // Lock ALL of alice's alpha with conviction — nothing is available to sell. + assert_ok!(SubtensorModule::do_lock_stake( + &alice_id, + netuid, + &bob_id, + initial_alpha, + )); + + let sell_amount = min_default_stake().to_u64(); + let signed = make_signed_order( + alice, + bob_id.clone(), + netuid, + OrderType::TakeProfit, + sell_amount, + 0, // price floor — always satisfied + u64::MAX, + Perbill::zero(), + charlie_id.clone(), + ); + let id = order_id(&signed.order); + + // Best-effort: the locked order is silently skipped, extrinsic still returns Ok. + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(charlie_id), + make_order_batch(vec![signed]), + false, + )); + + // Order must NOT be in storage — it was skipped, not fulfilled. + assert_eq!( + Orders::::get(id), + None, + "order should be skipped when alpha is conviction-locked" + ); + + // Alice's staked alpha must be completely unchanged. + let remaining = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&bob_id, &alice_id, netuid); + assert_eq!( + remaining, initial_alpha, + "conviction-locked alpha must not be moved by a skipped sell order" + ); + }); +} + +/// A batched sell order whose alpha is fully conviction-locked causes the +/// entire `execute_batched_orders` call to fail atomically with +/// `StakeUnavailable` — no state is committed. +#[test] +fn batched_sell_order_fails_when_alpha_is_conviction_locked() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + + setup_subnet(netuid); + let _ = SubtensorModule::create_account_if_non_existent(&alice_id, &bob_id); + + // Give alice staked alpha through bob. + let initial_alpha: AlphaBalance = (min_default_stake().to_u64() * 3u64).into(); + SubtensorModule::increase_stake_for_hotkey_and_coldkey_on_subnet( + &bob_id, + &alice_id, + netuid, + initial_alpha, + ); + seed_subnet_tao(netuid, TaoBalance::from(initial_alpha.to_u64())); + + // Lock ALL of alice's alpha with conviction — nothing is available to sell. + assert_ok!(SubtensorModule::do_lock_stake( + &alice_id, + netuid, + &bob_id, + initial_alpha, + )); + + let sell = make_signed_order( + alice, + bob_id.clone(), + netuid, + OrderType::TakeProfit, + min_default_stake().to_u64(), + 0, // price floor — always satisfied + u64::MAX, + Perbill::zero(), + charlie_id.clone(), + ); + + // Atomic path: the lock violation must revert the entire batch. + assert_noop!( + LimitOrders::execute_batched_orders( + RuntimeOrigin::signed(charlie_id), + netuid, + make_order_batch(vec![sell]), + ), + pallet_subtensor::Error::::StakeUnavailable + ); + }); +} + +/// Regression test for `#[transactional]` on `try_execute_order`. +/// +/// Invariant: a single buy order is **atomic** — either the TAO→alpha swap AND +/// the fee transfer both commit (and the order is recorded), or neither does. +/// +/// ## Trigger (buy path) +/// `buy_alpha` only checks the signer can remove `tao_after_fee`, NOT the full +/// `tao_in`. So if the signer's free TAO sits in the window +/// `[tao_after_fee, tao_in)`, `buy_alpha` succeeds but the subsequent +/// `forward_fee` of `fee_tao` fails for insufficient funds. In best-effort mode +/// (`should_fail = false`) the caller catches that `Err`, emits `OrderSkipped`, +/// and returns `Ok(())`. Without `#[transactional]` the orphaned `buy_alpha` +/// swap would be committed by the outer storage layer; with it, the whole order +/// rolls back. +/// +/// ## Arithmetic (ED = 500, min_default_stake = 2_000_000) +/// - `amount = tao_in = min_default_stake * 10 = 20_000_000` +/// - `fee_rate = 10%` → `fee_tao = 2_000_000`, `tao_after_fee = 18_000_000` +/// (≥ min_default_stake, so it clears the `AmountTooLow` check). +/// - Fund the signer with `B = 19_000_000`, which sits strictly inside the +/// vulnerable window `[18_000_000, 20_000_000)`: +/// * `buy_alpha` passes: `tao_after_fee (18_000_000) ≤ B`, and +/// `stake_into_subnet` debits the full `18_000_000` because +/// `B - ED = 18_999_500 ≥ 18_000_000` (no ED clamp). Balance → 1_000_000. +/// * `forward_fee` of `fee_tao (2_000_000)` then fails: only 1_000_000 left. +#[test] +fn fee_failure_after_buy_rolls_back_swap() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1u16); + let alice = Sr25519Keyring::Alice; + let alice_id = alice.to_account_id(); + let bob_id = Sr25519Keyring::Bob.to_account_id(); + // Fee recipient distinct from the signer (Alice) and the relayer. + let charlie_id = Sr25519Keyring::Charlie.to_account_id(); + // Relayer that submits the batch — kept distinct so its balance is irrelevant. + let dave_id = Sr25519Keyring::Dave.to_account_id(); + + setup_subnet(netuid); + + // Create the hotkey association so buy_alpha's validation passes. + let _ = SubtensorModule::create_account_if_non_existent(&alice_id, &bob_id); + + // amount = tao_in = min_default_stake * 10; fee_rate = 10%. + // fee_tao = 2_000_000 + // tao_after_fee = 18_000_000 (≥ min_default_stake, clears AmountTooLow) + let tao_in = min_default_stake().to_u64() * 10u64; + + // Fund Alice's coldkey so her free TAO is inside the vulnerable window + // [tao_after_fee, tao_in) = [18_000_000, 20_000_000): 19_000_000. + // buy_alpha passes (18_000_000 ≤ 19_000_000, and 19_000_000 - ED clears the + // full debit), leaving 1_000_000 — which is < fee_tao (2_000_000), so + // forward_fee fails for insufficient funds. + let signer_balance = TaoBalance::from(19_000_000u64); + add_balance_to_coldkey_account(&alice_id, signer_balance); + + let signed = make_signed_order( + alice, + bob_id.clone(), + netuid, + OrderType::LimitBuy, + tao_in, + u64::MAX, // price ceiling — always satisfied + u64::MAX, // no expiry + Perbill::from_percent(10), + charlie_id.clone(), + ); + let id = order_id(&signed.order); + + // Snapshot the observable state before the call. + let alice_balance_before = SubtensorModule::get_coldkey_balance(&alice_id); + let alice_stake_before = + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&bob_id, &alice_id, netuid); + let charlie_balance_before = SubtensorModule::get_coldkey_balance(&charlie_id); + + // Sanity: Alice really is funded to 19_000_000, inside the window. + assert_eq!(alice_balance_before, signer_balance); + + let orders = make_order_batch(vec![signed]); + + // Best-effort path: the per-order error is caught, OrderSkipped is emitted, + // and the extrinsic returns Ok(()). + assert_ok!(LimitOrders::execute_orders( + RuntimeOrigin::signed(dave_id), + orders, + false, + )); + + // ── Atomic rollback assertions ─────────────────────────────────────────── + + // 1. The signer's free TAO is unchanged: the buy_alpha debit was rolled back. + assert_eq!( + SubtensorModule::get_coldkey_balance(&alice_id), + alice_balance_before, + "signer's TAO must be unchanged: the orphaned buy_alpha swap must roll back when forward_fee fails" + ); + + // 2. No alpha was credited to the signer: the swap was rolled back. + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(&bob_id, &alice_id, netuid), + alice_stake_before, + "signer's staked alpha must be unchanged: no alpha may be credited from a rolled-back buy" + ); + + // 3. The order was NOT recorded — it must remain replayable-free, i.e. absent. + assert!( + Orders::::get(id).is_none(), + "order must not be recorded when its execution failed and rolled back" + ); + + // 4. The fee recipient received nothing. + assert_eq!( + SubtensorModule::get_coldkey_balance(&charlie_id), + charlie_balance_before, + "fee recipient's balance must be unchanged: the fee transfer failed and rolled back" + ); + + // 5. An OrderSkipped event was emitted for this order id. + let skipped = System::events().into_iter().any(|record| { + matches!( + record.event, + RuntimeEvent::LimitOrders(pallet_limit_orders::Event::OrderSkipped { + order_id: skipped_id, + .. + }) if skipped_id == id + ) + }); + assert!( + skipped, + "an OrderSkipped event must be emitted for the failed order" + ); + }); +} diff --git a/runtime/tests/metadata.rs b/runtime/tests/metadata.rs index 3409098b41..fb73c58890 100644 --- a/runtime/tests/metadata.rs +++ b/runtime/tests/metadata.rs @@ -9,7 +9,6 @@ fn is_pallet_error(segments: &[String]) -> bool { "pallet_admin_utils", "pallet_subtensor_collective", "pallet_commitments", - "pallet_registry", "pallet_subtensor", ]; diff --git a/scripts/discover_pallets.sh b/scripts/discover_pallets.sh index 0b37239380..e42e6f7825 100755 --- a/scripts/discover_pallets.sh +++ b/scripts/discover_pallets.sh @@ -1,20 +1,53 @@ #!/usr/bin/env bash +set -euo pipefail + # Auto-discover benchmarked pallets. # # Finds all pallets under pallets/ that have both: -# - src/benchmarking.rs (or src/benchmarks.rs) -# - src/weights.rs +# - src/benchmarking.rs (or src/benchmarks.rs) +# - src/weights.rs +# +# Then filters that list to pallets actually registered in runtime/src/lib.rs +# define_benchmarks!(...). A pallet having benchmark files is not enough for: +# +# node-subtensor benchmark pallet --pallet= +# +# The pallet must also be present in the runtime benchmark registry. # # Outputs one line per pallet: "pallet_name pallets//src/weights.rs" # The pallet name is derived from the Cargo.toml `name` field with dashes -> underscores. ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +RUNTIME_FILE="$ROOT_DIR/runtime/src/lib.rs" + +RUNTIME_BENCHMARKS="$( + perl -0ne ' + if (/define_benchmarks!\s*\((.*?)\)\s*;/s) { + my $body = $1; + while ($body =~ /\[\s*([A-Za-z0-9_:]+)\s*,/g) { + my $name = $1; + $name =~ s/::.*$//; + print "$name\n"; + } + } + ' "$RUNTIME_FILE" | sort -u +)" for dir in "$ROOT_DIR"/pallets/*/; do - [ -f "$dir/src/weights.rs" ] || continue - [ -f "$dir/src/benchmarking.rs" ] || [ -f "$dir/src/benchmarks.rs" ] || continue + [ -f "$dir/src/weights.rs" ] || continue + [ -f "$dir/src/benchmarking.rs" ] || [ -f "$dir/src/benchmarks.rs" ] || continue + + name="$( + awk -F '"' '/^name[[:space:]]*=/ { print $2; exit }' "$dir/Cargo.toml" \ + | tr '-' '_' + )" + + [ -n "$name" ] || continue + + if ! printf '%s\n' "$RUNTIME_BENCHMARKS" | grep -qxF "$name"; then + continue + fi - name=$(grep '^name' "$dir/Cargo.toml" | head -1 | sed 's/.*= *"\(.*\)"/\1/' | tr '-' '_') - relpath="pallets/$(basename "$dir")/src/weights.rs" - echo "$name $relpath" -done + relpath="pallets/$(basename "$dir")/src/weights.rs" + echo "$name $relpath" +done \ No newline at end of file diff --git a/support/linting/src/pallet_index.rs b/support/linting/src/pallet_index.rs index e14617be24..f6fae12fbe 100644 --- a/support/linting/src/pallet_index.rs +++ b/support/linting/src/pallet_index.rs @@ -174,7 +174,6 @@ mod tests { Preimage : pallet_preimage = 14, Scheduler : pallet_scheduler = 15, Proxy : pallet_subtensor_proxy = 16, - Registry : pallet_registry = 17, Commitments : pallet_commitments = 18, AdminUtils : pallet_admin_utils = 19, SafeMode : pallet_safe_mode = 20 diff --git a/ts-tests/moonwall.config.json b/ts-tests/moonwall.config.json index cc5667af9f..1ef1793749 100644 --- a/ts-tests/moonwall.config.json +++ b/ts-tests/moonwall.config.json @@ -12,7 +12,7 @@ "suites/dev" ], "runScripts": [], - "multiThreads": true, + "multiThreads": false, "reporters": ["basic"], "foundation": { "type": "dev", @@ -32,7 +32,9 @@ "--sealing=manual" ], "disableDefaultEthProviders": true, - "newRpcBehaviour": true + "newRpcBehaviour": true, + "maxStartupTimeout": 120000, + "connectTimeout": 30000 } ] } diff --git a/ts-tests/pnpm-lock.yaml b/ts-tests/pnpm-lock.yaml index d92a6b9cd6..d81731311f 100644 --- a/ts-tests/pnpm-lock.yaml +++ b/ts-tests/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + toml: 3.0.0 + importers: .: @@ -40,7 +43,7 @@ importers: version: 14.0.1(@polkadot/util@14.0.1) '@zombienet/orchestrator': specifier: 0.0.105 - version: 0.0.105(@polkadot/util@14.0.1)(@types/node@25.3.5)(chokidar@3.6.0) + version: 0.0.105(@polkadot/util@14.0.1)(@types/node@25.3.5)(chokidar@3.6.0)(supports-color@8.1.1) ethereum-cryptography: specifier: 3.1.0 version: 3.1.0 @@ -56,13 +59,13 @@ importers: devDependencies: '@acala-network/chopsticks': specifier: 1.2.3 - version: 1.2.3(debug@4.3.7)(ts-node@10.9.2(@types/node@25.3.5)(typescript@5.8.3)) + version: 1.2.3(debug@4.3.7(supports-color@8.1.1))(ts-node@10.9.2(@types/node@25.3.5)(typescript@5.8.3)) '@biomejs/biome': specifier: 1.9.4 version: 1.9.4 '@moonwall/cli': specifier: 5.18.3 - version: 5.18.3(@polkadot/api-base@16.5.4)(@polkadot/api-derive@16.5.4)(@polkadot/api@16.5.4)(@polkadot/keyring@14.0.1(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1))(@polkadot/rpc-provider@16.5.4)(@polkadot/types-codec@16.5.4)(@polkadot/types@16.5.4)(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1)(@types/debug@4.1.12)(@types/node@25.3.5)(chokidar@3.6.0)(debug@4.3.7)(encoding@0.1.13)(jsdom@23.2.0)(postcss@8.5.8)(rxjs@7.8.2)(ts-node@10.9.2(@types/node@25.3.5)(typescript@5.8.3))(tsx@4.21.0)(typescript@5.8.3)(zod@3.25.76) + version: 5.18.3(@polkadot/api-base@16.5.4)(@polkadot/api-derive@16.5.4)(@polkadot/api@16.5.4)(@polkadot/keyring@14.0.1(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1))(@polkadot/rpc-provider@16.5.4)(@polkadot/types-codec@16.5.4)(@polkadot/types@16.5.4)(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1)(@types/debug@4.1.12)(@types/node@25.3.5)(chokidar@3.6.0)(debug@4.3.7(supports-color@8.1.1))(encoding@0.1.13)(jsdom@23.2.0)(postcss@8.5.8)(rxjs@7.8.2)(supports-color@8.1.1)(ts-node@10.9.2(@types/node@25.3.5)(typescript@5.8.3))(tsx@4.21.0)(typescript@5.8.3)(zod@3.25.76) '@moonwall/util': specifier: 5.18.3 version: 5.18.3(@polkadot/api-base@16.5.4)(@polkadot/api-derive@16.5.4)(@polkadot/api@16.5.4)(@polkadot/keyring@14.0.1(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1))(@polkadot/rpc-provider@16.5.4)(@polkadot/types-codec@16.5.4)(@polkadot/types@16.5.4)(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1)(@types/debug@4.1.12)(@types/node@25.3.5)(chokidar@3.6.0)(encoding@0.1.13)(jsdom@23.2.0)(postcss@8.5.8)(rxjs@7.8.2)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.2)(zod@3.25.76) @@ -89,7 +92,7 @@ importers: version: 3.1.3(vitest@3.2.4) '@zombienet/utils': specifier: ^0.0.28 - version: 0.0.28(@types/node@25.3.5)(chokidar@3.6.0)(typescript@5.8.3) + version: 0.0.28(@types/node@25.3.5)(chokidar@3.6.0)(supports-color@8.1.1)(typescript@5.8.3) bottleneck: specifier: 2.19.5 version: 2.19.5 @@ -110,9 +113,9 @@ importers: version: 10.32.1 solc: specifier: 0.8.21 - version: 0.8.21(debug@4.3.7) + version: 0.8.21(debug@4.3.7(supports-color@8.1.1)) toml: - specifier: ^3.0.0 + specifier: 3.0.0 version: 3.0.0 tsx: specifier: '*' @@ -1219,7 +1222,7 @@ packages: '@polkadot-api/descriptors@file:.papi/descriptors': resolution: {directory: .papi/descriptors, type: directory} peerDependencies: - polkadot-api: '>=1.11.2' + polkadot-api: '>=2.0.0' '@polkadot-api/ink-contracts@0.4.0': resolution: {integrity: sha512-e2u5KhuYoiM+PyHsvjkI0O1nmFuC0rLH64uBerMqwK7hWENdM/ej9OqKawIzp6NQuYSHF5P4U8NBT0mjP9Y1yQ==} @@ -4158,10 +4161,6 @@ packages: toml@3.0.0: resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} - toml@https://codeload.github.com/pepoviola/toml-node/tar.gz/5e17114f1af5b5b70e4f2ec10cd007623c928988: - resolution: {tarball: https://codeload.github.com/pepoviola/toml-node/tar.gz/5e17114f1af5b5b70e4f2ec10cd007623c928988} - version: 3.0.0 - totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -4380,6 +4379,7 @@ packages: uuid@10.0.0: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@11.1.0: @@ -4873,7 +4873,7 @@ snapshots: '@polkadot/util': 14.0.1 '@polkadot/wasm-util': 7.5.4(@polkadot/util@14.0.1) - '@acala-network/chopsticks@1.2.3(debug@4.3.7)(ts-node@10.9.2(@types/node@25.3.5)(typescript@5.8.3))': + '@acala-network/chopsticks@1.2.3(debug@4.3.7(supports-color@8.1.1))(ts-node@10.9.2(@types/node@25.3.5)(typescript@5.8.3))': dependencies: '@acala-network/chopsticks-core': 1.2.3 '@acala-network/chopsticks-db': 1.2.3(ts-node@10.9.2(@types/node@25.3.5)(typescript@5.8.3)) @@ -4884,7 +4884,7 @@ snapshots: '@polkadot/types': 16.5.4 '@polkadot/util': 13.5.9 '@polkadot/util-crypto': 13.5.9(@polkadot/util@13.5.9) - axios: 1.13.6(debug@4.3.7) + axios: 1.13.6(debug@4.3.7(supports-color@8.1.1)) comlink: 4.4.2 dotenv: 16.6.1 global-agent: 3.0.0 @@ -4917,7 +4917,7 @@ snapshots: - typeorm-aurora-data-api-driver - utf-8-validate - '@acala-network/chopsticks@1.2.7(debug@4.3.7)(ts-node@10.9.2(@types/node@25.3.5)(typescript@5.8.3))': + '@acala-network/chopsticks@1.2.7(debug@4.3.7(supports-color@8.1.1))(ts-node@10.9.2(@types/node@25.3.5)(typescript@5.8.3))': dependencies: '@acala-network/chopsticks-core': 1.2.7 '@acala-network/chopsticks-db': 1.2.7(ts-node@10.9.2(@types/node@25.3.5)(typescript@5.8.3)) @@ -4929,7 +4929,7 @@ snapshots: '@polkadot/types': 16.5.4 '@polkadot/util': 14.0.1 '@polkadot/util-crypto': 14.0.1(@polkadot/util@14.0.1) - axios: 1.13.6(debug@4.3.7) + axios: 1.13.6(debug@4.3.7(supports-color@8.1.1)) comlink: 4.4.2 dotenv: 16.6.1 global-agent: 3.0.0 @@ -5559,9 +5559,9 @@ snapshots: '@js-sdsl/ordered-map@4.4.2': {} - '@moonwall/cli@5.18.3(@polkadot/api-base@16.5.4)(@polkadot/api-derive@16.5.4)(@polkadot/api@16.5.4)(@polkadot/keyring@14.0.1(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1))(@polkadot/rpc-provider@16.5.4)(@polkadot/types-codec@16.5.4)(@polkadot/types@16.5.4)(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1)(@types/debug@4.1.12)(@types/node@25.3.5)(chokidar@3.6.0)(debug@4.3.7)(encoding@0.1.13)(jsdom@23.2.0)(postcss@8.5.8)(rxjs@7.8.2)(ts-node@10.9.2(@types/node@25.3.5)(typescript@5.8.3))(tsx@4.21.0)(typescript@5.8.3)(zod@3.25.76)': + '@moonwall/cli@5.18.3(@polkadot/api-base@16.5.4)(@polkadot/api-derive@16.5.4)(@polkadot/api@16.5.4)(@polkadot/keyring@14.0.1(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1))(@polkadot/rpc-provider@16.5.4)(@polkadot/types-codec@16.5.4)(@polkadot/types@16.5.4)(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1)(@types/debug@4.1.12)(@types/node@25.3.5)(chokidar@3.6.0)(debug@4.3.7(supports-color@8.1.1))(encoding@0.1.13)(jsdom@23.2.0)(postcss@8.5.8)(rxjs@7.8.2)(supports-color@8.1.1)(ts-node@10.9.2(@types/node@25.3.5)(typescript@5.8.3))(tsx@4.21.0)(typescript@5.8.3)(zod@3.25.76)': dependencies: - '@acala-network/chopsticks': 1.2.7(debug@4.3.7)(ts-node@10.9.2(@types/node@25.3.5)(typescript@5.8.3)) + '@acala-network/chopsticks': 1.2.7(debug@4.3.7(supports-color@8.1.1))(ts-node@10.9.2(@types/node@25.3.5)(typescript@5.8.3)) '@ast-grep/napi': 0.40.5 '@effect/cluster': 0.55.0(@effect/platform@0.93.8(effect@3.19.19))(@effect/rpc@0.72.2(@effect/platform@0.93.8(effect@3.19.19))(effect@3.19.19))(@effect/sql@0.48.6(@effect/experimental@0.57.11(@effect/platform@0.93.8(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.93.8(effect@3.19.19))(effect@3.19.19))(@effect/workflow@0.15.2(@effect/experimental@0.57.11(@effect/platform@0.93.8(effect@3.19.19))(effect@3.19.19))(@effect/platform@0.93.8(effect@3.19.19))(@effect/rpc@0.72.2(@effect/platform@0.93.8(effect@3.19.19))(effect@3.19.19))(effect@3.19.19))(effect@3.19.19) '@effect/experimental': 0.57.11(@effect/platform@0.93.8(effect@3.19.19))(effect@3.19.19) @@ -5594,7 +5594,7 @@ snapshots: clear: 0.1.0 cli-progress: 3.12.0 colors: 1.4.0 - dockerode: 4.0.9 + dockerode: 4.0.9(supports-color@8.1.1) dotenv: 17.2.3 effect: 3.19.19 ethers: 6.16.0 @@ -6311,7 +6311,7 @@ snapshots: '@polkadot/api-augment': 14.3.1 '@polkadot/api-base': 14.3.1 '@polkadot/api-derive': 14.3.1 - '@polkadot/keyring': 13.5.9(@polkadot/util-crypto@13.5.9(@polkadot/util@14.0.1))(@polkadot/util@13.5.9) + '@polkadot/keyring': 13.5.9(@polkadot/util-crypto@13.5.9(@polkadot/util@13.5.9))(@polkadot/util@13.5.9) '@polkadot/rpc-augment': 14.3.1 '@polkadot/rpc-core': 14.3.1 '@polkadot/rpc-provider': 14.3.1 @@ -6354,10 +6354,10 @@ snapshots: - supports-color - utf-8-validate - '@polkadot/keyring@13.5.9(@polkadot/util-crypto@13.5.9(@polkadot/util@14.0.1))(@polkadot/util@13.5.9)': + '@polkadot/keyring@13.5.9(@polkadot/util-crypto@13.5.9(@polkadot/util@13.5.9))(@polkadot/util@13.5.9)': dependencies: '@polkadot/util': 13.5.9 - '@polkadot/util-crypto': 13.5.9(@polkadot/util@14.0.1) + '@polkadot/util-crypto': 13.5.9(@polkadot/util@13.5.9) tslib: 2.8.1 '@polkadot/keyring@13.5.9(@polkadot/util-crypto@13.5.9(@polkadot/util@14.0.1))(@polkadot/util@14.0.1)': @@ -6436,7 +6436,7 @@ snapshots: '@polkadot/rpc-provider@14.3.1': dependencies: - '@polkadot/keyring': 13.5.9(@polkadot/util-crypto@13.5.9(@polkadot/util@14.0.1))(@polkadot/util@13.5.9) + '@polkadot/keyring': 13.5.9(@polkadot/util-crypto@13.5.9(@polkadot/util@13.5.9))(@polkadot/util@13.5.9) '@polkadot/types': 14.3.1 '@polkadot/types-support': 14.3.1 '@polkadot/util': 13.5.9 @@ -6544,7 +6544,7 @@ snapshots: '@polkadot/types@14.3.1': dependencies: - '@polkadot/keyring': 13.5.9(@polkadot/util-crypto@13.5.9(@polkadot/util@14.0.1))(@polkadot/util@13.5.9) + '@polkadot/keyring': 13.5.9(@polkadot/util-crypto@13.5.9(@polkadot/util@13.5.9))(@polkadot/util@13.5.9) '@polkadot/types-augment': 14.3.1 '@polkadot/types-codec': 14.3.1 '@polkadot/types-create': 14.3.1 @@ -6570,10 +6570,10 @@ snapshots: '@noble/hashes': 1.8.0 '@polkadot/networks': 13.5.9 '@polkadot/util': 13.5.9 - '@polkadot/wasm-crypto': 7.5.4(@polkadot/util@13.5.9)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1))) + '@polkadot/wasm-crypto': 7.5.4(@polkadot/util@13.5.9)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9))) '@polkadot/wasm-util': 7.5.4(@polkadot/util@13.5.9) '@polkadot/x-bigint': 13.5.9 - '@polkadot/x-randomvalues': 13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)) + '@polkadot/x-randomvalues': 13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9)) '@scure/base': 1.2.6 tslib: 2.8.1 @@ -6624,11 +6624,11 @@ snapshots: bn.js: 5.2.3 tslib: 2.8.1 - '@polkadot/wasm-bridge@7.5.4(@polkadot/util@13.5.9)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)))': + '@polkadot/wasm-bridge@7.5.4(@polkadot/util@13.5.9)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9)))': dependencies: '@polkadot/util': 13.5.9 '@polkadot/wasm-util': 7.5.4(@polkadot/util@13.5.9) - '@polkadot/x-randomvalues': 13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)) + '@polkadot/x-randomvalues': 13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9)) tslib: 2.8.1 '@polkadot/wasm-bridge@7.5.4(@polkadot/util@14.0.1)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@14.0.1)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)))': @@ -6655,14 +6655,14 @@ snapshots: '@polkadot/util': 14.0.1 tslib: 2.8.1 - '@polkadot/wasm-crypto-init@7.5.4(@polkadot/util@13.5.9)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)))': + '@polkadot/wasm-crypto-init@7.5.4(@polkadot/util@13.5.9)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9)))': dependencies: '@polkadot/util': 13.5.9 - '@polkadot/wasm-bridge': 7.5.4(@polkadot/util@13.5.9)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1))) + '@polkadot/wasm-bridge': 7.5.4(@polkadot/util@13.5.9)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9))) '@polkadot/wasm-crypto-asmjs': 7.5.4(@polkadot/util@13.5.9) '@polkadot/wasm-crypto-wasm': 7.5.4(@polkadot/util@13.5.9) '@polkadot/wasm-util': 7.5.4(@polkadot/util@13.5.9) - '@polkadot/x-randomvalues': 13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)) + '@polkadot/x-randomvalues': 13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9)) tslib: 2.8.1 '@polkadot/wasm-crypto-init@7.5.4(@polkadot/util@14.0.1)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@14.0.1)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)))': @@ -6697,15 +6697,15 @@ snapshots: '@polkadot/wasm-util': 7.5.4(@polkadot/util@14.0.1) tslib: 2.8.1 - '@polkadot/wasm-crypto@7.5.4(@polkadot/util@13.5.9)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)))': + '@polkadot/wasm-crypto@7.5.4(@polkadot/util@13.5.9)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9)))': dependencies: '@polkadot/util': 13.5.9 - '@polkadot/wasm-bridge': 7.5.4(@polkadot/util@13.5.9)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1))) + '@polkadot/wasm-bridge': 7.5.4(@polkadot/util@13.5.9)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9))) '@polkadot/wasm-crypto-asmjs': 7.5.4(@polkadot/util@13.5.9) - '@polkadot/wasm-crypto-init': 7.5.4(@polkadot/util@13.5.9)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1))) + '@polkadot/wasm-crypto-init': 7.5.4(@polkadot/util@13.5.9)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9))) '@polkadot/wasm-crypto-wasm': 7.5.4(@polkadot/util@13.5.9) '@polkadot/wasm-util': 7.5.4(@polkadot/util@13.5.9) - '@polkadot/x-randomvalues': 13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)) + '@polkadot/x-randomvalues': 13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9)) tslib: 2.8.1 '@polkadot/wasm-crypto@7.5.4(@polkadot/util@14.0.1)(@polkadot/x-randomvalues@13.5.9(@polkadot/util@14.0.1)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)))': @@ -6770,10 +6770,10 @@ snapshots: dependencies: tslib: 2.8.1 - '@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1))': + '@polkadot/x-randomvalues@13.5.9(@polkadot/util@13.5.9)(@polkadot/wasm-util@7.5.4(@polkadot/util@13.5.9))': dependencies: '@polkadot/util': 13.5.9 - '@polkadot/wasm-util': 7.5.4(@polkadot/util@14.0.1) + '@polkadot/wasm-util': 7.5.4(@polkadot/util@13.5.9) '@polkadot/x-global': 13.5.9 tslib: 2.8.1 @@ -7158,12 +7158,12 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@zombienet/orchestrator@0.0.105(@polkadot/util@14.0.1)(@types/node@25.3.5)(chokidar@3.6.0)': + '@zombienet/orchestrator@0.0.105(@polkadot/util@14.0.1)(@types/node@25.3.5)(chokidar@3.6.0)(supports-color@8.1.1)': dependencies: '@polkadot/api': 14.3.1 '@polkadot/keyring': 13.5.9(@polkadot/util-crypto@13.5.9(@polkadot/util@14.0.1))(@polkadot/util@14.0.1) '@polkadot/util-crypto': 13.5.9(@polkadot/util@14.0.1) - '@zombienet/utils': 0.0.28(@types/node@25.3.5)(chokidar@3.6.0)(typescript@5.8.3) + '@zombienet/utils': 0.0.28(@types/node@25.3.5)(chokidar@3.6.0)(supports-color@8.1.1)(typescript@5.8.3) JSONStream: 1.3.5 chai: 4.5.0 debug: 4.3.7(supports-color@8.1.1) @@ -7222,13 +7222,13 @@ snapshots: - supports-color - utf-8-validate - '@zombienet/utils@0.0.28(@types/node@25.3.5)(chokidar@3.6.0)(typescript@5.8.3)': + '@zombienet/utils@0.0.28(@types/node@25.3.5)(chokidar@3.6.0)(supports-color@8.1.1)(typescript@5.8.3)': dependencies: cli-table3: 0.6.5 debug: 4.3.7(supports-color@8.1.1) mocha: 10.8.2 nunjucks: 3.2.4(chokidar@3.6.0) - toml: https://codeload.github.com/pepoviola/toml-node/tar.gz/5e17114f1af5b5b70e4f2ec10cd007623c928988 + toml: 3.0.0 ts-node: 10.9.2(@types/node@25.3.5)(typescript@5.8.3) transitivePeerDependencies: - '@swc/core' @@ -7244,7 +7244,7 @@ snapshots: debug: 4.4.3 mocha: 10.8.2 nunjucks: 3.2.4(chokidar@3.6.0) - toml: https://codeload.github.com/pepoviola/toml-node/tar.gz/5e17114f1af5b5b70e4f2ec10cd007623c928988 + toml: 3.0.0 ts-node: 10.9.2(@types/node@24.12.0)(typescript@5.8.3) transitivePeerDependencies: - '@swc/core' @@ -7260,7 +7260,7 @@ snapshots: debug: 4.4.3 mocha: 10.8.2 nunjucks: 3.2.4(chokidar@3.6.0) - toml: https://codeload.github.com/pepoviola/toml-node/tar.gz/5e17114f1af5b5b70e4f2ec10cd007623c928988 + toml: 3.0.0 ts-node: 10.9.2(@types/node@25.3.5)(typescript@5.8.3) transitivePeerDependencies: - '@swc/core' @@ -7383,9 +7383,9 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - axios@1.13.6(debug@4.3.7): + axios@1.13.6(debug@4.3.7(supports-color@8.1.1)): dependencies: - follow-redirects: 1.15.11(debug@4.3.7) + follow-redirects: 1.15.11(debug@4.3.7(supports-color@8.1.1)) form-data: 4.0.5 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -7781,7 +7781,7 @@ snapshots: diff@5.2.2: {} - docker-modem@5.0.6: + docker-modem@5.0.6(supports-color@8.1.1): dependencies: debug: 4.3.7(supports-color@8.1.1) readable-stream: 3.6.2 @@ -7790,12 +7790,12 @@ snapshots: transitivePeerDependencies: - supports-color - dockerode@4.0.9: + dockerode@4.0.9(supports-color@8.1.1): dependencies: '@balena/dockerignore': 1.0.2 '@grpc/grpc-js': 1.14.3 '@grpc/proto-loader': 0.7.15 - docker-modem: 5.0.6 + docker-modem: 5.0.6(supports-color@8.1.1) protobufjs: 7.5.4 tar-fs: 2.1.4 uuid: 10.0.0 @@ -8029,7 +8029,7 @@ snapshots: flatted@3.3.4: {} - follow-redirects@1.15.11(debug@4.3.7): + follow-redirects@1.15.11(debug@4.3.7(supports-color@8.1.1)): optionalDependencies: debug: 4.3.7(supports-color@8.1.1) @@ -9412,11 +9412,11 @@ snapshots: smart-buffer: 4.2.0 optional: true - solc@0.8.21(debug@4.3.7): + solc@0.8.21(debug@4.3.7(supports-color@8.1.1)): dependencies: command-exists: 1.2.9 commander: 8.3.0 - follow-redirects: 1.15.11(debug@4.3.7) + follow-redirects: 1.15.11(debug@4.3.7(supports-color@8.1.1)) js-sha3: 0.8.0 memorystream: 0.3.1 semver: 5.7.2 @@ -9651,8 +9651,6 @@ snapshots: toml@3.0.0: {} - toml@https://codeload.github.com/pepoviola/toml-node/tar.gz/5e17114f1af5b5b70e4f2ec10cd007623c928988: {} - totalist@3.0.1: {} tough-cookie@4.1.4: diff --git a/ts-tests/pnpm-workspace.yaml b/ts-tests/pnpm-workspace.yaml index 856299a3ed..be232d6643 100644 --- a/ts-tests/pnpm-workspace.yaml +++ b/ts-tests/pnpm-workspace.yaml @@ -1,6 +1,21 @@ packages: - "**" +overrides: + toml: 3.0.0 + +strictDepBuilds: false + +allowBuilds: + '@biomejs/biome': set this to true or false + '@parcel/watcher': set this to true or false + cpu-features: set this to true or false + esbuild: set this to true or false + msgpackr-extract: set this to true or false + protobufjs: set this to true or false + sqlite3: set this to true or false + ssh2: set this to true or false + onlyBuiltDependencies: - '@biomejs/biome' - '@chainsafe/blst' diff --git a/ts-tests/scripts/build-spec.sh b/ts-tests/scripts/build-spec.sh index 8ef4e40b96..9356b5fc5c 100755 --- a/ts-tests/scripts/build-spec.sh +++ b/ts-tests/scripts/build-spec.sh @@ -4,6 +4,8 @@ set -e cd $(dirname $0)/.. +# Clean vitest cache, so the tests order are the same on CI and locally +rm -rf node_modules/.vite/vitest mkdir -p specs ../target/release/node-subtensor build-spec --disable-default-bootnode --raw --chain local > specs/chain-spec.json \ No newline at end of file diff --git a/ts-tests/suites/dev/subtensor/limit-orders/test-batched-all-buys.ts b/ts-tests/suites/dev/subtensor/limit-orders/test-batched-all-buys.ts new file mode 100644 index 0000000000..1d2bf9b4a8 --- /dev/null +++ b/ts-tests/suites/dev/subtensor/limit-orders/test-batched-all-buys.ts @@ -0,0 +1,99 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { ApiPromise } from "@polkadot/api"; +import type { KeyringPair } from "@moonwall/util"; +import { tao, generateKeyringPair } from "../../../../utils"; +import { + devForceSetBalance, + devGetAlphaStake, + devAssociateHotKey, + devEnableSubtoken, + devRegisterSubnet, + devSudoSetLockReductionInterval, +} from "../../../../utils/dev-helpers.js"; +import { buildSignedOrder, FAR_FUTURE, filterEvents, registerLimitOrderTypes } from "../../../../utils/limit-orders.js"; + +// execute_batched_orders — all-buy batch. Own subnet, own file. + +describeSuite({ + id: "DEV_SUB_LIMIT_ORDERS_BATCH_BUY", + title: "execute_batched_orders — all-buy batch", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let aliceHotKey: KeyringPair; + let bob: KeyringPair; + let bobHotKey: KeyringPair; + let netuid: number; + + beforeAll(async () => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + aliceHotKey = generateKeyringPair("sr25519"); + bob = context.keyring.bob; + bobHotKey = generateKeyringPair("sr25519"); + + registerLimitOrderTypes(polkadotJs); + + await devForceSetBalance(polkadotJs, context, alice.address, tao(10_000)); + await devForceSetBalance(polkadotJs, context, bob.address, tao(10_000)); + + await devSudoSetLockReductionInterval(polkadotJs, context, alice, 1); + + netuid = await devRegisterSubnet(polkadotJs, context, alice, aliceHotKey); + + await devEnableSubtoken(polkadotJs, context, alice, netuid); + await devAssociateHotKey(polkadotJs, context, alice, aliceHotKey.address); + await devAssociateHotKey(polkadotJs, context, bob, bobHotKey.address); + }); + + it({ + id: "T01", + title: "all buyers receive alpha and GroupExecutionSummary is emitted", + test: async () => { + const aliceStakeBefore = await devGetAlphaStake(polkadotJs, aliceHotKey.address, alice.address, netuid); + const bobStakeBefore = await devGetAlphaStake(polkadotJs, bobHotKey.address, bob.address, netuid); + + const orderAlice = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(50), + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + const orderBob = buildSignedOrder(polkadotJs, { + signer: bob, + hotkey: bobHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(50), + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: bob.address, + }); + + await context.createBlock([ + await polkadotJs.tx.limitOrders + .executeBatchedOrders(netuid, [orderAlice, orderBob]) + .signAsync(alice), + ]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderExecuted").length).toBe(2); + expect(filterEvents(events, "GroupExecutionSummary").length).toBe(1); + + const aliceStakeAfter = await devGetAlphaStake(polkadotJs, aliceHotKey.address, alice.address, netuid); + expect(aliceStakeAfter).toBeGreaterThan(aliceStakeBefore); + + const bobStakeAfter = await devGetAlphaStake(polkadotJs, bobHotKey.address, bob.address, netuid); + expect(bobStakeAfter).toBeGreaterThan(bobStakeBefore); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/limit-orders/test-batched-all-sells.ts b/ts-tests/suites/dev/subtensor/limit-orders/test-batched-all-sells.ts new file mode 100644 index 0000000000..9ce3fa0c2e --- /dev/null +++ b/ts-tests/suites/dev/subtensor/limit-orders/test-batched-all-sells.ts @@ -0,0 +1,105 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { ApiPromise } from "@polkadot/api"; +import type { KeyringPair } from "@moonwall/util"; +import { tao, generateKeyringPair } from "../../../../utils"; +import { + devForceSetBalance, + devAddStake, + devAssociateHotKey, + devEnableSubtoken, + devRegisterSubnet, + devSudoSetLockReductionInterval, +} from "../../../../utils/dev-helpers.js"; +import { buildSignedOrder, FAR_FUTURE, filterEvents, registerLimitOrderTypes } from "../../../../utils/limit-orders.js"; + +describeSuite({ + id: "DEV_SUB_LIMIT_ORDERS_BATCH_SELL", + title: "execute_batched_orders — all-sell batch", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let aliceHotKey: KeyringPair; + let bob: KeyringPair; + let bobHotKey: KeyringPair; + let netuid: number; + + beforeAll(async () => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + aliceHotKey = generateKeyringPair("sr25519"); + bob = context.keyring.bob; + bobHotKey = generateKeyringPair("sr25519"); + + registerLimitOrderTypes(polkadotJs); + + await devForceSetBalance(polkadotJs, context, alice.address, tao(10_000)); + await devForceSetBalance(polkadotJs, context, bob.address, tao(10_000)); + + await devSudoSetLockReductionInterval(polkadotJs, context, alice, 1); + + netuid = await devRegisterSubnet(polkadotJs, context, alice, aliceHotKey); + + await devEnableSubtoken(polkadotJs, context, alice, netuid); + await devAssociateHotKey(polkadotJs, context, alice, aliceHotKey.address); + await devAssociateHotKey(polkadotJs, context, bob, bobHotKey.address); + + // Stake alpha for both sellers + await devAddStake(polkadotJs, context, alice, aliceHotKey.address, netuid, tao(200)); + await devAddStake(polkadotJs, context, bob, bobHotKey.address, netuid, tao(200)); + }); + + it({ + id: "T01", + title: "all sellers receive TAO and GroupExecutionSummary is emitted", + test: async () => { + const aliceTaoBefore = ( + (await polkadotJs.query.system.account(alice.address)) as any + ).data.free.toBigInt(); + const bobTaoBefore = ((await polkadotJs.query.system.account(bob.address)) as any).data.free.toBigInt(); + + const orderAlice = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "TakeProfit", + amount: tao(50), + limitPrice: 1_000_000_000n, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + const orderBob = buildSignedOrder(polkadotJs, { + signer: bob, + hotkey: bobHotKey.address, + netuid, + orderType: "TakeProfit", + amount: tao(50), + limitPrice: 1_000_000_000n, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: bob.address, + }); + + await context.createBlock([ + await polkadotJs.tx.limitOrders + .executeBatchedOrders(netuid, [orderAlice, orderBob]) + .signAsync(alice), + ]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderExecuted").length).toBe(2); + expect(filterEvents(events, "GroupExecutionSummary").length).toBe(1); + + const aliceTaoAfter = ( + (await polkadotJs.query.system.account(alice.address)) as any + ).data.free.toBigInt(); + const bobTaoAfter = ((await polkadotJs.query.system.account(bob.address)) as any).data.free.toBigInt(); + + expect(aliceTaoAfter).toBeGreaterThan(aliceTaoBefore); + expect(bobTaoAfter).toBeGreaterThan(bobTaoBefore); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/limit-orders/test-batched-fees.ts b/ts-tests/suites/dev/subtensor/limit-orders/test-batched-fees.ts new file mode 100644 index 0000000000..48be9461c4 --- /dev/null +++ b/ts-tests/suites/dev/subtensor/limit-orders/test-batched-fees.ts @@ -0,0 +1,168 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { ApiPromise } from "@polkadot/api"; +import type { KeyringPair } from "@moonwall/util"; +import { generateKeyringPair, tao } from "../../../../utils"; +import { + devForceSetBalance, + devAssociateHotKey, + devEnableSubtoken, + devRegisterSubnet, + devSudoSetLockReductionInterval, +} from "../../../../utils/dev-helpers.js"; +import { + buildSignedOrder, + FAR_FUTURE, + filterEvents, + PERBILL_ONE_PERCENT, + registerLimitOrderTypes, +} from "../../../../utils/limit-orders.js"; + +// Batched buy orders with fee recipients — own file, hits pool. + +describeSuite({ + id: "DEV_SUB_LIMIT_ORDERS_BATCH_FEES", + title: "execute_batched_orders — fee collection", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let aliceHotKey: KeyringPair; + let bob: KeyringPair; + let bobHotKey: KeyringPair; + let netuid: number; + + beforeAll(async () => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + aliceHotKey = generateKeyringPair("sr25519"); + bob = context.keyring.bob; + bobHotKey = generateKeyringPair("sr25519"); + + registerLimitOrderTypes(polkadotJs); + + await devForceSetBalance(polkadotJs, context, alice.address, tao(10_000)); + await devForceSetBalance(polkadotJs, context, bob.address, tao(10_000)); + + await devSudoSetLockReductionInterval(polkadotJs, context, alice, 1); + + netuid = await devRegisterSubnet(polkadotJs, context, alice, aliceHotKey); + + await devEnableSubtoken(polkadotJs, context, alice, netuid); + await devAssociateHotKey(polkadotJs, context, alice, aliceHotKey.address); + await devAssociateHotKey(polkadotJs, context, bob, bobHotKey.address); + }); + + it({ + id: "T01", + title: "unique fee recipients each receive their own fee", + test: async () => { + const feeRecipient1 = generateKeyringPair(); + const feeRecipient2 = generateKeyringPair(); + + const r1Before = ( + (await polkadotJs.query.system.account(feeRecipient1.address)) as any + ).data.free.toBigInt(); + const r2Before = ( + (await polkadotJs.query.system.account(feeRecipient2.address)) as any + ).data.free.toBigInt(); + + const orderAlice = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(100), + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: PERBILL_ONE_PERCENT, + feeRecipient: feeRecipient1.address, + }); + + const orderBob = buildSignedOrder(polkadotJs, { + signer: bob, + hotkey: bobHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(100), + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: PERBILL_ONE_PERCENT, + feeRecipient: feeRecipient2.address, + }); + + await context.createBlock([ + await polkadotJs.tx.limitOrders + .executeBatchedOrders(netuid, [orderAlice, orderBob]) + .signAsync(alice), + ]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderExecuted").length).toBe(2); + + const r1After = ( + (await polkadotJs.query.system.account(feeRecipient1.address)) as any + ).data.free.toBigInt(); + const r2After = ( + (await polkadotJs.query.system.account(feeRecipient2.address)) as any + ).data.free.toBigInt(); + + // Both recipients must have received some fee + expect(r1After).toBeGreaterThan(r1Before); + expect(r2After).toBeGreaterThan(r2Before); + }, + }); + + it({ + id: "T02", + title: "shared fee recipient receives aggregated fee", + test: async () => { + const sharedRecipient = generateKeyringPair(); + + const recipientBefore = ( + (await polkadotJs.query.system.account(sharedRecipient.address)) as any + ).data.free.toBigInt(); + + const orderAlice = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(100), + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: PERBILL_ONE_PERCENT, + feeRecipient: sharedRecipient.address, + }); + + const orderBob = buildSignedOrder(polkadotJs, { + signer: bob, + hotkey: bobHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(100), + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: PERBILL_ONE_PERCENT, + feeRecipient: sharedRecipient.address, + }); + + await context.createBlock([ + await polkadotJs.tx.limitOrders + .executeBatchedOrders(netuid, [orderAlice, orderBob]) + .signAsync(alice), + ]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderExecuted").length).toBe(2); + + const recipientAfter = ( + (await polkadotJs.query.system.account(sharedRecipient.address)) as any + ).data.free.toBigInt(); + + // Should have received fees from both orders in a single transfer + const expectedFee = tao(100) / 100n + tao(100) / 100n; // 1% * 2 + expect(recipientAfter - recipientBefore).toBe(expectedFee); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/limit-orders/test-batched-hardfail.ts b/ts-tests/suites/dev/subtensor/limit-orders/test-batched-hardfail.ts new file mode 100644 index 0000000000..f36f845efe --- /dev/null +++ b/ts-tests/suites/dev/subtensor/limit-orders/test-batched-hardfail.ts @@ -0,0 +1,156 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { ApiPromise } from "@polkadot/api"; +import type { KeyringPair } from "@moonwall/util"; +import { tao, generateKeyringPair } from "../../../../utils"; +import { + devForceSetBalance, + devAssociateHotKey, + devEnableSubtoken, + devRegisterSubnet, + devSudoSetLockReductionInterval, +} from "../../../../utils/dev-helpers.js"; +import { buildSignedOrder, FAR_FUTURE, registerLimitOrderTypes } from "../../../../utils/limit-orders.js"; + +// Hard-fail cases for execute_batched_orders — no pool interaction needed, +// all batches fail before reaching the swap step. Single subnet is fine. + +describeSuite({ + id: "DEV_SUB_LIMIT_ORDERS_BATCH_HARDFAIL", + title: "execute_batched_orders — hard-fail conditions", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let aliceHotKey: KeyringPair; + let netuid: number; + + beforeAll(async () => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + aliceHotKey = generateKeyringPair("sr25519"); + + registerLimitOrderTypes(polkadotJs); + + await devForceSetBalance(polkadotJs, context, alice.address, tao(10_000)); + await devSudoSetLockReductionInterval(polkadotJs, context, alice, 1); + + netuid = await devRegisterSubnet(polkadotJs, context, alice, aliceHotKey); + + await devEnableSubtoken(polkadotJs, context, alice, netuid); + await devAssociateHotKey(polkadotJs, context, alice, aliceHotKey.address); + }); + + it({ + id: "T01", + title: "batch fails entirely when one order has an invalid signature", + test: async () => { + const valid = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(1), + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + const badSig = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(2), + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + // Tamper after signing — signature now covers different bytes + const tampered = { + ...badSig, + order: { V1: { ...badSig.order.V1, amount: tao(999) } }, + }; + + const { + result: [attempt], + } = await context.createBlock([ + await polkadotJs.tx.limitOrders.executeBatchedOrders(netuid, [valid, tampered]).signAsync(alice), + ]); + + // The whole extrinsic should fail — hard-fail on invalid signature + expect(attempt.successful).toEqual(false); + expect(attempt.error.name).toEqual("InvalidSignature"); + }, + }); + + it({ + id: "T02", + title: "batch fails when one order targets a different netuid", + test: async () => { + const correct = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(1), + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + const wrongNetuid = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid: netuid + 1, // different subnet + orderType: "LimitBuy", + amount: tao(2), + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + const { + result: [attempt], + } = await context.createBlock([ + await polkadotJs.tx.limitOrders + .executeBatchedOrders(netuid, [correct, wrongNetuid]) + .signAsync(alice), + ]); + + expect(attempt.successful).toEqual(false); + expect(attempt.error.name).toEqual("OrderNetUidMismatch"); + }, + }); + + it({ + id: "T03", + title: "root netuid (0) as batch parameter fails immediately", + test: async () => { + const order = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid: 0, + orderType: "LimitBuy", + amount: tao(1), + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + const { + result: [attempt], + } = await context.createBlock([ + await polkadotJs.tx.limitOrders.executeBatchedOrders(0, [order]).signAsync(alice), + ]); + + expect(attempt.successful).toEqual(false); + expect(attempt.error.name).toEqual("RootNetUidNotAllowed"); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/limit-orders/test-batched-mixed-buy-dominant.ts b/ts-tests/suites/dev/subtensor/limit-orders/test-batched-mixed-buy-dominant.ts new file mode 100644 index 0000000000..ed846b0b07 --- /dev/null +++ b/ts-tests/suites/dev/subtensor/limit-orders/test-batched-mixed-buy-dominant.ts @@ -0,0 +1,124 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { ApiPromise } from "@polkadot/api"; +import type { KeyringPair } from "@moonwall/util"; +import { tao, generateKeyringPair } from "../../../../utils"; +import { + devForceSetBalance, + devAddStake, + devGetAlphaStake, + devAssociateHotKey, + devEnableSubtoken, + devRegisterSubnet, + devSudoSetLockReductionInterval, +} from "../../../../utils/dev-helpers.js"; +import { + buildSignedOrder, + computeNetAmount, + FAR_FUTURE, + filterEvents, + registerLimitOrderTypes, +} from "../../../../utils/limit-orders.js"; + +// Buy-dominant mixed batch — net buy hits the pool. Own file. + +describeSuite({ + id: "DEV_SUB_LIMIT_ORDERS_BATCH_MIX_BUY", + title: "execute_batched_orders — buy-dominant mixed batch", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let aliceHotKey: KeyringPair; + let bob: KeyringPair; + let bobHotKey: KeyringPair; + let netuid: number; + + beforeAll(async () => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + aliceHotKey = generateKeyringPair("sr25519"); + bob = context.keyring.bob; + bobHotKey = generateKeyringPair("sr25519"); + + registerLimitOrderTypes(polkadotJs); + + await devForceSetBalance(polkadotJs, context, alice.address, tao(10_000)); + await devForceSetBalance(polkadotJs, context, bob.address, tao(10_000)); + + await devSudoSetLockReductionInterval(polkadotJs, context, alice, 1); + + netuid = await devRegisterSubnet(polkadotJs, context, alice, aliceHotKey); + + await devEnableSubtoken(polkadotJs, context, alice, netuid); + await devAssociateHotKey(polkadotJs, context, alice, aliceHotKey.address); + await devAssociateHotKey(polkadotJs, context, bob, bobHotKey.address); + + // Bob sells, needs alpha + await devAddStake(polkadotJs, context, bob, bobHotKey.address, netuid, tao(200)); + }); + + it({ + id: "T01", + title: "buy side dominates: both orders fulfilled, net buy hits pool", + test: async () => { + const aliceStakeBefore = await devGetAlphaStake(polkadotJs, aliceHotKey.address, alice.address, netuid); + const bobTaoBefore = ((await polkadotJs.query.system.account(bob.address)) as any).data.free.toBigInt(); + + // Alice buys 200 TAO worth, Bob sells 10 alpha (~10 TAO equiv) + // → net buy ~190 TAO hits the pool + const buyOrder = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(200), + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + const sellOrder = buildSignedOrder(polkadotJs, { + signer: bob, + hotkey: bobHotKey.address, + netuid, + orderType: "TakeProfit", + amount: tao(10), + limitPrice: 1_000_000_000n, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: bob.address, + }); + + // Read price before the swap — pallet uses pre-swap price for netting + const expectedNetAmount = await computeNetAmount(polkadotJs, netuid, tao(200), tao(10), "Buy"); + + await context.createBlock([ + await polkadotJs.tx.limitOrders + .executeBatchedOrders(netuid, [buyOrder, sellOrder]) + .signAsync(alice), + ]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderExecuted").length).toBe(2); + + const summary = filterEvents(events, "GroupExecutionSummary"); + expect(summary.length).toBe(1); + const summaryData = summary[0].event.data; + // net_side should be Buy (residual TAO sent to pool) + expect(summaryData[1].type).toBe("Buy"); + // net_amount matches buy_tao - alpha_to_tao(sell_alpha, price) + const netAmountDiff = summaryData[2].toBigInt() - expectedNetAmount; + expect(netAmountDiff < 0n ? -netAmountDiff : netAmountDiff).toBeLessThanOrEqual(10n); + // actual_out > 0 proves the pool returned alpha + expect(summaryData[3].toBigInt()).toBeGreaterThan(0n); + + const aliceStakeAfter = await devGetAlphaStake(polkadotJs, aliceHotKey.address, alice.address, netuid); + expect(aliceStakeAfter).toBeGreaterThan(aliceStakeBefore); + + const bobTaoAfter = ((await polkadotJs.query.system.account(bob.address)) as any).data.free.toBigInt(); + expect(bobTaoAfter).toBeGreaterThan(bobTaoBefore); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/limit-orders/test-batched-mixed-sell-dominant.ts b/ts-tests/suites/dev/subtensor/limit-orders/test-batched-mixed-sell-dominant.ts new file mode 100644 index 0000000000..b4eb8b19d2 --- /dev/null +++ b/ts-tests/suites/dev/subtensor/limit-orders/test-batched-mixed-sell-dominant.ts @@ -0,0 +1,122 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { ApiPromise } from "@polkadot/api"; +import type { KeyringPair } from "@moonwall/util"; +import { tao, generateKeyringPair } from "../../../../utils"; +import { + devForceSetBalance, + devAddStake, + devGetAlphaStake, + devAssociateHotKey, + devEnableSubtoken, + devRegisterSubnet, + devSudoSetLockReductionInterval, +} from "../../../../utils/dev-helpers.js"; +import { + buildSignedOrder, + computeNetAmount, + FAR_FUTURE, + filterEvents, + registerLimitOrderTypes, +} from "../../../../utils/limit-orders.js"; + +describeSuite({ + id: "DEV_SUB_LIMIT_ORDERS_BATCH_MIX_SELL", + title: "execute_batched_orders — sell-dominant mixed batch", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let aliceHotKey: KeyringPair; + let bob: KeyringPair; + let bobHotKey: KeyringPair; + let netuid: number; + + beforeAll(async () => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + aliceHotKey = generateKeyringPair("sr25519"); + bob = context.keyring.bob; + bobHotKey = generateKeyringPair("sr25519"); + + registerLimitOrderTypes(polkadotJs); + + await devForceSetBalance(polkadotJs, context, alice.address, tao(10_000)); + await devForceSetBalance(polkadotJs, context, bob.address, tao(10_000)); + + await devSudoSetLockReductionInterval(polkadotJs, context, alice, 1); + + netuid = await devRegisterSubnet(polkadotJs, context, alice, aliceHotKey); + + await devEnableSubtoken(polkadotJs, context, alice, netuid); + await devAssociateHotKey(polkadotJs, context, alice, aliceHotKey.address); + await devAssociateHotKey(polkadotJs, context, bob, bobHotKey.address); + + // Bob sells a large amount, needs alpha + await devAddStake(polkadotJs, context, bob, bobHotKey.address, netuid, tao(500)); + }); + + it({ + id: "T01", + title: "sell side dominates: both orders fulfilled, net sell hits pool", + test: async () => { + const aliceStakeBefore = await devGetAlphaStake(polkadotJs, aliceHotKey.address, alice.address, netuid); + const bobTaoBefore = ((await polkadotJs.query.system.account(bob.address)) as any).data.free.toBigInt(); + + // Alice buys 10 TAO, Bob sells 200 alpha (~200 TAO equiv) + // → net sell ~190 alpha hits the pool + const buyOrder = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(10), + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + const sellOrder = buildSignedOrder(polkadotJs, { + signer: bob, + hotkey: bobHotKey.address, + netuid, + orderType: "TakeProfit", + amount: tao(200), + limitPrice: 1_000_000_000n, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: bob.address, + }); + + // Read price before the swap — pallet uses pre-swap price for netting + const expectedNetAmount = await computeNetAmount(polkadotJs, netuid, tao(10), tao(200), "Sell"); + + await context.createBlock([ + await polkadotJs.tx.limitOrders + .executeBatchedOrders(netuid, [buyOrder, sellOrder]) + .signAsync(alice), + ]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderExecuted").length).toBe(2); + + const summary = filterEvents(events, "GroupExecutionSummary"); + expect(summary.length).toBe(1); + const summaryData = summary[0].event.data; + // net_side should be Sell (residual alpha sent to pool) + expect(summaryData[1].type).toBe("Sell"); + // net_amount matches sell_alpha - tao_to_alpha(buy_tao, price) + const netAmountDiff = summaryData[2].toBigInt() - expectedNetAmount; + expect(netAmountDiff < 0n ? -netAmountDiff : netAmountDiff).toBeLessThanOrEqual(10n); + // actual_out > 0 proves the pool returned TAO + expect(summaryData[3].toBigInt()).toBeGreaterThan(0n); + + const aliceStakeAfter = await devGetAlphaStake(polkadotJs, aliceHotKey.address, alice.address, netuid); + expect(aliceStakeAfter).toBeGreaterThan(aliceStakeBefore); + + const bobTaoAfter = ((await polkadotJs.query.system.account(bob.address)) as any).data.free.toBigInt(); + expect(bobTaoAfter).toBeGreaterThan(bobTaoBefore); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/limit-orders/test-batched-partial-fill.ts b/ts-tests/suites/dev/subtensor/limit-orders/test-batched-partial-fill.ts new file mode 100644 index 0000000000..6d1a4637e9 --- /dev/null +++ b/ts-tests/suites/dev/subtensor/limit-orders/test-batched-partial-fill.ts @@ -0,0 +1,142 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { ApiPromise } from "@polkadot/api"; +import type { KeyringPair } from "@moonwall/util"; +import { tao, generateKeyringPair } from "../../../../utils"; +import { + devForceSetBalance, + devGetAlphaStake, + devAssociateHotKey, + devEnableSubtoken, + devRegisterSubnet, + devSudoSetLockReductionInterval, +} from "../../../../utils/dev-helpers.js"; +import { + buildSignedOrder, + FAR_FUTURE, + filterEvents, + getOrderStatus, + getPartiallyFilledAmount, + orderId, + registerLimitOrderTypes, +} from "../../../../utils/limit-orders.js"; + +// Tests for partial fill via execute_batched_orders. +// Same semantics as the execute_orders variant: the signed VersionedOrder +// payload is reused unchanged; only partial_fill on the envelope changes. + +describeSuite({ + id: "DEV_SUB_LIMIT_ORDERS_BATCH_PARTIAL_FILL", + title: "execute_batched_orders — partial fill", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let aliceHotKey: KeyringPair; + let netuid: number; + + beforeAll(async () => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + aliceHotKey = generateKeyringPair("sr25519"); + + registerLimitOrderTypes(polkadotJs); + + await devForceSetBalance(polkadotJs, context, alice.address, tao(10_000)); + await devSudoSetLockReductionInterval(polkadotJs, context, alice, 1); + + netuid = await devRegisterSubnet(polkadotJs, context, alice, aliceHotKey); + + await devEnableSubtoken(polkadotJs, context, alice, netuid); + await devAssociateHotKey(polkadotJs, context, alice, aliceHotKey.address); + }); + + it({ + id: "T01", + title: "first batched partial fill sets status to PartiallyFilled", + test: async () => { + const orderAmount = tao(100); + const firstFill = Number(tao(50)); + + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: orderAmount, + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + relayer: [alice.address], + partialFillsEnabled: true, + }); + + const id = orderId(polkadotJs, signed.order); + + // Submit first partial fill (50 out of 100 TAO) via execute_batched_orders. + const firstEnvelope = { ...signed, partial_fill: firstFill }; + await context.createBlock([ + await polkadotJs.tx.limitOrders.executeBatchedOrders(netuid, [firstEnvelope]).signAsync(alice), + ]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderExecuted").length).toBe(1); + expect(filterEvents(events, "OrderSkipped").length).toBe(0); + + expect(await getOrderStatus(polkadotJs, id)).toBe("PartiallyFilled"); + const filled = await getPartiallyFilledAmount(polkadotJs, id); + expect(filled).toBe(BigInt(firstFill)); + + // Alpha stake should have increased from the partial buy. + const stakeAfter = await devGetAlphaStake(polkadotJs, aliceHotKey.address, alice.address, netuid); + expect(stakeAfter).toBeGreaterThan(0n); + }, + }); + + it({ + id: "T02", + title: "second batched partial fill completing the order sets status to Fulfilled", + test: async () => { + const orderAmount = tao(200); + const firstFill = Number(tao(100)); + const secondFill = Number(tao(100)); + + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: orderAmount, + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + relayer: [alice.address], + partialFillsEnabled: true, + }); + + const id = orderId(polkadotJs, signed.order); + + // First fill: 100 / 200. + const firstEnvelope = { ...signed, partial_fill: firstFill }; + await context.createBlock([ + await polkadotJs.tx.limitOrders.executeBatchedOrders(netuid, [firstEnvelope]).signAsync(alice), + ]); + + expect(await getOrderStatus(polkadotJs, id)).toBe("PartiallyFilled"); + expect(await getPartiallyFilledAmount(polkadotJs, id)).toBe(BigInt(firstFill)); + + // Second fill: the remaining 100 — completes the order. + const secondEnvelope = { ...signed, partial_fill: secondFill }; + await context.createBlock([ + await polkadotJs.tx.limitOrders.executeBatchedOrders(netuid, [secondEnvelope]).signAsync(alice), + ]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderExecuted").length).toBe(1); + + expect(await getOrderStatus(polkadotJs, id)).toBe("Fulfilled"); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/limit-orders/test-cancel-order.ts b/ts-tests/suites/dev/subtensor/limit-orders/test-cancel-order.ts new file mode 100644 index 0000000000..11c72eaf12 --- /dev/null +++ b/ts-tests/suites/dev/subtensor/limit-orders/test-cancel-order.ts @@ -0,0 +1,145 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { ApiPromise } from "@polkadot/api"; +import type { KeyringPair } from "@moonwall/util"; +import { tao } from "../../../../utils"; +import { devForceSetBalance, devExecuteOrders } from "../../../../utils/dev-helpers.js"; +import { + buildSignedOrder, + FAR_FUTURE, + filterEvents, + getOrderStatus, + orderId, + registerLimitOrderTypes, +} from "../../../../utils/limit-orders.js"; + +describeSuite({ + id: "DEV_SUB_LIMIT_ORDERS_CANCEL", + title: "cancel_order", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let bob: KeyringPair; + let netuid: number; + + beforeAll(async () => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + bob = context.keyring.bob; + + registerLimitOrderTypes(polkadotJs); + await devForceSetBalance(polkadotJs, context, alice.address, tao(1_000)); + }); + + it({ + id: "T01", + title: "signer can cancel their own order", + test: async () => { + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: alice.address, + netuid, + orderType: "LimitBuy", + amount: tao(1), + limitPrice: BigInt(2_000_000_000), + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + const tx = polkadotJs.tx.limitOrders.cancelOrder(signed.order); + await context.createBlock([await tx.signAsync(alice)]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderCancelled").length).toBe(1); + + const id = orderId(polkadotJs, signed.order); + expect(await getOrderStatus(polkadotJs, id)).toBe("Cancelled"); + }, + }); + + it({ + id: "T02", + title: "non-signer cannot cancel another account's order", + test: async () => { + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: alice.address, + netuid, + orderType: "LimitBuy", + amount: tao(2), + limitPrice: BigInt(2_000_000_000), + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + // Bob tries to cancel Alice's order + const tx = polkadotJs.tx.limitOrders.cancelOrder(signed.order); + const { + result: [attempt], + } = await context.createBlock([await tx.signAsync(bob)]); + + expect(attempt.successful).toEqual(false); + expect(attempt.error.name).toEqual("Unauthorized"); + }, + }); + + it({ + id: "T03", + title: "cancelling an already-cancelled order fails", + test: async () => { + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: alice.address, + netuid, + orderType: "LimitBuy", + amount: tao(3), + limitPrice: BigInt(2_000_000_000), + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + const tx = polkadotJs.tx.limitOrders.cancelOrder(signed.order); + await context.createBlock([await tx.signAsync(alice)]); + + // Second cancel must fail + const tx2 = polkadotJs.tx.limitOrders.cancelOrder(signed.order); + await context.createBlock([await tx2.signAsync(alice)]); + + const events = await polkadotJs.query.system.events(); + const cancelled = filterEvents(events, "OrderCancelled"); + expect(cancelled.length).toBe(0); + }, + }); + + it({ + id: "T04", + title: "executing a cancelled order emits OrderSkipped", + test: async () => { + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: alice.address, + netuid, + orderType: "LimitBuy", + amount: tao(4), + limitPrice: BigInt(2_000_000_000), + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + // Cancel first + await context.createBlock([await polkadotJs.tx.limitOrders.cancelOrder(signed.order).signAsync(alice)]); + + // Now try to execute + await devExecuteOrders(polkadotJs, context, alice, [signed]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderSkipped").length).toBe(1); + expect(filterEvents(events, "OrderExecuted").length).toBe(0); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/limit-orders/test-execute-orders-fees.ts b/ts-tests/suites/dev/subtensor/limit-orders/test-execute-orders-fees.ts new file mode 100644 index 0000000000..2945ecb535 --- /dev/null +++ b/ts-tests/suites/dev/subtensor/limit-orders/test-execute-orders-fees.ts @@ -0,0 +1,124 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { ApiPromise } from "@polkadot/api"; +import type { KeyringPair } from "@moonwall/util"; +import { generateKeyringPair, tao } from "../../../../utils"; +import { + devForceSetBalance, + devAssociateHotKey, + devEnableSubtoken, + devRegisterSubnet, + devSudoSetLockReductionInterval, + devExecuteOrders, +} from "../../../../utils/dev-helpers.js"; +import { + buildSignedOrder, + FAR_FUTURE, + filterEvents, + PERBILL_ONE_PERCENT, + registerLimitOrderTypes, +} from "../../../../utils/limit-orders.js"; + +// Each test hits the pool so each gets its own file. +// This file covers fee collection for a buy order only. +// Sell-order fee is covered in 07. + +describeSuite({ + id: "DEV_SUB_LIMIT_ORDERS_FEE_BUY", + title: "execute_orders — buy order fee collection", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let aliceHotKey: KeyringPair; + let bob: KeyringPair; + let feeRecipient: KeyringPair; + let netuid: number; + + beforeAll(async () => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + aliceHotKey = generateKeyringPair(); + bob = context.keyring.bob; + feeRecipient = generateKeyringPair(); + registerLimitOrderTypes(polkadotJs); + + await devForceSetBalance(polkadotJs, context, alice.address, tao(10_000)); + await devForceSetBalance(polkadotJs, context, bob.address, tao(10_000)); + + await devSudoSetLockReductionInterval(polkadotJs, context, alice, 1); + + netuid = await devRegisterSubnet(polkadotJs, context, alice, aliceHotKey); + + // ENable subtoken + await devEnableSubtoken(polkadotJs, context, alice, netuid); + // associate hotkeys + await devAssociateHotKey(polkadotJs, context, alice, aliceHotKey.address); + }); + + it({ + id: "T01", + title: "fee recipient receives TAO for a buy order with 1% fee", + test: async () => { + const recipientBefore = ( + await polkadotJs.query.system.account(feeRecipient.address) + ).data.free.toBigInt(); + + const orderAmount = tao(100); + const expectedFee = orderAmount / 100n; // 1% + + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: orderAmount, + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: PERBILL_ONE_PERCENT, + feeRecipient: feeRecipient.address, + }); + + await devExecuteOrders(polkadotJs, context, alice, [signed]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderExecuted").length).toBe(1); + + const recipientAfter = ( + await polkadotJs.query.system.account(feeRecipient.address) + ).data.free.toBigInt(); + + expect(recipientAfter - recipientBefore).toBe(expectedFee); + }, + }); + + it({ + id: "T02", + title: "zero fee rate — fee recipient balance unchanged", + test: async () => { + const recipientBefore = ( + await polkadotJs.query.system.account(feeRecipient.address) + ).data.free.toBigInt(); + + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(10), + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: feeRecipient.address, + }); + + await devExecuteOrders(polkadotJs, context, alice, [signed]); + + const recipientAfter = ( + await polkadotJs.query.system.account(feeRecipient.address) + ).data.free.toBigInt(); + + expect(recipientAfter).toBe(recipientBefore); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/limit-orders/test-execute-orders-limit-buy.ts b/ts-tests/suites/dev/subtensor/limit-orders/test-execute-orders-limit-buy.ts new file mode 100644 index 0000000000..c1d43601ae --- /dev/null +++ b/ts-tests/suites/dev/subtensor/limit-orders/test-execute-orders-limit-buy.ts @@ -0,0 +1,105 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { ApiPromise } from "@polkadot/api"; +import type { KeyringPair } from "@moonwall/util"; +import { tao, generateKeyringPair } from "../../../../utils"; +import { + devForceSetBalance, + devGetAlphaStake, + devAssociateHotKey, + devEnableSubtoken, + devRegisterSubnet, + devSudoSetLockReductionInterval, + devExecuteOrders, +} from "../../../../utils/dev-helpers.js"; +import { + buildSignedOrder, + FAR_FUTURE, + fetchChainId, + filterEvents, + getOrderStatus, + orderId, + registerLimitOrderTypes, +} from "../../../../utils/limit-orders.js"; + +// One subnet per file — this test submits a real buy order that hits the pool. + +describeSuite({ + id: "DEV_SUB_LIMIT_ORDERS_BUY", + title: "execute_orders — LimitBuy execution", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let aliceHotKey: KeyringPair; + let bob: KeyringPair; + let bobHotKey: KeyringPair; + let netuid: number; + let chainId: bigint; + + beforeAll(async () => { + polkadotJs = context.polkadotJs(); + + alice = context.keyring.alice; + aliceHotKey = generateKeyringPair("sr25519"); + bob = context.keyring.bob; + bobHotKey = generateKeyringPair("sr25519"); + + registerLimitOrderTypes(polkadotJs); + chainId = await fetchChainId(polkadotJs); + + await devForceSetBalance(polkadotJs, context, alice.address, tao(10_000)); + await devForceSetBalance(polkadotJs, context, bob.address, tao(10_000)); + + await devSudoSetLockReductionInterval(polkadotJs, context, alice, 1); + + netuid = await devRegisterSubnet(polkadotJs, context, alice, aliceHotKey); + + // ENable subtoken + await devEnableSubtoken(polkadotJs, context, alice, netuid); + // associate hotkeys + await devAssociateHotKey(polkadotJs, context, alice, aliceHotKey.address); + await devAssociateHotKey(polkadotJs, context, bob, bobHotKey.address); + }); + + it({ + id: "T01", + title: "LimitBuy executes when price condition is met", + test: async () => { + const stakeBefore = await devGetAlphaStake(polkadotJs, aliceHotKey.address, alice.address, netuid); + const taoBalanceBefore = (await polkadotJs.query.system.account(alice.address)).data.free.toBigInt(); + + // TODO: why here far future? + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(100), + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + chainId, + }); + + await devExecuteOrders(polkadotJs, context, alice, [signed]); + + const events = await polkadotJs.query.system.events(); + const executed = filterEvents(events, "OrderExecuted"); + expect(executed.length).toBe(1); + + // OrderId should be stored as Fulfilled + const id = orderId(polkadotJs, signed.order); + expect(await getOrderStatus(polkadotJs, id)).toBe("Fulfilled"); + + // Alpha stake should have increased + const stakeAfter = await devGetAlphaStake(polkadotJs, aliceHotKey.address, alice.address, netuid); + expect(stakeAfter).toBeGreaterThan(stakeBefore); + + // TAO balance should have decreased + const taoBalanceAfter = (await polkadotJs.query.system.account(alice.address)).data.free.toBigInt(); + expect(taoBalanceAfter).toBeLessThan(taoBalanceBefore); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/limit-orders/test-execute-orders-partial-fill.ts b/ts-tests/suites/dev/subtensor/limit-orders/test-execute-orders-partial-fill.ts new file mode 100644 index 0000000000..8e70dd358b --- /dev/null +++ b/ts-tests/suites/dev/subtensor/limit-orders/test-execute-orders-partial-fill.ts @@ -0,0 +1,146 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { ApiPromise } from "@polkadot/api"; +import type { KeyringPair } from "@moonwall/util"; +import { tao, generateKeyringPair } from "../../../../utils"; +import { + devForceSetBalance, + devGetAlphaStake, + devAssociateHotKey, + devEnableSubtoken, + devRegisterSubnet, + devSudoSetLockReductionInterval, +} from "../../../../utils/dev-helpers.js"; +import { + buildSignedOrder, + FAR_FUTURE, + filterEvents, + getOrderStatus, + getPartiallyFilledAmount, + orderId, + registerLimitOrderTypes, +} from "../../../../utils/limit-orders.js"; + +// Tests for partial fill via execute_orders. +// The relayer (alice) submits the same signed payload twice with different +// partial_fill values on the envelope. + +describeSuite({ + id: "DEV_SUB_LIMIT_ORDERS_PARTIAL_FILL", + title: "execute_orders — partial fill", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let aliceHotKey: KeyringPair; + let netuid: number; + + beforeAll(async () => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + aliceHotKey = generateKeyringPair("sr25519"); + + registerLimitOrderTypes(polkadotJs); + + await devForceSetBalance(polkadotJs, context, alice.address, tao(10_000)); + await devSudoSetLockReductionInterval(polkadotJs, context, alice, 1); + + netuid = await devRegisterSubnet(polkadotJs, context, alice, aliceHotKey); + + await devEnableSubtoken(polkadotJs, context, alice, netuid); + await devAssociateHotKey(polkadotJs, context, alice, aliceHotKey.address); + }); + + it({ + id: "T01", + title: "first partial fill sets status to PartiallyFilled", + test: async () => { + const orderAmount = tao(100); + const firstFill = Number(tao(60)); + + // Build a partial-fills-enabled order with alice as relayer. + // The signed VersionedOrder payload is the same for both fills. + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: orderAmount, + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + relayer: [alice.address], + partialFillsEnabled: true, + }); + + const id = orderId(polkadotJs, signed.order); + + // Submit first partial fill (60 out of 100 TAO). + const firstEnvelope = { ...signed, partial_fill: firstFill }; + await context.createBlock([ + await polkadotJs.tx.limitOrders.executeOrders([firstEnvelope], false).signAsync(alice), + ]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderExecuted").length).toBe(1); + expect(filterEvents(events, "OrderSkipped").length).toBe(0); + + expect(await getOrderStatus(polkadotJs, id)).toBe("PartiallyFilled"); + const filled = await getPartiallyFilledAmount(polkadotJs, id); + expect(filled).toBe(BigInt(firstFill)); + + // Alpha stake should have increased (partial buy occurred). + const stakeAfter = await devGetAlphaStake(polkadotJs, aliceHotKey.address, alice.address, netuid); + expect(stakeAfter).toBeGreaterThan(0n); + }, + }); + + it({ + id: "T02", + title: "second partial fill completing the order sets status to Fulfilled", + test: async () => { + const orderAmount = tao(200); + const firstFill = Number(tao(120)); + const secondFill = Number(tao(80)); + + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: orderAmount, + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + relayer: [alice.address], + partialFillsEnabled: true, + }); + + const id = orderId(polkadotJs, signed.order); + + // First fill: 120 / 200. + const firstEnvelope = { ...signed, partial_fill: firstFill }; + await context.createBlock([ + await polkadotJs.tx.limitOrders.executeOrders([firstEnvelope], false).signAsync(alice), + ]); + + expect(await getOrderStatus(polkadotJs, id)).toBe("PartiallyFilled"); + expect(await getPartiallyFilledAmount(polkadotJs, id)).toBe(BigInt(firstFill)); + + // Second fill: the remaining 80 — completes the order. + // The signed VersionedOrder payload is identical; only partial_fill on the + // envelope changes, per the Rust design. + const secondEnvelope = { ...signed, partial_fill: secondFill }; + await context.createBlock([ + await polkadotJs.tx.limitOrders.executeOrders([secondEnvelope], false).signAsync(alice), + ]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderExecuted").length).toBe(1); + + expect(await getOrderStatus(polkadotJs, id)).toBe("Fulfilled"); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/limit-orders/test-execute-orders-sell-fees.ts b/ts-tests/suites/dev/subtensor/limit-orders/test-execute-orders-sell-fees.ts new file mode 100644 index 0000000000..761af62de8 --- /dev/null +++ b/ts-tests/suites/dev/subtensor/limit-orders/test-execute-orders-sell-fees.ts @@ -0,0 +1,95 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { ApiPromise } from "@polkadot/api"; +import type { KeyringPair } from "@moonwall/util"; +import { generateKeyringPair, tao } from "../../../../utils"; +import { + devForceSetBalance, + devAddStake, + devGetAlphaStake, + devAssociateHotKey, + devEnableSubtoken, + devRegisterSubnet, + devSudoSetLockReductionInterval, + devExecuteOrders, +} from "../../../../utils/dev-helpers.js"; +import { + buildSignedOrder, + FAR_FUTURE, + filterEvents, + PERBILL_ONE_PERCENT, + registerLimitOrderTypes, +} from "../../../../utils/limit-orders.js"; + +// Sell order with fee — separate file, hits pool. + +describeSuite({ + id: "DEV_SUB_LIMIT_ORDERS_FEE_SELL", + title: "execute_orders — sell order fee collection", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let aliceHotKey: KeyringPair; + let bob: KeyringPair; + let feeRecipient: KeyringPair; + let netuid: number; + + beforeAll(async () => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + aliceHotKey = generateKeyringPair(); + bob = context.keyring.bob; + feeRecipient = generateKeyringPair(); + registerLimitOrderTypes(polkadotJs); + + await devForceSetBalance(polkadotJs, context, alice.address, tao(10_000)); + await devForceSetBalance(polkadotJs, context, bob.address, tao(10_000)); + + await devSudoSetLockReductionInterval(polkadotJs, context, alice, 1); + + netuid = await devRegisterSubnet(polkadotJs, context, alice, aliceHotKey); + + // ENable subtoken + await devEnableSubtoken(polkadotJs, context, alice, netuid); + // associate hotkeys + await devAssociateHotKey(polkadotJs, context, alice, aliceHotKey.address); + + // Give Alice some alpha stake to sell + await devAddStake(polkadotJs, context, alice, aliceHotKey.address, netuid, tao(1000)); + }); + + it({ + id: "T01", + title: "fee recipient receives TAO from sell order output with 1% fee", + test: async () => { + const recipientBefore = ( + await polkadotJs.query.system.account(feeRecipient.address) + ).data.free.toBigInt(); + + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "TakeProfit", + amount: tao(100), + limitPrice: 1_000_000_000n, // always met when price >= 1 TAO/alpha (×10⁹ scale) + expiry: FAR_FUTURE, + feeRate: PERBILL_ONE_PERCENT, + feeRecipient: feeRecipient.address, + }); + + await devExecuteOrders(polkadotJs, context, alice, [signed]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderExecuted").length).toBe(1); + + const recipientAfter = ( + await polkadotJs.query.system.account(feeRecipient.address) + ).data.free.toBigInt(); + + // Fee recipient must have received something > 0 + expect(recipientAfter).toBeGreaterThan(recipientBefore); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/limit-orders/test-execute-orders-skip-conditions.ts b/ts-tests/suites/dev/subtensor/limit-orders/test-execute-orders-skip-conditions.ts new file mode 100644 index 0000000000..0be5de5200 --- /dev/null +++ b/ts-tests/suites/dev/subtensor/limit-orders/test-execute-orders-skip-conditions.ts @@ -0,0 +1,272 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { ApiPromise } from "@polkadot/api"; +import type { KeyringPair } from "@moonwall/util"; +import { tao, generateKeyringPair } from "../../../../utils"; +import { + devForceSetBalance, + devAssociateHotKey, + devEnableSubtoken, + devRegisterSubnet, + devSudoSetLockReductionInterval, +} from "../../../../utils/dev-helpers.js"; +import { + buildSignedOrder, + EXPIRED, + FAR_FUTURE, + filterEvents, + registerLimitOrderTypes, +} from "../../../../utils/limit-orders.js"; + +// Tests in this file cover skip conditions: price-not-met, expired, bad-sig, +// root-netuid, already-processed. Pool price after devEnableSubtoken is ~1 TAO/alpha, +// so LimitBuy with limitPrice=1n is always skipped and TakeProfit with limitPrice=FAR_FUTURE too. + +describeSuite({ + id: "DEV_SUB_LIMIT_ORDERS_SKIP", + title: "execute_orders — skip conditions", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let aliceHotKey: KeyringPair; + let netuid: number; + + beforeAll(async () => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + aliceHotKey = generateKeyringPair("sr25519"); + + registerLimitOrderTypes(polkadotJs); + + await devForceSetBalance(polkadotJs, context, alice.address, tao(10_000)); + await devSudoSetLockReductionInterval(polkadotJs, context, alice, 1); + + netuid = await devRegisterSubnet(polkadotJs, context, alice, aliceHotKey); + + await devEnableSubtoken(polkadotJs, context, alice, netuid); + await devAssociateHotKey(polkadotJs, context, alice, aliceHotKey.address); + }); + + it({ + id: "T01", + title: "LimitBuy skipped when limit_price below current price", + test: async () => { + // limit_price = 0: current_price (1.0 TAO/alpha) > 0 → condition never met + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(1), + limitPrice: 0n, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + await context.createBlock([ + await polkadotJs.tx.limitOrders.executeOrders([signed], false).signAsync(alice), + ]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderSkipped").length).toBe(1); + expect(filterEvents(events, "OrderExecuted").length).toBe(0); + }, + }); + + it({ + id: "T02", + title: "TakeProfit skipped when price below limit_price", + test: async () => { + // limit_price = u64::MAX — price can never reach this + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "TakeProfit", + amount: tao(1), + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + await context.createBlock([ + await polkadotJs.tx.limitOrders.executeOrders([signed], false).signAsync(alice), + ]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderSkipped").length).toBe(1); + expect(filterEvents(events, "OrderExecuted").length).toBe(0); + }, + }); + + it({ + id: "T03", + title: "expired order is skipped", + test: async () => { + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(1), + limitPrice: FAR_FUTURE, + expiry: EXPIRED, + feeRate: 0, + feeRecipient: alice.address, + }); + + await context.createBlock([ + await polkadotJs.tx.limitOrders.executeOrders([signed], false).signAsync(alice), + ]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderSkipped").length).toBe(1); + }, + }); + + it({ + id: "T04", + title: "order with invalid signature is skipped", + test: async () => { + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(1), + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + // Tamper: change the amount inside the V1 inner order after signing. + // The signature now covers different bytes — validation must reject it. + const tampered = { + ...signed, + order: { V1: { ...signed.order.V1, amount: tao(999) } }, + }; + + await context.createBlock([ + await polkadotJs.tx.limitOrders.executeOrders([tampered], false).signAsync(alice), + ]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderSkipped").length).toBe(1); + }, + }); + + it({ + id: "T05", + title: "order targeting root netuid (0) is skipped", + test: async () => { + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid: 0, + orderType: "LimitBuy", + amount: tao(1), + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + await context.createBlock([ + await polkadotJs.tx.limitOrders.executeOrders([signed], false).signAsync(alice), + ]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderSkipped").length).toBe(1); + }, + }); + + it({ + id: "T06", + title: "already-fulfilled order is skipped on second execution attempt", + test: async () => { + // Use a price condition that is always met (limitPrice = u64::MAX for buy) + // so the first call succeeds and fulfils the order. + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(1), + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + // First execution — should succeed. + await context.createBlock([ + await polkadotJs.tx.limitOrders.executeOrders([signed], false).signAsync(alice), + ]); + + // Second attempt — order already Fulfilled, must be skipped. + await context.createBlock([ + await polkadotJs.tx.limitOrders.executeOrders([signed], false).signAsync(alice), + ]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderSkipped").length).toBe(1); + expect(filterEvents(events, "OrderExecuted").length).toBe(0); + }, + }); + + it({ + id: "T07", + title: "mixed batch: valid orders execute, invalid ones are skipped", + test: async () => { + const valid = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(4), // distinct from T06 to get a different OrderId + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + const expired = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(2), + limitPrice: FAR_FUTURE, + expiry: EXPIRED, + feeRate: 0, + feeRecipient: alice.address, + }); + + const priceNotMet = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(3), + limitPrice: 0n, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + await context.createBlock([ + await polkadotJs.tx.limitOrders + .executeOrders([valid, expired, priceNotMet], false) + .signAsync(alice), + ]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderExecuted").length).toBe(1); + expect(filterEvents(events, "OrderSkipped").length).toBe(2); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/limit-orders/test-execute-orders-stop-loss.ts b/ts-tests/suites/dev/subtensor/limit-orders/test-execute-orders-stop-loss.ts new file mode 100644 index 0000000000..6f32bbb17b --- /dev/null +++ b/ts-tests/suites/dev/subtensor/limit-orders/test-execute-orders-stop-loss.ts @@ -0,0 +1,105 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { ApiPromise } from "@polkadot/api"; +import type { KeyringPair } from "@moonwall/util"; +import { tao, generateKeyringPair } from "../../../../utils"; +import { + devForceSetBalance, + devAddStake, + devGetAlphaStake, + devAssociateHotKey, + devEnableSubtoken, + devRegisterSubnet, + devSudoSetLockReductionInterval, + devExecuteOrders, +} from "../../../../utils/dev-helpers.js"; + +import { + buildSignedOrder, + FAR_FUTURE, + filterEvents, + getOrderStatus, + orderId, + registerLimitOrderTypes, +} from "../../../../utils/limit-orders.js"; + +// Separate file — StopLoss sell changes pool price. + +describeSuite({ + id: "DEV_SUB_LIMIT_ORDERS_SL", + title: "execute_orders — StopLoss execution", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let aliceHotKey: KeyringPair; + let bob: KeyringPair; + let bobHotKey: KeyringPair; + let netuid: number; + + beforeAll(async () => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + aliceHotKey = generateKeyringPair("sr25519"); + bob = context.keyring.bob; + bobHotKey = generateKeyringPair("sr25519"); + + registerLimitOrderTypes(polkadotJs); + + await devForceSetBalance(polkadotJs, context, alice.address, tao(10_000)); + await devForceSetBalance(polkadotJs, context, bob.address, tao(10_000)); + + await devSudoSetLockReductionInterval(polkadotJs, context, alice, 1); + + netuid = await devRegisterSubnet(polkadotJs, context, alice, aliceHotKey); + + // ENable subtoken + await devEnableSubtoken(polkadotJs, context, alice, netuid); + // associate hotkeys + await devAssociateHotKey(polkadotJs, context, alice, aliceHotKey.address); + await devAssociateHotKey(polkadotJs, context, bob, bobHotKey.address); + + // Give Alice some alpha stake to sell + await devAddStake(polkadotJs, context, alice, aliceHotKey.address, netuid, tao(1000)); + }); + + it({ + id: "T01", + title: "StopLoss executes when price <= limit_price", + test: async () => { + const stakeBefore = await devGetAlphaStake(polkadotJs, aliceHotKey.address, alice.address, netuid); + const taoBalanceBefore = (await polkadotJs.query.system.account(alice.address)).data.free.toBigInt(); + + // limit_price = 100_000_000_000 (100.0 TAO/alpha in ×10⁹ scale) — safely above the + // actual pool price on the freshly registered dynamic subnet after devAddStake(tao(1000)). + // max_slippage is unset (None) so the effective AMM floor is 0; the limit_price here + // only controls the StopLoss trigger condition, not the swap execution price. + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "StopLoss", + amount: tao(100), + limitPrice: 100_000_000_000n, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + await devExecuteOrders(polkadotJs, context, alice, [signed]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderExecuted").length).toBe(1); + expect(filterEvents(events, "OrderSkipped").length).toBe(0); + + const id = orderId(polkadotJs, signed.order); + expect(await getOrderStatus(polkadotJs, id)).toBe("Fulfilled"); + + const stakeAfter = await devGetAlphaStake(polkadotJs, aliceHotKey.address, alice.address, netuid); + expect(stakeAfter).toBeLessThan(stakeBefore); + + const taoBalanceAfter = (await polkadotJs.query.system.account(alice.address)).data.free.toBigInt(); + expect(taoBalanceAfter).toBeGreaterThan(taoBalanceBefore); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/limit-orders/test-execute-orders-take-profit.ts b/ts-tests/suites/dev/subtensor/limit-orders/test-execute-orders-take-profit.ts new file mode 100644 index 0000000000..338bc075eb --- /dev/null +++ b/ts-tests/suites/dev/subtensor/limit-orders/test-execute-orders-take-profit.ts @@ -0,0 +1,104 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { ApiPromise } from "@polkadot/api"; +import type { KeyringPair } from "@moonwall/util"; +import { tao, generateKeyringPair } from "../../../../utils"; +import { + devForceSetBalance, + devAddStake, + devGetAlphaStake, + devAssociateHotKey, + devEnableSubtoken, + devRegisterSubnet, + devSudoSetLockReductionInterval, + devExecuteOrders, +} from "../../../../utils/dev-helpers.js"; +import { + buildSignedOrder, + FAR_FUTURE, + filterEvents, + getOrderStatus, + orderId, + registerLimitOrderTypes, +} from "../../../../utils/limit-orders.js"; + +// Separate file because a TakeProfit sell changes pool price. + +describeSuite({ + id: "DEV_SUB_LIMIT_ORDERS_TP", + title: "execute_orders — TakeProfit execution", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let aliceHotKey: KeyringPair; + let bob: KeyringPair; + let bobHotKey: KeyringPair; + let netuid: number; + + beforeAll(async () => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + aliceHotKey = generateKeyringPair("sr25519"); + bob = context.keyring.bob; + bobHotKey = generateKeyringPair("sr25519"); + + registerLimitOrderTypes(polkadotJs); + + await devForceSetBalance(polkadotJs, context, alice.address, tao(10_000)); + await devForceSetBalance(polkadotJs, context, bob.address, tao(10_000)); + + await devSudoSetLockReductionInterval(polkadotJs, context, alice, 1); + + netuid = await devRegisterSubnet(polkadotJs, context, alice, aliceHotKey); + + // ENable subtoken + await devEnableSubtoken(polkadotJs, context, alice, netuid); + // associate hotkeys + await devAssociateHotKey(polkadotJs, context, alice, aliceHotKey.address); + await devAssociateHotKey(polkadotJs, context, bob, bobHotKey.address); + + // Give Alice some alpha stake to sell + await devAddStake(polkadotJs, context, alice, aliceHotKey.address, netuid, tao(1000)); + }); + + it({ + id: "T01", + title: "TakeProfit executes when price >= limit_price", + test: async () => { + const stakeBefore = await devGetAlphaStake(polkadotJs, aliceHotKey.address, alice.address, netuid); + const taoBalanceBefore = (await polkadotJs.query.system.account(alice.address)).data.free.toBigInt(); + + // limit_price = 1_000_000_000 (1.0 TAO/alpha in ×10⁹ scale) — current price after + // devAddStake(tao(1000)) is above 1.0 TAO/alpha, so this condition is always met + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "TakeProfit", + amount: tao(100), + limitPrice: 1_000_000_000n, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + await devExecuteOrders(polkadotJs, context, alice, [signed]); + + const events = await polkadotJs.query.system.events(); + expect(filterEvents(events, "OrderExecuted").length).toBe(1); + expect(filterEvents(events, "OrderSkipped").length).toBe(0); + + const id = orderId(polkadotJs, signed.order); + expect(await getOrderStatus(polkadotJs, id)).toBe("Fulfilled"); + + // Alpha stake should have decreased + const stakeAfter = await devGetAlphaStake(polkadotJs, aliceHotKey.address, alice.address, netuid); + expect(stakeAfter).toBeLessThan(stakeBefore); + + // TAO balance should have increased + const taoBalanceAfter = (await polkadotJs.query.system.account(alice.address)).data.free.toBigInt(); + expect(taoBalanceAfter).toBeGreaterThan(taoBalanceBefore); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/limit-orders/test-mevshield-execute-orders.ts b/ts-tests/suites/dev/subtensor/limit-orders/test-mevshield-execute-orders.ts new file mode 100644 index 0000000000..daa06882f5 --- /dev/null +++ b/ts-tests/suites/dev/subtensor/limit-orders/test-mevshield-execute-orders.ts @@ -0,0 +1,189 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { ApiPromise } from "@polkadot/api"; +import type { KeyringPair } from "@moonwall/util"; +import { tao, generateKeyringPair } from "../../../../utils"; +import { + devForceSetBalance, + devGetAlphaStake, + devAssociateHotKey, + devEnableSubtoken, + devRegisterSubnet, + devSudoSetLockReductionInterval, +} from "../../../../utils/dev-helpers.js"; +import { + buildSignedOrder, + FAR_FUTURE, + fetchChainId, + getOrderStatus, + orderId, + registerLimitOrderTypes, +} from "../../../../utils/limit-orders.js"; +import { encryptTransaction } from "../../../../utils/shield_helpers.js"; +import { u8aToHex } from "@polkadot/util"; + +describeSuite({ + id: "DEV_SUB_LIMIT_ORDERS_MEVSHIELD", + title: "execute_orders via MEVShield submit_encrypted", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let aliceHotKey: KeyringPair; + let netuid: number; + let chainId: bigint; + + beforeAll(async () => { + polkadotJs = context.polkadotJs(); + + alice = context.keyring.alice; + aliceHotKey = generateKeyringPair("sr25519"); + + registerLimitOrderTypes(polkadotJs); + chainId = await fetchChainId(polkadotJs); + + // Create 3+ blocks so PendingKey is populated (needs 2 blocks for the + // AuthorKeys → NextKey → PendingKey pipeline to fill). The subsequent setup + // transactions each create additional blocks, so 2 here is sufficient. + await context.createBlock([]); + await context.createBlock([]); + + await devForceSetBalance(polkadotJs, context, alice.address, tao(10_000)); + await devSudoSetLockReductionInterval(polkadotJs, context, alice, 1); + + netuid = await devRegisterSubnet(polkadotJs, context, alice, aliceHotKey); + + await devEnableSubtoken(polkadotJs, context, alice, netuid); + await devAssociateHotKey(polkadotJs, context, alice, aliceHotKey.address); + }); + + it({ + id: "T01", + title: "LimitBuy submitted via MEVShield submit_encrypted is decrypted and executed in the same block", + test: async () => { + // Use PendingKey — this is the key the current block's proposer checks against. + // NextKey is one rotation ahead; encrypting with it would require waiting an extra + // block for it to advance to PendingKey, which doesn't happen automatically in + // manual-seal mode. + const pendingKeyRaw = await polkadotJs.query.mevShield.pendingKey(); + if ((pendingKeyRaw as any).isNone) + throw new Error("MEVShield PendingKey not available — create more blocks first"); + const nextKeyBytes = (pendingKeyRaw as any).unwrap().toU8a(true); + + const signedOrder = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(100), + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + relayer: null, + chainId, + }); + + // Get alice's current nonce so we can pre-sign the inner tx at nonce+1 + const aliceNonce = ( + (await polkadotJs.query.system.account(alice.address)) as any + ).nonce.toNumber() as number; + + // Sign the inner execute_orders tx at nonce+1, then get its raw bytes + const innerTx = await polkadotJs.tx.limitOrders + .executeOrders([signedOrder], false) + .signAsync(alice, { nonce: aliceNonce + 1 }); + const innerTxBytes = innerTx.toU8a(); + + // Encrypt the inner tx with the MEVShield NextKey + const ciphertext = await encryptTransaction(innerTxBytes, nextKeyBytes); + + // submit_encrypted requires a mortal era — immortal is rejected by CheckMortality. + // Anchor to the PARENT block, not the current best block. + // + // try_decode_shielded_tx is a runtime API call executed at parent_hash (block B's + // state). CheckMortality::implicit() looks up BlockHash[birth]. In block B's state, + // only blocks 0..B-1 are stored — BlockHash[B] is populated when block B+1 + // initializes. If we sign with { current: B }, birth = B and the lookup fails + // (AncientBirthBlock), check() returns Err, and try_decode_shielded_tx returns None, + // so the outer tx is included as a plain tx with no inner tx extracted. + // Anchoring to B-1 (the parent) means birth = B-1, which IS in BlockHash at block + // B's state, so implicit() succeeds and the signature verifies correctly. + const header = await polkadotJs.rpc.chain.getHeader(); + const blockNumber = header.number.toNumber() - 1; + const blockHash = header.parentHash; + const era = polkadotJs.createType("ExtrinsicEra", { current: blockNumber, period: 8 }); + + // Submit the wrapper directly to the pool (not via createBlock) so the proposer + // scans the pool naturally and runs shielded-tx detection. + const signedWrapper = await polkadotJs.tx.mevShield + .submitEncrypted(u8aToHex(ciphertext)) + .signAsync(alice, { nonce: aliceNonce, era, blockHash }); + await polkadotJs.rpc.author.submitExtrinsic(signedWrapper.toHex()); + + // Seal a block — the proposer detects the shielded tx in the pool, decrypts the + // inner execute_orders, and includes both in the same block. + await context.createBlock([]); + + // Assert the order is Fulfilled + const id = orderId(polkadotJs, signedOrder.order); + expect(await getOrderStatus(polkadotJs, id)).toBe("Fulfilled"); + }, + }); + + it({ + id: "T02", + title: "LimitBuy with a designated relayer is executed when the relayer submits via MEVShield", + test: async () => { + const relayer = generateKeyringPair("sr25519"); + await devForceSetBalance(polkadotJs, context, relayer.address, tao(100)); + + const pendingKeyRaw = await polkadotJs.query.mevShield.pendingKey(); + if ((pendingKeyRaw as any).isNone) + throw new Error("MEVShield PendingKey not available — create more blocks first"); + const pendingKeyBytes = (pendingKeyRaw as any).unwrap().toU8a(true); + + const signedOrder = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: aliceHotKey.address, + netuid, + orderType: "LimitBuy", + amount: tao(100), + limitPrice: FAR_FUTURE, + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + relayer: [relayer.address], + chainId, + }); + + // The relayer submits the encrypted execute_orders tx on Alice's behalf. + // relayerNonce+0 = outer submit_encrypted, relayerNonce+1 = inner execute_orders. + const relayerNonce = ( + (await polkadotJs.query.system.account(relayer.address)) as any + ).nonce.toNumber() as number; + + const innerTx = await polkadotJs.tx.limitOrders + .executeOrders([signedOrder], false) + .signAsync(relayer, { nonce: relayerNonce + 1 }); + const innerTxBytes = innerTx.toU8a(); + + const ciphertext = await encryptTransaction(innerTxBytes, pendingKeyBytes); + + const header = await polkadotJs.rpc.chain.getHeader(); + const blockNumber = header.number.toNumber() - 1; + const blockHash = header.parentHash; + const era = polkadotJs.createType("ExtrinsicEra", { current: blockNumber, period: 8 }); + + const signedWrapper = await polkadotJs.tx.mevShield + .submitEncrypted(u8aToHex(ciphertext)) + .signAsync(relayer, { nonce: relayerNonce, era, blockHash }); + await polkadotJs.rpc.author.submitExtrinsic(signedWrapper.toHex()); + + await context.createBlock([]); + + const id = orderId(polkadotJs, signedOrder.order); + expect(await getOrderStatus(polkadotJs, id)).toBe("Fulfilled"); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/limit-orders/test-pallet-status.ts b/ts-tests/suites/dev/subtensor/limit-orders/test-pallet-status.ts new file mode 100644 index 0000000000..f61e6d823a --- /dev/null +++ b/ts-tests/suites/dev/subtensor/limit-orders/test-pallet-status.ts @@ -0,0 +1,107 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { ApiPromise } from "@polkadot/api"; +import type { KeyringPair } from "@moonwall/util"; +import { tao } from "../../../../utils"; +import { devForceSetBalance } from "../../../../utils/dev-helpers.js"; +import { buildSignedOrder, FAR_FUTURE, filterEvents, registerLimitOrderTypes } from "../../../../utils/limit-orders.js"; + +describeSuite({ + id: "DEV_SUB_LIMIT_ORDERS_STATUS", + title: "set_pallet_status", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + + beforeAll(async () => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + registerLimitOrderTypes(polkadotJs); + await devForceSetBalance(polkadotJs, context, alice.address, tao(1_000)); + }); + + it({ + id: "T01", + title: "root can disable the pallet", + test: async () => { + await context.createBlock([ + await polkadotJs.tx.sudo.sudo(polkadotJs.tx.limitOrders.setPalletStatus(false)).signAsync(alice), + ]); + + const events = await polkadotJs.query.system.events(); + const statusEvent = filterEvents(events, "LimitOrdersPalletStatusChanged"); + expect(statusEvent.length).toBe(1); + expect(statusEvent[0].event.data[0].isTrue).toBe(false); + }, + }); + + it({ + id: "T02", + title: "execute_orders is blocked when pallet is disabled", + test: async () => { + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: alice.address, + netuid: 1, + orderType: "LimitBuy", + amount: tao(1), + limitPrice: BigInt(2_000_000_000), + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + const { + result: [attempt], + } = await context.createBlock([ + await polkadotJs.tx.limitOrders.executeOrders([signed], false).signAsync(alice), + ]); + + expect(attempt.successful).toEqual(false); + expect(attempt.error.name).toEqual("LimitOrdersDisabled"); + }, + }); + + it({ + id: "T03", + title: "execute_batched_orders is blocked when pallet is disabled", + test: async () => { + const signed = buildSignedOrder(polkadotJs, { + signer: alice, + hotkey: alice.address, + netuid: 1, + orderType: "LimitBuy", + amount: tao(1), + limitPrice: BigInt(2_000_000_000), + expiry: FAR_FUTURE, + feeRate: 0, + feeRecipient: alice.address, + }); + + const { + result: [attempt], + } = await context.createBlock([ + await polkadotJs.tx.limitOrders.executeBatchedOrders(1, [signed]).signAsync(alice), + ]); + + expect(attempt.successful).toEqual(false); + expect(attempt.error.name).toEqual("LimitOrdersDisabled"); + }, + }); + + it({ + id: "T04", + title: "root can re-enable the pallet", + test: async () => { + await context.createBlock([ + await polkadotJs.tx.sudo.sudo(polkadotJs.tx.limitOrders.setPalletStatus(true)).signAsync(alice), + ]); + + const events = await polkadotJs.query.system.events(); + const statusEvent = filterEvents(events, "LimitOrdersPalletStatusChanged"); + expect(statusEvent.length).toBe(1); + expect(statusEvent[0].event.data[0].isTrue).toBe(true); + }, + }); + }, +}); diff --git a/ts-tests/suites/dev/subtensor/staking/test-transfer-stake-rate-limit.ts b/ts-tests/suites/dev/subtensor/staking/test-transfer-stake-rate-limit.ts new file mode 100644 index 0000000000..c8bc53419e --- /dev/null +++ b/ts-tests/suites/dev/subtensor/staking/test-transfer-stake-rate-limit.ts @@ -0,0 +1,253 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; +import type { ApiPromise } from "@polkadot/api"; +import type { KeyringPair } from "@moonwall/util"; +import { generateKeyringPair } from "../../../../utils/account"; + +const TAO = 1_000_000_000n; // 10^9 RAO per TAO +const tao = (value: number): bigint => TAO * BigInt(value); + +async function devForceSetBalance( + polkadotJs: ApiPromise, + context: any, + address: string, + amount: bigint +): Promise { + await context.createBlock([ + await polkadotJs.tx.sudo + .sudo(polkadotJs.tx.balances.forceSetBalance(address, amount)) + .signAsync(context.keyring.alice), + ]); +} + +async function devSudoSetLockReductionInterval( + polkadotJs: ApiPromise, + context: any, + alice: KeyringPair, + interval: number +): Promise { + await context.createBlock([await polkadotJs.tx.adminUtils.sudoSetLockReductionInterval(interval).signAsync(alice)]); +} + +async function devRegisterSubnet( + polkadotJs: ApiPromise, + context: any, + alice: KeyringPair, + hotkey: KeyringPair +): Promise { + await context.createBlock([await polkadotJs.tx.subtensorModule.registerNetwork(hotkey.address).signAsync(alice)]); + const events = (await polkadotJs.query.system.events()) as any; + const netuid = (events as any[]).filter((e: any) => e.event.method === "NetworkAdded")[0].event.data[0].toNumber(); + return netuid; +} + +async function devEnableSubtoken( + polkadotJs: ApiPromise, + context: any, + alice: KeyringPair, + netuid: number +): Promise { + await context.createBlock([ + await polkadotJs.tx.sudo.sudo(polkadotJs.tx.adminUtils.sudoSetSubtokenEnabled(netuid, true)).signAsync(alice), + ]); +} + +async function devAssociateHotKey( + polkadotJs: ApiPromise, + context: any, + coldkey: KeyringPair, + hotkey: string +): Promise { + await context.createBlock([await polkadotJs.tx.subtensorModule.tryAssociateHotkey(hotkey).signAsync(coldkey)]); +} + +async function devGetAlphaStake( + polkadotJs: ApiPromise, + hotkey: string, + coldkey: string, + netuid: number +): Promise { + const value = (await polkadotJs.query.subtensorModule.alphaV2(hotkey, coldkey, netuid)) as any; + const mantissa = value.mantissa; + const exponent = value.exponent; + if (exponent >= 0n) { + return BigInt(mantissa) * BigInt(10) ** BigInt(exponent); + } + return BigInt(mantissa) / BigInt(10) ** BigInt(-exponent); +} + +describeSuite({ + id: "DEV_SUB_STAKING_TRANSFER_RATE_LIMIT", + title: "staking — same-block add_stake / transfer_stake (no per-block rate limiter)", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let aliceHotKey: KeyringPair; + let destinationColdkey: KeyringPair; + let netuid: number; + + beforeAll(async () => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + aliceHotKey = generateKeyringPair("sr25519"); + destinationColdkey = generateKeyringPair("sr25519"); + + await devForceSetBalance(polkadotJs, context, alice.address, tao(10_000)); + // ensure destination coldkey can receive transferred stake + await devForceSetBalance(polkadotJs, context, destinationColdkey.address, tao(10_000)); + await devSudoSetLockReductionInterval(polkadotJs, context, alice, 1); + + await context.createBlock([ + await polkadotJs.tx.sudo.sudo(polkadotJs.tx.adminUtils.sudoSetNetworkRateLimit(0)).signAsync(alice), + ]); + + netuid = await devRegisterSubnet(polkadotJs, context, alice, aliceHotKey); + + await devEnableSubtoken(polkadotJs, context, alice, netuid); + await devAssociateHotKey(polkadotJs, context, alice, aliceHotKey.address); + }); + + it({ + id: "T01", + title: "add_stake + same-subnet transfer_stake in one block both succeed", + test: async () => { + // Both extrinsics are signed by alice, so use explicit incrementing + // nonces to land them in the same block in submission order. + const aliceNonce = ((await polkadotJs.query.system.account(alice.address)) as any).nonce.toNumber(); + + // Stake a large amount so the same-block transfer has plenty of alpha + // to move and clears the DefaultMinStake floor. + const addTx = await polkadotJs.tx.subtensorModule + .addStake(aliceHotKey.address, netuid, tao(100)) + .signAsync(alice, { nonce: aliceNonce }); + + const transferAmount = 1_000_000_000n; + const transferTx = await polkadotJs.tx.subtensorModule + .transferStake(destinationColdkey.address, aliceHotKey.address, netuid, netuid, transferAmount) + .signAsync(alice, { nonce: aliceNonce + 1 }); + + const { result } = await context.createBlock([addTx, transferTx]); + const [addAttempt, transferAttempt] = result; + + expect(addAttempt.successful).toEqual(true); + expect(transferAttempt.successful).toEqual(true); + }, + }); + + it({ + id: "T02", + title: "add_stake then transfer_stake across SEPARATE blocks both succeed", + test: async () => { + // add in its own block + const { + result: [addAttempt2], + } = await context.createBlock([ + await polkadotJs.tx.subtensorModule + .addStake(aliceHotKey.address, netuid, tao(100)) + .signAsync(alice), + ]); + expect(addAttempt2.successful).toEqual(true); + + const alphaStaked = await devGetAlphaStake(polkadotJs, aliceHotKey.address, alice.address, netuid); + const transferAmount = alphaStaked / 2n; + expect(transferAmount > 0n).toEqual(true); + + // transfer in the NEXT block — same triple, succeeds + const { + result: [transferAttempt2], + } = await context.createBlock([ + await polkadotJs.tx.subtensorModule + .transferStake(destinationColdkey.address, aliceHotKey.address, netuid, netuid, transferAmount) + .signAsync(alice), + ]); + expect(transferAttempt2.successful).toEqual(true); + }, + }); + + it({ + id: "T03", + title: "two add_stake on the IDENTICAL (coldkey, hotkey, netuid) in the SAME block both succeed", + test: async () => { + const aliceNonce = ((await polkadotJs.query.system.account(alice.address)) as any).nonce.toNumber(); + + const addTx1 = await polkadotJs.tx.subtensorModule + .addStake(aliceHotKey.address, netuid, tao(10)) + .signAsync(alice, { nonce: aliceNonce }); + + const addTx2 = await polkadotJs.tx.subtensorModule + .addStake(aliceHotKey.address, netuid, tao(10)) + .signAsync(alice, { nonce: aliceNonce + 1 }); + + const { result } = await context.createBlock([addTx1, addTx2]); + const [addAttempt1, addAttempt2] = result; + + expect(addAttempt1.successful).toEqual(true); + expect(addAttempt2.successful).toEqual(true); + }, + }); + + it({ + id: "T04", + title: "remove_stake then transfer_stake on the IDENTICAL (coldkey, hotkey, netuid) in the SAME block both succeed", + test: async () => { + const { + result: [seedAdd], + } = await context.createBlock([ + await polkadotJs.tx.subtensorModule + .addStake(aliceHotKey.address, netuid, tao(100)) + .signAsync(alice), + ]); + expect(seedAdd.successful).toEqual(true); + + // Size both legs as a real fraction of available alpha so neither trips the + // DefaultMinStake floor, and their sum stays below the available balance. + const alphaStaked = await devGetAlphaStake(polkadotJs, aliceHotKey.address, alice.address, netuid); + const legAmount = alphaStaked / 4n; + expect(legAmount > 0n).toEqual(true); + + const aliceNonce = ((await polkadotJs.query.system.account(alice.address)) as any).nonce.toNumber(); + + const removeTx = await polkadotJs.tx.subtensorModule + .removeStake(aliceHotKey.address, netuid, legAmount) + .signAsync(alice, { nonce: aliceNonce }); + + const transferTx = await polkadotJs.tx.subtensorModule + .transferStake(destinationColdkey.address, aliceHotKey.address, netuid, netuid, legAmount) + .signAsync(alice, { nonce: aliceNonce + 1 }); + + const { result } = await context.createBlock([removeTx, transferTx]); + const [removeAttempt, transferAttempt] = result; + + expect(removeAttempt.successful).toEqual(true); + expect(transferAttempt.successful).toEqual(true); + }, + }); + + it({ + id: "T05", + title: "add_stake + CROSS-subnet transfer_stake in one block is no longer rate-limited (limiter removed) — it now falls through to the normal amount check", + test: async () => { + const netuid2 = await devRegisterSubnet(polkadotJs, context, alice, aliceHotKey); + await devEnableSubtoken(polkadotJs, context, alice, netuid2); + + const aliceNonce = ((await polkadotJs.query.system.account(alice.address)) as any).nonce.toNumber(); + + const addTx = await polkadotJs.tx.subtensorModule + .addStake(aliceHotKey.address, netuid, tao(100)) + .signAsync(alice, { nonce: aliceNonce }); + + const transferTx = await polkadotJs.tx.subtensorModule + .transferStake(destinationColdkey.address, aliceHotKey.address, netuid, netuid2, 1000n) + .signAsync(alice, { nonce: aliceNonce + 1 }); + + const { result } = await context.createBlock([addTx, transferTx]); + const [addAttempt, transferAttempt] = result; + + expect(addAttempt.successful).toEqual(true); + expect(transferAttempt.successful).toEqual(false); + expect(transferAttempt.error.name).not.toEqual("StakingOperationRateLimitExceeded"); + expect(transferAttempt.error.name).toEqual("AmountTooLow"); + }, + }); + }, +}); diff --git a/ts-tests/suites/zombienet_staking/02.04-claim-root-hotkey-swap.test.ts b/ts-tests/suites/zombienet_staking/02.04-claim-root-hotkey-swap.test.ts index 0124bae671..3578061b6f 100644 --- a/ts-tests/suites/zombienet_staking/02.04-claim-root-hotkey-swap.test.ts +++ b/ts-tests/suites/zombienet_staking/02.04-claim-root-hotkey-swap.test.ts @@ -68,7 +68,7 @@ async function setupTwoSubnetsWithClaimable( log(`Created netuid2: ${netuid2}`); for (const netuid of [netuid1, netuid2]) { - await sudoSetTempo(api, netuid, 1); + await sudoSetTempo(api, netuid, 5); await sudoSetEmaPriceHalvingPeriod(api, netuid, 1); await sudoSetRootClaimThreshold(api, netuid, 0n); } @@ -91,14 +91,15 @@ async function setupTwoSubnetsWithClaimable( await addStake(api, owner1Coldkey, owner1Hotkey.address, netuid1, tao(50)); await addStake(api, owner2Coldkey, owner2Hotkey.address, netuid2, tao(50)); - log("Waiting 30 blocks for RootClaimable to accumulate on both subnets..."); - await waitForBlocks(api, 30); + const waitBlocks = 90; + log(`Waiting ${waitBlocks} blocks for RootClaimable to accumulate on both subnets...`); + await waitForBlocks(api, waitBlocks); return { oldHotkey, oldHotkeyColdkey, newHotkey, netuid1, netuid2 }; } describeSuite({ - id: "0203_swap_hotkey_root_claimable", + id: "0204_claim-root_hotkey_swap", title: "▶ swap_hotkey RootClaimable per-subnet transfer", foundationMethods: "zombie", testCases: ({ it, context, log }) => { diff --git a/ts-tests/utils/balance.ts b/ts-tests/utils/balance.ts index f6fe83d3b0..b172bf1546 100644 --- a/ts-tests/utils/balance.ts +++ b/ts-tests/utils/balance.ts @@ -1,6 +1,6 @@ import { waitForTransactionWithRetry } from "./transactions.js"; import type { TypedApi } from "polkadot-api"; -import { type subtensor, MultiAddress } from "@polkadot-api/descriptors"; +import type { subtensor } from "@polkadot-api/descriptors"; import { Keyring } from "@polkadot/keyring"; export const TAO = BigInt(1000000000); // 10^9 RAO per TAO @@ -19,6 +19,7 @@ export async function forceSetBalance( ss58Address: string, amount: bigint = tao(1e10) ): Promise { + const { MultiAddress } = await import("@polkadot-api/descriptors"); const keyring = new Keyring({ type: "sr25519" }); const alice = keyring.addFromUri("//Alice"); const internalCall = api.tx.Balances.force_set_balance({ diff --git a/ts-tests/utils/dev-helpers.ts b/ts-tests/utils/dev-helpers.ts new file mode 100644 index 0000000000..bc7b07659a --- /dev/null +++ b/ts-tests/utils/dev-helpers.ts @@ -0,0 +1,105 @@ +/** + * Polkadot.js (ApiPromise) compatible helpers for dev tests. + * Uses ApiPromise, not PAPI TypedApi — keep them separate. + */ +import type { ApiPromise } from "@polkadot/api"; +import type { KeyringPair } from "@moonwall/util"; +import { SignedOrder } from "./index.js"; + +export async function devForceSetBalance( + polkadotJs: ApiPromise, + context: any, + address: string, + amount: bigint +): Promise { + await context.createBlock([ + await polkadotJs.tx.sudo + .sudo(polkadotJs.tx.balances.forceSetBalance(address, amount)) + .signAsync(context.keyring.alice), + ]); +} + +export async function devAddStake( + polkadotJs: ApiPromise, + context: any, + coldkey: KeyringPair, + hotkey: string, + netuid: number, + amount: bigint +): Promise { + await context.createBlock([ + await polkadotJs.tx.subtensorModule.addStake(hotkey, netuid, amount).signAsync(coldkey), + ]); +} + +export async function devAssociateHotKey( + polkadotJs: ApiPromise, + context: any, + coldkey: KeyringPair, + hotkey: string +): Promise { + await context.createBlock([await polkadotJs.tx.subtensorModule.tryAssociateHotkey(hotkey).signAsync(coldkey)]); +} + +export async function devGetAlphaStake( + polkadotJs: ApiPromise, + hotkey: string, + coldkey: string, + netuid: number +): Promise { + const value = await polkadotJs.query.subtensorModule.alphaV2(hotkey, coldkey, netuid); + + const mantissa = value.mantissa; + const exponent = value.exponent; + + let result: bigint; + + if (exponent >= 0n) { + result = BigInt(mantissa) * BigInt(10) ** BigInt(exponent); + } else { + result = BigInt(mantissa) / BigInt(10) ** BigInt(-exponent); + } + + return result; +} + +export async function devSudoSetLockReductionInterval( + polkadotJs: ApiPromise, + context: any, + alice: KeyringPair, + interval: number +): Promise { + await context.createBlock([await polkadotJs.tx.adminUtils.sudoSetLockReductionInterval(interval).signAsync(alice)]); +} + +export async function devRegisterSubnet( + polkadotJs: ApiPromise, + context: any, + alice: KeyringPair, + hotkey: KeyringPair +): Promise { + await context.createBlock([await polkadotJs.tx.subtensorModule.registerNetwork(hotkey.address).signAsync(alice)]); + const events = (await polkadotJs.query.system.events()) as any; + const netuid = (events as any[]).filter((e: any) => e.event.method === "NetworkAdded")[0].event.data[0].toNumber(); + return netuid; +} + +export async function devEnableSubtoken( + polkadotJs: ApiPromise, + context: any, + alice: KeyringPair, + netuid: number +): Promise { + await context.createBlock([ + await polkadotJs.tx.sudo.sudo(polkadotJs.tx.adminUtils.sudoSetSubtokenEnabled(netuid, true)).signAsync(alice), + ]); +} +export async function devExecuteOrders( + polkadotJs: ApiPromise, + context: any, + alice: KeyringPair, + orders: SignedOrder[], + shouldFail = false +): Promise { + await context.createBlock([await polkadotJs.tx.limitOrders.executeOrders(orders, shouldFail).signAsync(alice)]); +} diff --git a/ts-tests/utils/limit-orders.ts b/ts-tests/utils/limit-orders.ts new file mode 100644 index 0000000000..0ffbe177e0 --- /dev/null +++ b/ts-tests/utils/limit-orders.ts @@ -0,0 +1,267 @@ +import type { KeyringPair } from "@moonwall/util"; +import type { TypedApi } from "polkadot-api"; +import type { subtensor } from "@polkadot-api/descriptors"; +import { Keyring } from "@polkadot/keyring"; +import { u8aToHex } from "@polkadot/util"; +import { blake2AsHex } from "@polkadot/util-crypto"; +import { waitForTransactionWithRetry } from "./transactions.js"; +import { MultiAddress } from "@polkadot-api/descriptors"; + +// ── Types ───────────────────────────────────────────────────────────────────── + +export type OrderType = "LimitBuy" | "TakeProfit" | "StopLoss"; + +export interface OrderParams { + signer: KeyringPair; + hotkey: string; + netuid: number; + orderType: OrderType; + amount: bigint; + limitPrice: bigint; + expiry: bigint; + feeRate: number; // Perbill (parts per billion), e.g. 10_000_000 = 1% + feeRecipient: string; + chainId?: bigint; // defaults to 42n (the dev node's EVM chain ID) + relayer?: string[] | null; // Optional: if set, only these accounts may relay the order + maxSlippage?: number | null; // Optional: Perbill (ppb). When set, effective swap limit = limit_price ± limit_price * maxSlippage / 1e9 + partialFillsEnabled?: boolean; // Optional: if true, order can be partially filled (requires relayer) +} + +export interface Order { + signer: string; + hotkey: string; + netuid: number; + order_type: OrderType; + amount: bigint; + limit_price: bigint; + expiry: bigint; + fee_rate: number; + fee_recipient: string; + relayer: string[] | null; + max_slippage: number | null; + chain_id: bigint; + partial_fills_enabled: boolean; +} + +export interface VersionedOrder { + V1: Order; +} + +export interface SignedOrder { + order: VersionedOrder; + signature: { Sr25519: `0x${string}` } | { Ed25519: `0x${string}` } | { Ecdsa: `0x${string}` }; + partial_fill: number | null; +} + +// ── Constants ───────────────────────────────────────────────────────────────── + +export const PERBILL_ONE_PERCENT = 10_000_000; +export const FAR_FUTURE = BigInt("18446744073709551615"); // u64::MAX +export const EXPIRED = BigInt(1); // 1ms — always in the past + +// ── Order building & signing ────────────────────────────────────────────────── + +/** + * Build a SignedOrder ready for submission to execute_orders / + * execute_batched_orders. The Order struct is SCALE-encoded via the + * polkadot.js registry and then signed with the signer's sr25519 key. + */ +export function buildSignedOrder(api: any, params: OrderParams): SignedOrder { + const inner: Order = { + signer: params.signer.address, + hotkey: params.hotkey, + netuid: params.netuid, + order_type: params.orderType, + amount: params.amount, + limit_price: params.limitPrice, + expiry: params.expiry, + fee_rate: params.feeRate, + fee_recipient: params.feeRecipient, + relayer: params.relayer ?? null, + max_slippage: params.maxSlippage ?? null, + chain_id: params.chainId ?? 42n, + partial_fills_enabled: params.partialFillsEnabled ?? false, + }; + + const versionedOrder: VersionedOrder = { V1: inner }; + + // SCALE-encode the VersionedOrder so the signature covers the version tag. + const encoded = api.registry.createType("LimitVersionedOrder", versionedOrder); + const sig = params.signer.sign(encoded.toU8a()); + + return { + order: versionedOrder, + signature: { Sr25519: u8aToHex(sig) as `0x${string}` }, + partial_fill: null, + }; +} + +/** + * Compute the on-chain OrderId (blake2_256 of SCALE-encoded VersionedOrder). + * Mirrors `Pallet::derive_order_id` in Rust. + */ +export function orderId(api: any, order: VersionedOrder): `0x${string}` { + const encoded = api.registry.createType("LimitVersionedOrder", order); + return blake2AsHex(encoded.toU8a(), 256) as `0x${string}`; +} + +// ── Registry ────────────────────────────────────────────────────────────────── + +/** + * Register the custom SCALE types used by pallet-limit-orders with the + * polkadot.js ApiPromise registry. Call this once after obtaining the api. + */ +export function registerLimitOrderTypes(api: any): void { + api.registry.register({ + LimitOrderType: { + _enum: ["LimitBuy", "TakeProfit", "StopLoss"], + }, + LimitOrder: { + signer: "AccountId", + hotkey: "AccountId", + netuid: "u16", + order_type: "LimitOrderType", + amount: "u64", + limit_price: "u64", + expiry: "u64", + fee_rate: "u32", // Perbill + fee_recipient: "AccountId", + relayer: "Option>", + max_slippage: "Option", + chain_id: "u64", + partial_fills_enabled: "bool", + }, + LimitVersionedOrder: { + _enum: { + V1: "LimitOrder", + }, + }, + LimitSignedOrder: { + order: "LimitVersionedOrder", + signature: "MultiSignature", + partial_fill: "Option", + }, + LimitOrderStatus: { + _enum: { + Fulfilled: null, + PartiallyFilled: "u64", + Cancelled: null, + }, + }, + }); +} + +// ── Chain helpers ───────────────────────────────────────────────────────────── + +/** Read current SubnetTAO and SubnetAlphaIn to derive spot price (TAO per alpha). */ +export async function getAlphaPrice(api: TypedApi, netuid: number): Promise { + const taoReserve = await api.query.SubtensorModule.SubnetTAO.getValue(netuid); + const alphaIn = await api.query.SubtensorModule.SubnetAlphaIn.getValue(netuid); + if (alphaIn === 0n) return 0n; + return taoReserve / alphaIn; // integer approximation +} + +/** Enable the subtoken for a subnet (required for swaps to work). */ +export async function enableSubtoken(api: TypedApi, netuid: number): Promise { + const keyring = new Keyring({ type: "sr25519" }); + const alice = keyring.addFromUri("//Alice"); + const internalCall = api.tx.AdminUtils.sudo_set_subtoken_enabled({ + netuid, + subtoken_enabled: true, + }); + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }); + await waitForTransactionWithRetry(api, tx, alice, "sudo_set_subtoken_enabled"); +} + +/** Sudo-enable or disable the limit-orders pallet. */ +export async function setPalletStatus(api: TypedApi, enabled: boolean): Promise { + const keyring = new Keyring({ type: "sr25519" }); + const alice = keyring.addFromUri("//Alice"); + const tx = api.tx.Sudo.sudo({ + call: api.tx.LimitOrders.set_pallet_status({ enabled }).decodedCall, + }); + await waitForTransactionWithRetry(api, tx, alice, "set_pallet_status"); +} + +/** Read the on-chain OrderStatus for a given order id (hex). */ +export async function getOrderStatus( + polkadotJs: any, + id: `0x${string}` +): Promise<"Fulfilled" | "PartiallyFilled" | "Cancelled" | undefined> { + const result = await polkadotJs.query.limitOrders.orders(id); + if (result.isNone) return undefined; + return result.unwrap().type as "Fulfilled" | "PartiallyFilled" | "Cancelled"; +} + +/** Read the on-chain OrderStatus and return the PartiallyFilled amount, or null. */ +export async function getPartiallyFilledAmount(polkadotJs: any, id: `0x${string}`): Promise { + const result = await polkadotJs.query.limitOrders.orders(id); + if (result.isNone) return null; + const status = result.unwrap(); + if (status.type !== "PartiallyFilled") return null; + return BigInt(status.asPartiallyFilled.toString()); +} + +/** Filter system events by method name. */ +export function filterEvents(events: any, method: string): any[] { + return (events as any[]).filter((e: any) => e.event.method === method); +} + +/** Read the EVM chain ID from pallet_evm_chain_id storage. */ +export async function fetchChainId(api: any): Promise { + const result = await api.query.evmChainId.chainId(); + return BigInt(result.toString()); +} + +/** + * Compute the expected `net_amount` field of `GroupExecutionSummary` for a + * mixed buy/sell batch, mirroring the pallet's netting logic. + * + * The runtime API returns `floor(price_actual * 1e9)` as a u64, so our + * bigint replication differs from the on-chain U96F32 result by at most a + * few RAO — use `toBeCloseTo` or a small tolerance window when asserting. + * + * @param polkadotJs polkadot-js ApiPromise + * @param netuid subnet id + * @param buySideTao total net TAO from buy orders (after fees, in RAO) + * @param sellSideAlpha total net alpha from sell orders (in RAO) + * @param side which side dominates ("Buy" | "Sell") + */ +export async function computeNetAmount( + polkadotJs: any, + netuid: number, + buySideTao: bigint, + sellSideAlpha: bigint, + side: "Buy" | "Sell" +): Promise { + // price_scaled = floor(price_actual * 1e9) [RAO per alpha * 1e9 / 1e9 = dimensionless] + const priceRaw = await polkadotJs.call.swapRuntimeApi.currentAlphaPrice(netuid); + const price = BigInt(priceRaw.toString()); + const SCALE = 1_000_000_000n; + + if (side === "Buy") { + // net_amount (TAO) = buy_tao - alpha_to_tao(sell_alpha, price) + // alpha_to_tao ≈ floor(price * sell_alpha / 1e9) + const sellTaoEquiv = (price * sellSideAlpha) / SCALE; + return buySideTao - sellTaoEquiv; + } else { + // net_amount (alpha) = sell_alpha - tao_to_alpha(buy_tao, price) + // tao_to_alpha ≈ floor(buy_tao * 1e9 / price) + const buyAlphaEquiv = (buySideTao * SCALE) / price; + return sellSideAlpha - buyAlphaEquiv; + } +} + +export async function executeBatchedOrders( + api: TypedApi, + netuid: number, + orders: SignedOrder[] +): Promise { + const keyring = new Keyring({ type: "sr25519" }); + const alice = keyring.addFromUri("//Alice"); + const tx = api.tx.LimitOrders.execute_batched_orders({ + netuid, + orders, + }); + await waitForTransactionWithRetry(api, tx, alice, "execute_batched_orders"); +}