From 02203f8089318df7876853f88e84baed8fa2d791 Mon Sep 17 00:00:00 2001 From: Dinonard Date: Fri, 28 Jan 2022 12:52:38 +0100 Subject: [PATCH] Initial commit --- .editorconfig | 19 + .github/ISSUE_TEMPLATE/bug_report.md | 40 + .github/ISSUE_TEMPLATE/custom.md | 10 + .github/ISSUE_TEMPLATE/feature_request.md | 13 + .github/ISSUE_TEMPLATE/future_task.md | 22 + .github/PULL_REQUEST_TEMPLATE.md | 29 + .github/workflows/fmt.yml | 13 + .github/workflows/integration.yml | 73 + .gitignore | 144 + CODE_OF_CONDUCT.md | 133 + CONTRIBUTING.md | 70 + Cargo.lock | 3050 ++++++++++++++++++ Cargo.toml | 11 + LICENSE | 674 ++++ README.md | 37 + frame/block-reward/Cargo.toml | 28 + frame/block-reward/src/lib.rs | 66 + frame/collator-selection/Cargo.toml | 62 + frame/collator-selection/README.md | 1 + frame/collator-selection/src/benchmarking.rs | 265 ++ frame/collator-selection/src/lib.rs | 518 +++ frame/collator-selection/src/mock.rs | 264 ++ frame/collator-selection/src/tests.rs | 409 +++ frame/collator-selection/src/weights.rs | 129 + frame/custom-signatures/Cargo.toml | 42 + frame/custom-signatures/src/ethereum.rs | 83 + frame/custom-signatures/src/lib.rs | 200 ++ frame/custom-signatures/src/tests.rs | 243 ++ frame/dapps-staking/Cargo.toml | 55 + frame/dapps-staking/README.md | 281 ++ frame/dapps-staking/src/benchmarking.rs | 192 ++ frame/dapps-staking/src/lib.rs | 81 + frame/dapps-staking/src/mock.rs | 263 ++ frame/dapps-staking/src/pallet/mod.rs | 735 +++++ frame/dapps-staking/src/testing_utils.rs | 208 ++ frame/dapps-staking/src/tests.rs | 1691 ++++++++++ frame/dapps-staking/src/traits.rs | 5 + frame/dapps-staking/src/weights.rs | 186 ++ frame/vesting/Cargo.toml | 43 + frame/vesting/README.md | 31 + frame/vesting/src/benchmarking.rs | 383 +++ frame/vesting/src/lib.rs | 853 +++++ frame/vesting/src/migrations.rs | 98 + frame/vesting/src/mock.rs | 160 + frame/vesting/src/tests.rs | 1304 ++++++++ frame/vesting/src/vesting_info.rs | 118 + frame/vesting/src/weights.rs | 253 ++ precompiles/staking/Cargo.toml | 37 + precompiles/staking/Staking.sol | 21 + precompiles/staking/src/lib.rs | 108 + 50 files changed, 13754 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/custom.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/future_task.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/fmt.yml create mode 100644 .github/workflows/integration.yml create mode 100644 .gitignore create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 frame/block-reward/Cargo.toml create mode 100644 frame/block-reward/src/lib.rs create mode 100644 frame/collator-selection/Cargo.toml create mode 100644 frame/collator-selection/README.md create mode 100644 frame/collator-selection/src/benchmarking.rs create mode 100644 frame/collator-selection/src/lib.rs create mode 100644 frame/collator-selection/src/mock.rs create mode 100644 frame/collator-selection/src/tests.rs create mode 100644 frame/collator-selection/src/weights.rs create mode 100644 frame/custom-signatures/Cargo.toml create mode 100644 frame/custom-signatures/src/ethereum.rs create mode 100644 frame/custom-signatures/src/lib.rs create mode 100644 frame/custom-signatures/src/tests.rs create mode 100644 frame/dapps-staking/Cargo.toml create mode 100644 frame/dapps-staking/README.md create mode 100644 frame/dapps-staking/src/benchmarking.rs create mode 100644 frame/dapps-staking/src/lib.rs create mode 100644 frame/dapps-staking/src/mock.rs create mode 100644 frame/dapps-staking/src/pallet/mod.rs create mode 100644 frame/dapps-staking/src/testing_utils.rs create mode 100644 frame/dapps-staking/src/tests.rs create mode 100644 frame/dapps-staking/src/traits.rs create mode 100644 frame/dapps-staking/src/weights.rs create mode 100644 frame/vesting/Cargo.toml create mode 100644 frame/vesting/README.md create mode 100644 frame/vesting/src/benchmarking.rs create mode 100644 frame/vesting/src/lib.rs create mode 100644 frame/vesting/src/migrations.rs create mode 100644 frame/vesting/src/mock.rs create mode 100644 frame/vesting/src/tests.rs create mode 100644 frame/vesting/src/vesting_info.rs create mode 100644 frame/vesting/src/weights.rs create mode 100644 precompiles/staking/Cargo.toml create mode 100644 precompiles/staking/Staking.sol create mode 100644 precompiles/staking/src/lib.rs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..d2c7e911 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 + +[*.yml] +indent_style=space +indent_size=2 +tab_width=8 +end_of_line=lf diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..1d9f2870 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,40 @@ +--- +name: Report a Bug +about: Report a problem with this project. +title: '' +labels: bug +assignees: '' +--- + +**Description** + +> Tell us what happened. In particular, tell us how and why you are using this project, and describe the bug that you encountered. Please note that we are not able to support all conceivable use cases, but the more information you are able to provide the more equipped we will be to help. + +**Steps to Reproduce** + +> Replace the example steps below with actual steps to reproduce the bug you're reporting. + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected vs. Actual Behavior** + +> What did you expect to happen after you followed the steps you described in the last section? What actually happened? + +**Environment** + +> Describe the environment in which you encountered this bug. Use the list below as a starting point and add additional information if you think it's relevant. + +- Operating system: +- Project version/tag: +- Rust version (run `rustup show`): + +**Logs, Errors or Screenshots** + +> Please provide the text of any logs or errors that you experienced; if applicable, provide screenshots to help illustrate the problem. + +**Additional Information** + +> Please add any other details that you think may help us solve your problem. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 00000000..48d5f81f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..1b878a3a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,13 @@ +# Description +{write description of what the feature should be} + +## Required Tasks +- [ ] {task 1} +- [ ] {task 2} +- [ ] ... + +## Expired(Estimating). +{When finished this task?} + +## Dependencies +{Dependencies issue or PR.} diff --git a/.github/ISSUE_TEMPLATE/future_task.md b/.github/ISSUE_TEMPLATE/future_task.md new file mode 100644 index 00000000..3e9723cd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/future_task.md @@ -0,0 +1,22 @@ +--- +name: Future task issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + +# Description +{write description of what the feature should be} + +## Required Tasks +- [ ] {task 1} +- [ ] {task 2} +- [ ] ... + +## Estimated done. +{When finished this task?} + +## Dependencies +{Dependencies issue or PR.} diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..ab60ee9c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,29 @@ +**Pull Request Summary** + +> Describe what changes this pull request makes to the repository + +**Check list** +- [ ] contains breaking changes +- [ ] adds new feature +- [ ] modifies existing feature (bug fix or improvements) +- [ ] updated spec version +- [ ] relies on other tasks +- [ ] documentation changes +- [ ] tests and/or benchmarks are included +- [ ] changed API client type definition or chain metadata + +**This pull request makes the following changes:** + +**Adds** +- (ex: Add feature A) + +**Fixes** +- (ex: Fix validation function) + +**Changes** +- (ex: Change document B) + +**To-dos** +> *Feel free to remove this section if it's not applicable + +- [ ] (ex: add user list) diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml new file mode 100644 index 00000000..4ced7302 --- /dev/null +++ b/.github/workflows/fmt.yml @@ -0,0 +1,13 @@ +name: fmt +on: [push] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: hecrj/setup-rust-action@v1 + - name: Checkout the source code + uses: actions/checkout@master + - name: Check targets are installed correctly + run: rustup target list --installed + - name: Check fmt + run: cargo fmt -- --check diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 00000000..620aabd5 --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,73 @@ +on: [push] + +name: Integration + +env: + RUST_VERSION: 'nightly-2021-11-01' + +jobs: + native: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macOS-latest] + steps: + - uses: hecrj/setup-rust-action@v1 + with: + rust-version: ${{ env.RUST_VERSION }} + targets: 'wasm32-unknown-unknown' + + - name: Checkout the source code + uses: actions/checkout@master + with: + submodules: true + + - name: Check targets are installed correctly + run: rustup target list --installed + + - name: Run all tests + run: cargo test --all + + - name: Cleanup after tests + run: cargo clean + + - name: Build optimized binary + run: cargo check --release --verbose + + runtime-benchmarks: + runs-on: ubuntu-latest + steps: + - uses: hecrj/setup-rust-action@v1 + with: + rust-version: ${{ env.RUST_VERSION }} + targets: 'wasm32-unknown-unknown' + + - name: Checkout the source code + uses: actions/checkout@master + with: + submodules: true + + - name: Check targets are installed correctly + run: rustup target list --installed + + - name: Build optimized binary for benchmarking + run: cargo check --release --features runtime-benchmarks + + try-runtime: + runs-on: ubuntu-latest + steps: + - uses: hecrj/setup-rust-action@v1 + with: + rust-version: ${{ env.RUST_VERSION }} + targets: 'wasm32-unknown-unknown' + + - name: Checkout the source code + uses: actions/checkout@master + with: + submodules: true + + - name: Check targets are installed correctly + run: rustup target list --installed + + - name: Check try-runtime compilation + run: cargo check --release --features try-runtime diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..1df5fd84 --- /dev/null +++ b/.gitignore @@ -0,0 +1,144 @@ +# Created by https://www.gitignore.io/api/rust,macos,linux,jetbrains +# Edit at https://www.gitignore.io/?templates=rust,macos,linux,jetbrains + +### JetBrains ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### JetBrains Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +.idea/sonarlint + +.idea/** + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Rust ### +# Generated by Cargo +# will have compiled files and executables +**/target/** + +# These are backup files generated by rustfmt +**/*.rs.bk + +# End of https://www.gitignore.io/api/rust,macos,linux,jetbrains + +### build-for-debian ### +**/target-debian/** + +# Testnet keys +.keys + +.vscode diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..96e3f077 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +info@stake.co.jp. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..0318b38f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,70 @@ +# Contributing + +Welcome potential contributor of `Astar Network`! The Astar Network project (formerly known as Plasm Network) is a collection of **Open Source Projects** maintained by the Astar Team and Stake Technologies. We want to make contributing to this project as easy and transparent as possible. + +This document will act as a starting point for those who want to be part of the Astar Ecosystem, which includes code contribution and community contribution. + +## Types of Contribution + +We welcome any types of contributions that can improve the project/network in any shape or form, may it be directly to the Astar repository codebase, feedback, or making community contributions. You don't have to be a developer to contribute to the network. + +## Using GitHub + +The Astar Network project uses GitHub as the main source control hosting service. Most forms of communication regarding changes to the code will be done within the issue board of the repository. + +### Opening an Issue + +Contributions within GitHub can take on the following forms: + +- **Bug Report**: If you find any bugs or unexpected behaviors using the Astar node, please open an issue that describes the issue and other information that the developer may need when investigating. +- **User Questions**: Posting your question that is not addressed on our [official docs](https://docs.astar.network/), [Substrate docs](https://substrate.dev/docs/en/), or through other issue tickets helps us improve our wiki and keep the community informed. For any inquiries related to the usage/development of the code, please open an issue on our repository. +- **Feature Request**: If you have any suggestions or requests for a feature that can be made within a *relatively short development time*, feel free to open an issue that describes the 'what,' 'why,' 'how,' of the feature. + +### Code Changes (Pull Request) + +For those who wish to make changes to the source code, please ensure that there is an open issue that is related to the changes you're trying to make. *You must open an issue before you open a pull request* as this allows us to understand what changes will come and prevent stale pull requests. The issue should contain a rough description of how you are planning on changing the code to fix or add features. Once the repository maintainer gives the green light, you can fork the repository and open a pull request with your changes to our main branch (Dusty). + +In short: + +1. Open an issue regarding a bug fix or feature request (fill in our issue templates) +2. Briefly describe how you plan to make changes to the code +3. Fork our main branch (Dusty) +4. Open a pull request to the main branch (fill in our pull request template) +5. Ensure all workflow checks have passed +6. Wait for the maintainers approval or change requests +7. Your code will be merged + +### Coding Styles + +Contributors should adhere to the [house coding style](https://substrate.dev/recipes/) and the [`rustfmt` styles](https://github.com/rust-lang/rustfmt). + +### Branch Rules and Release Process + +![branch-release](https://mermaid.ink/img/eyJjb2RlIjoiZ3JhcGggVERcbiAgICBGRUFUVVJFW2ZlYXR1cmUgYnJhbmNoXSAtLT58QWRkcyBuZXcgZmVhdHVyZXwgUFIocHVsbCByZXF1ZXN0KVxuICAgIEZJWFtmaXggYnJhbmNoXSAtLT58Rml4ZXMgYnVnfCBQUihwdWxsIHJlcXVlc3QpXG4gICAgRE9DW2RvY3VtZW50YXRpb24gYnJhbmNoXSAtLT58QWRkcyBkb2N1bWVudGF0aW9ufCBQUihvcGVuIHB1bGwgcmVxdWVzdClcbiAgICBQUiAtLT58SW5jcmVtZW50IHNwZWMgdmVyICYgTWVyZ2UgYnJhbmNofCBERVZbZGV2ZWxvcG1lbnQgYnJhbmNoXVxuICAgIERFViAtLT4gVEVTVChuZXR3b3JrIHRlc3RpbmcpXG4gICAgVEVTVCAtLT4gfEltcHJvdmVtZW50c3wgUFJcbiAgICBURVNUIC0tPiB8Q3JlYXRlIG5ldyByZWxlYXNlIHRhZ3wgUkVMRUFTRVtydW50aW1lIHVwZ3JhZGVdIiwibWVybWFpZCI6e30sInVwZGF0ZUVkaXRvciI6ZmFsc2UsImF1dG9TeW5jIjp0cnVlLCJ1cGRhdGVEaWFncmFtIjpmYWxzZX0) + +All branch names should adhere to the following rules: + +- `feature/*`: +- `documentation/*`: +- `fix/*`: +- `development/*`: nodes that are actively in development (including release candidates) will have the `development/` prefix in their branch name. + +Every major features made for the `development` branch must go through at least one week of internal testing before it is released and deployed. + +Due to the different base runtime version for each chain, we need to maintain Astar Ecosystem in separate branches. +We will be improving this project structure in the near future, but to maintain network stability, major runtime upgrades will follow this process: + +- `development/dusty` → `production/astar` (independent blockchain network planed to be a Parachain of Polkadot network) +- `development/shiden` → `production/shiden` (Parachain of Kusama and focused on cutting edge XCMP development) + +In the future, we will merge Dusty Network and Shibuya Network into a single Parachain so that the release flow will be as the following: + +`development/unnamed-testnet` → `production/shiden` → `production/astar` + +### Contributor Licenses + +By contributing, you agree that your contributions will be licensed under the [GNU General Public License v3.0](https://github.com/PlasmNetwork/Astar/blob/development/dusty/LICENSE) as is with the Astar source code. If you have any concerns regarding this matter, please contact the maintainer. + +## Community Contribution + +As a public blockchain network, Astar Network welcomes any contributions that help our community members and create the best blockchain network. Anyone can interact with the community through our [official forum](https://forum.astar.network/), [discord](https://discord.gg/aApeT8r2e4), and [Telegram](https://t.me/PlasmOfficial). You can contribute to the community by actively participating in the forum discussions, helping other members, or sharing Astar Network with others. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..1f68aa12 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3050 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.4", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3" + +[[package]] +name = "approx" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "072df7202e63b127ab55acfe16ce97013d5b97bf160489336d3f1840fd78e99e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" +dependencies = [ + "nodrop", +] + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "async-trait" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "backtrace" +version = "0.3.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321629d8ba6513061f26707241fa9bc89524ff1cd7a915a97ef0c62c666ce1b6" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base58" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitvec" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2-rfc" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" +dependencies = [ + "arrayvec 0.4.12", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding 0.1.5", + "byte-tools", + "byteorder", + "generic-array 0.12.4", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "block-padding 0.2.1", + "generic-array 0.14.5", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + +[[package]] +name = "bumpalo" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" + +[[package]] +name = "byte-slice-cast" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c751592b77c499e7bce34d99d67c2c11bdc0574e9a488ddade14150a4698" + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cc" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "winapi", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cpufeatures" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array 0.14.5", + "subtle", +] + +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array 0.14.5", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b85542f99a2dfa2a1b8e192662741c9859a846b296bef1c92ef9b58b5a216" +dependencies = [ + "byteorder", + "digest 0.8.1", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.5", +] + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "dyn-clonable" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e9232f0e607a262ceb9bd5141a3dfb3e4db6994b31989bbfd845878cba59fd4" +dependencies = [ + "dyn-clonable-impl", + "dyn-clone", +] + +[[package]] +name = "dyn-clonable-impl" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558e40ea573c374cf53507fd240b7ee2f5477df7cfebdb97323ec61c719399c5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dyn-clone" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf" + +[[package]] +name = "ed25519" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74e1069e39f1454367eb2de793ed062fac4c35c2934b76a81d90dd9abcd28816" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek 3.2.0", + "ed25519", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "environmental" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b91989ae21441195d7d9b9993a2f9295c7e1a8c96255d8b729accddc124797" + +[[package]] +name = "ethbloom" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb684ac8fa8f6c5759f788862bb22ec6fe3cb392f6bfd08e3c64b603661e3f8" +dependencies = [ + "crunchy", + "fixed-hash", + "impl-codec", + "impl-rlp", + "impl-serde", + "scale-info", + "tiny-keccak", +] + +[[package]] +name = "ethereum" +version = "0.9.0" +source = "git+https://github.com/PlasmNetwork/ethereum?branch=polkadot-v0.9.13#2b98173bd88c05bfeefad421d260101b9008497b" +dependencies = [ + "bytes", + "ethereum-types", + "hash-db", + "hash256-std-hasher", + "parity-scale-codec", + "rlp", + "rlp-derive", + "scale-info", + "serde", + "sha3 0.9.1", + "triehash", +] + +[[package]] +name = "ethereum-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05136f7057fe789f06e6d41d07b34e6f70d8c86e5693b60f97aaa6553553bdaf" +dependencies = [ + "ethbloom", + "fixed-hash", + "impl-codec", + "impl-rlp", + "impl-serde", + "primitive-types", + "scale-info", + "uint", +] + +[[package]] +name = "evm" +version = "0.30.1" +source = "git+https://github.com/PlasmNetwork/evm?branch=polkadot-v0.9.13#d83f8f43699ceaa3f19f7c17d909dd079c482420" +dependencies = [ + "environmental", + "ethereum", + "evm-core", + "evm-gasometer", + "evm-runtime", + "log", + "parity-scale-codec", + "primitive-types", + "rlp", + "scale-info", + "serde", + "sha3 0.8.2", +] + +[[package]] +name = "evm-core" +version = "0.30.0" +source = "git+https://github.com/PlasmNetwork/evm?branch=polkadot-v0.9.13#d83f8f43699ceaa3f19f7c17d909dd079c482420" +dependencies = [ + "funty", + "parity-scale-codec", + "primitive-types", + "scale-info", + "serde", +] + +[[package]] +name = "evm-gasometer" +version = "0.30.0" +source = "git+https://github.com/PlasmNetwork/evm?branch=polkadot-v0.9.13#d83f8f43699ceaa3f19f7c17d909dd079c482420" +dependencies = [ + "environmental", + "evm-core", + "evm-runtime", + "primitive-types", +] + +[[package]] +name = "evm-runtime" +version = "0.30.0" +source = "git+https://github.com/PlasmNetwork/evm?branch=polkadot-v0.9.13#d83f8f43699ceaa3f19f7c17d909dd079c482420" +dependencies = [ + "environmental", + "evm-core", + "primitive-types", + "sha3 0.8.2", +] + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "fixed-hash" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" +dependencies = [ + "byteorder", + "rand 0.8.4", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fp-evm" +version = "3.0.0-dev" +source = "git+https://github.com/PlasmNetwork/frontier?branch=polkadot-v0.9.13#d08ed161bf32f83c74f5394c92996475701f136e" +dependencies = [ + "evm", + "impl-trait-for-tuples 0.1.3", + "parity-scale-codec", + "serde", + "sp-core", + "sp-std", +] + +[[package]] +name = "frame-benchmarking" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "frame-support", + "frame-system", + "linregress", + "log", + "parity-scale-codec", + "paste", + "scale-info", + "sp-api", + "sp-io", + "sp-runtime", + "sp-runtime-interface", + "sp-std", + "sp-storage", +] + +[[package]] +name = "frame-metadata" +version = "14.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ed5e5c346de62ca5c184b4325a6600d1eaca210666e4606fe4e449574978d0" +dependencies = [ + "cfg-if", + "parity-scale-codec", + "scale-info", + "serde", +] + +[[package]] +name = "frame-support" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "bitflags", + "frame-metadata", + "frame-support-procedural", + "impl-trait-for-tuples 0.2.1", + "log", + "once_cell", + "parity-scale-codec", + "paste", + "scale-info", + "serde", + "smallvec", + "sp-arithmetic", + "sp-core", + "sp-core-hashing-proc-macro", + "sp-inherents", + "sp-io", + "sp-runtime", + "sp-staking", + "sp-state-machine", + "sp-std", + "sp-tracing", + "tt-call", +] + +[[package]] +name = "frame-support-procedural" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "Inflector", + "frame-support-procedural-tools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "frame-support-procedural-tools" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "frame-support-procedural-tools-derive", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "frame-support-procedural-tools-derive" +version = "3.0.0" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "frame-system" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "frame-support", + "log", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "sp-version", +] + +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + +[[package]] +name = "futures" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" + +[[package]] +name = "futures-executor" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", + "num_cpus", +] + +[[package]] +name = "futures-io" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2" + +[[package]] +name = "futures-macro" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508" + +[[package]] +name = "futures-task" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" + +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + +[[package]] +name = "futures-util" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gimli" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" + +[[package]] +name = "hash-db" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d23bd4e7b5eda0d0f3a307e8b381fdc8ba9000f26fbe912250c0a4cc3956364a" + +[[package]] +name = "hash256-std-hasher" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c171d55b98633f4ed3860808f004099b36c1cc29c42cfc53aa8591b21efcf2" +dependencies = [ + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "961de220ec9a91af2e1e5bd80d02109155695e516771762381ef8581317066e0" +dependencies = [ + "hex-literal-impl", + "proc-macro-hack", +] + +[[package]] +name = "hex-literal-impl" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "853f769599eb31de176303197b7ba4973299c38c7a7604a6bc88c3eef05b9b46" +dependencies = [ + "proc-macro-hack", +] + +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac 0.8.0", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac 0.11.1", + "digest 0.9.0", +] + +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array 0.14.5", + "hmac 0.8.1", +] + +[[package]] +name = "impl-codec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161ebdfec3c8e3b52bf61c4f3550a1eea4f9579d10dc1b936f3171ebdcd6c443" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-rlp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +dependencies = [ + "rlp", +] + +[[package]] +name = "impl-serde" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4551f042f3438e64dbd6226b20527fc84a6e1fe65688b58746a2f53623f25f5c" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef5550a42e3740a0e71f909d4c861056a284060af885ae7aa6242820f920d9d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5dacb10c5b3bb92d46ba347505a9041e676bb20ad220101326bffb0c93031ee" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", +] + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "js-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" + +[[package]] +name = "libm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" + +[[package]] +name = "libsecp256k1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d220bc1feda2ac231cb78c3d26f27676b8cf82c96971f7aeef3d0cf2797c73" +dependencies = [ + "arrayref", + "base64 0.12.3", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core 0.2.2", + "libsecp256k1-gen-ecmult 0.2.1", + "libsecp256k1-gen-genmult 0.2.1", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "typenum", +] + +[[package]] +name = "libsecp256k1" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0452aac8bab02242429380e9b2f94ea20cea2b37e2c1777a1358799bbe97f37" +dependencies = [ + "arrayref", + "base64 0.13.0", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core 0.3.0", + "libsecp256k1-gen-ecmult 0.3.0", + "libsecp256k1-gen-genmult 0.3.0", + "rand 0.8.4", + "serde", + "sha2 0.9.9", + "typenum", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f6ab710cec28cef759c5f18671a27dae2a5f952cdaaee1d8e2908cb2478a80" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3" +dependencies = [ + "libsecp256k1-core 0.2.2", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core 0.3.0", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d" +dependencies = [ + "libsecp256k1-core 0.2.2", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core 0.3.0", +] + +[[package]] +name = "linregress" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6c601a85f5ecd1aba625247bca0031585fb1c446461b142878a16f8245ddeb8" +dependencies = [ + "nalgebra", + "statrs", +] + +[[package]] +name = "lock_api" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matchers" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matrixmultiply" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add85d4dd35074e6fedc608f8c8f513a3548619a9024b751949ef0e8e45a4d84" +dependencies = [ + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "memory-db" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de006e09d04fc301a5f7e817b75aa49801c4479a8af753764416b085337ddcc5" +dependencies = [ + "hash-db", + "hashbrown", + "parity-util-mem", +] + +[[package]] +name = "memory_units" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d96e3f3c0b6325d8ccd83c33b28acb183edcb6c67938ba104ec546854b0882" + +[[package]] +name = "merlin" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e261cf0f8b3c42ded9f7d2bb59dea03aa52bc8a1cbc7482f9fc3fd1229d3b42" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.5.1", + "zeroize", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "nalgebra" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "462fffe4002f4f2e1f6a9dcf12cc1a6fc0e15989014efc02a941d3e0f5dc2120" +dependencies = [ + "approx", + "matrixmultiply", + "nalgebra-macros", + "num-complex", + "num-rational 0.4.0", + "num-traits", + "rand 0.8.4", + "rand_distr", + "simba", + "typenum", +] + +[[package]] +name = "nalgebra-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01fcc0b8149b4632adc89ac3b7b31a12fb6099a0317a4eb2ebff574ef7de7218" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ac1d3f9a1d3616fd9a60c8d74296f22406a238b6a72f5cc1e6f314df4ffbf9" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "pallet-authorship" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "frame-support", + "frame-system", + "impl-trait-for-tuples 0.2.1", + "parity-scale-codec", + "scale-info", + "sp-authorship", + "sp-runtime", + "sp-std", +] + +[[package]] +name = "pallet-balances" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-runtime", + "sp-std", +] + +[[package]] +name = "pallet-block-reward" +version = "0.2.0" +dependencies = [ + "frame-support", + "frame-system", + "parity-scale-codec", + "scale-info", +] + +[[package]] +name = "pallet-collator-selection" +version = "3.0.0" +source = "git+https://github.com/paritytech/cumulus?branch=polkadot-v0.9.13#05cc5f0e2acacc18796f45ffa3c7b4626fd1046d" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-authorship", + "pallet-session", + "parity-scale-codec", + "rand 0.7.3", + "scale-info", + "serde", + "sp-runtime", + "sp-staking", + "sp-std", +] + +[[package]] +name = "pallet-custom-signatures" +version = "4.3.0" +dependencies = [ + "frame-support", + "frame-system", + "hex-literal", + "libsecp256k1 0.6.0", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-keyring", + "sp-runtime", + "sp-std", +] + +[[package]] +name = "pallet-dapps-staking" +version = "1.1.2" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "num-traits", + "pallet-balances", + "pallet-session", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "serde", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-runtime", + "sp-staking", + "sp-std", +] + +[[package]] +name = "pallet-evm" +version = "6.0.0-dev" +source = "git+https://github.com/PlasmNetwork/frontier?branch=polkadot-v0.9.13#d08ed161bf32f83c74f5394c92996475701f136e" +dependencies = [ + "evm", + "evm-gasometer", + "evm-runtime", + "fp-evm", + "frame-benchmarking", + "frame-support", + "frame-system", + "hex", + "log", + "pallet-balances", + "pallet-timestamp", + "parity-scale-codec", + "primitive-types", + "rlp", + "scale-info", + "serde", + "sha3 0.8.2", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + +[[package]] +name = "pallet-precompile-staking" +version = "0.2.1" +dependencies = [ + "evm", + "frame-support", + "frame-system", + "pallet-collator-selection", + "pallet-evm", + "pallet-session", + "parity-scale-codec", + "sp-core", + "sp-std", +] + +[[package]] +name = "pallet-session" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "frame-support", + "frame-system", + "impl-trait-for-tuples 0.2.1", + "log", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-session", + "sp-staking", + "sp-std", + "sp-trie", +] + +[[package]] +name = "pallet-timestamp" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-inherents", + "sp-runtime", + "sp-std", + "sp-timestamp", +] + +[[package]] +name = "parity-scale-codec" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373b1a4c1338d9cd3d1fa53b3a11bdab5ab6bd80a20f7f7becd76953ae2be909" +dependencies = [ + "arrayvec 0.7.2", + "bitvec", + "byte-slice-cast", + "impl-trait-for-tuples 0.2.1", + "parity-scale-codec-derive", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1557010476e0595c9b568d16dcfb81b93cdeb157612726f5170d31aa707bed27" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "parity-util-mem" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f4cb4e169446179cbc6b8b6320cc9fca49bd2e94e8db25f25f200a8ea774770" +dependencies = [ + "cfg-if", + "hashbrown", + "impl-trait-for-tuples 0.2.1", + "parity-util-mem-derive", + "parking_lot", + "primitive-types", + "winapi", +] + +[[package]] +name = "parity-util-mem-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f557c32c6d268a07c921471619c0295f5efad3a0e76d4f97a05c091a51d110b2" +dependencies = [ + "proc-macro2", + "syn", + "synstructure", +] + +[[package]] +name = "parity-wasm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be5e13c266502aadf83426d87d81a0f5d1ef45b8027f5a471c360abfe4bfae92" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "paste" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5" + +[[package]] +name = "pbkdf2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216eaa586a190f0a738f2f918511eecfa90f13295abec0e457cdebcceda80cbd" +dependencies = [ + "crypto-mac 0.8.0", +] + +[[package]] +name = "pbkdf2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa" +dependencies = [ + "crypto-mac 0.11.1", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "primitive-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e4722c697a58a99d5d06a08c30821d7c082a4632198de1eaa5a6c22ef42373" +dependencies = [ + "fixed-hash", + "impl-codec", + "impl-rlp", + "impl-serde", + "scale-info", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" +dependencies = [ + "thiserror", + "toml", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.4", +] + +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.4", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300f2a835d808734ee295d45007adacb9ebb29dd3ae2424acfa17930cae541da" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c38e3aecd2b21cb3959637b883bb3714bc7e43f0268b9a29d3743ee3e55cdd2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "rlp" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "999508abb0ae792aabed2460c45b89106d97fe4adac593bdaef433c2605847b5" +dependencies = [ + "bytes", + "rustc-hex", +] + +[[package]] +name = "rlp-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33d7b2abe0c340d8797fe2907d3f20d3b5ea5908683618bfe80df7f621f672a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "scale-info" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55b744399c25532d63a0d2789b109df8d46fc93752d46b0782991a931a782f" +dependencies = [ + "bitvec", + "cfg-if", + "derive_more", + "parity-scale-codec", + "scale-info-derive", + "serde", +] + +[[package]] +name = "scale-info-derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baeb2780690380592f86205aa4ee49815feb2acad8c2f59e6dd207148c3f1fcd" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "schnorrkel" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021b403afe70d81eea68f6ea12f6b3c9588e5d536a94c3bf80f15e7faa267862" +dependencies = [ + "arrayref", + "arrayvec 0.5.2", + "curve25519-dalek 2.1.3", + "getrandom 0.1.16", + "merlin", + "rand 0.7.3", + "rand_core 0.5.1", + "sha2 0.8.2", + "subtle", + "zeroize", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" + +[[package]] +name = "serde" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c059c05b48c5c0067d4b4b2b4f0732dd65feb52daf7e0ea09cd87e7dadc1af79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" +dependencies = [ + "block-buffer 0.7.3", + "digest 0.8.1", + "fake-simd", + "opaque-debug 0.2.3", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug 0.3.0", +] + +[[package]] +name = "sha3" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd26bc0e7a2e3a7c959bc494caf58b72ee0c71d67704e9520f736ca7e4853ecf" +dependencies = [ + "block-buffer 0.7.3", + "byte-tools", + "digest 0.8.1", + "keccak", + "opaque-debug 0.2.3", +] + +[[package]] +name = "sha3" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "keccak", + "opaque-debug 0.3.0", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signature" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4" + +[[package]] +name = "simba" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e82063457853d00243beda9952e910b82593e4b07ae9f721b9278a99a0d3d5c" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", +] + +[[package]] +name = "slab" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "sp-api" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "hash-db", + "log", + "parity-scale-codec", + "sp-api-proc-macro", + "sp-core", + "sp-runtime", + "sp-state-machine", + "sp-std", + "sp-version", + "thiserror", +] + +[[package]] +name = "sp-api-proc-macro" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "blake2-rfc", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sp-application-crypto" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-std", +] + +[[package]] +name = "sp-arithmetic" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "integer-sqrt", + "num-traits", + "parity-scale-codec", + "scale-info", + "serde", + "sp-debug-derive", + "sp-std", + "static_assertions", +] + +[[package]] +name = "sp-authorship" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "async-trait", + "parity-scale-codec", + "sp-inherents", + "sp-runtime", + "sp-std", +] + +[[package]] +name = "sp-core" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "base58", + "bitflags", + "blake2-rfc", + "byteorder", + "dyn-clonable", + "ed25519-dalek", + "futures", + "hash-db", + "hash256-std-hasher", + "hex", + "impl-serde", + "lazy_static", + "libsecp256k1 0.7.0", + "log", + "merlin", + "num-traits", + "parity-scale-codec", + "parity-util-mem", + "parking_lot", + "primitive-types", + "rand 0.7.3", + "regex", + "scale-info", + "schnorrkel", + "secrecy", + "serde", + "sha2 0.9.9", + "sp-core-hashing", + "sp-debug-derive", + "sp-externalities", + "sp-runtime-interface", + "sp-std", + "sp-storage", + "ss58-registry", + "substrate-bip39", + "thiserror", + "tiny-bip39", + "tiny-keccak", + "twox-hash", + "wasmi", + "zeroize", +] + +[[package]] +name = "sp-core-hashing" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "blake2-rfc", + "byteorder", + "sha2 0.9.9", + "sp-std", + "tiny-keccak", + "twox-hash", +] + +[[package]] +name = "sp-core-hashing-proc-macro" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "proc-macro2", + "quote", + "sp-core-hashing", + "syn", +] + +[[package]] +name = "sp-debug-derive" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sp-externalities" +version = "0.10.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "environmental", + "parity-scale-codec", + "sp-std", + "sp-storage", +] + +[[package]] +name = "sp-inherents" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "async-trait", + "impl-trait-for-tuples 0.2.1", + "parity-scale-codec", + "sp-core", + "sp-runtime", + "sp-std", + "thiserror", +] + +[[package]] +name = "sp-io" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "futures", + "hash-db", + "libsecp256k1 0.7.0", + "log", + "parity-scale-codec", + "parking_lot", + "sp-core", + "sp-externalities", + "sp-keystore", + "sp-runtime-interface", + "sp-state-machine", + "sp-std", + "sp-tracing", + "sp-trie", + "sp-wasm-interface", + "tracing", + "tracing-core", +] + +[[package]] +name = "sp-keyring" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "lazy_static", + "sp-core", + "sp-runtime", + "strum", +] + +[[package]] +name = "sp-keystore" +version = "0.10.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "async-trait", + "derive_more", + "futures", + "merlin", + "parity-scale-codec", + "parking_lot", + "schnorrkel", + "sp-core", + "sp-externalities", +] + +[[package]] +name = "sp-panic-handler" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "backtrace", + "lazy_static", + "regex", +] + +[[package]] +name = "sp-runtime" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "either", + "hash256-std-hasher", + "impl-trait-for-tuples 0.2.1", + "log", + "parity-scale-codec", + "parity-util-mem", + "paste", + "rand 0.7.3", + "scale-info", + "serde", + "sp-application-crypto", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-std", +] + +[[package]] +name = "sp-runtime-interface" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "impl-trait-for-tuples 0.2.1", + "parity-scale-codec", + "primitive-types", + "sp-externalities", + "sp-runtime-interface-proc-macro", + "sp-std", + "sp-storage", + "sp-tracing", + "sp-wasm-interface", + "static_assertions", +] + +[[package]] +name = "sp-runtime-interface-proc-macro" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "Inflector", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sp-session" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "parity-scale-codec", + "scale-info", + "sp-api", + "sp-core", + "sp-runtime", + "sp-staking", + "sp-std", +] + +[[package]] +name = "sp-staking" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "parity-scale-codec", + "scale-info", + "sp-runtime", + "sp-std", +] + +[[package]] +name = "sp-state-machine" +version = "0.10.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "hash-db", + "log", + "num-traits", + "parity-scale-codec", + "parking_lot", + "rand 0.7.3", + "smallvec", + "sp-core", + "sp-externalities", + "sp-panic-handler", + "sp-std", + "sp-trie", + "thiserror", + "tracing", + "trie-db", + "trie-root", +] + +[[package]] +name = "sp-std" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" + +[[package]] +name = "sp-storage" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "impl-serde", + "parity-scale-codec", + "ref-cast", + "serde", + "sp-debug-derive", + "sp-std", +] + +[[package]] +name = "sp-timestamp" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "async-trait", + "futures-timer", + "log", + "parity-scale-codec", + "sp-api", + "sp-inherents", + "sp-runtime", + "sp-std", + "thiserror", +] + +[[package]] +name = "sp-tracing" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "parity-scale-codec", + "sp-std", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "sp-trie" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "hash-db", + "memory-db", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-std", + "trie-db", + "trie-root", +] + +[[package]] +name = "sp-version" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "impl-serde", + "parity-scale-codec", + "parity-wasm", + "scale-info", + "serde", + "sp-runtime", + "sp-std", + "sp-version-proc-macro", + "thiserror", +] + +[[package]] +name = "sp-version-proc-macro" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "parity-scale-codec", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sp-wasm-interface" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.13#afb74de23dfe2994e7ce38c0870efb9734e966f7" +dependencies = [ + "impl-trait-for-tuples 0.2.1", + "parity-scale-codec", + "sp-std", + "wasmi", +] + +[[package]] +name = "ss58-registry" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1230685dc82f8699110640244d361a7099c602f08bddc5c90765a5153b4881dc" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "serde", + "serde_json", + "unicode-xid", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "statrs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05bdbb8e4e78216a85785a85d3ec3183144f98d0097b9281802c019bb07a6f05" +dependencies = [ + "approx", + "lazy_static", + "nalgebra", + "num-traits", + "rand 0.8.4", +] + +[[package]] +name = "strum" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7ac893c7d471c8a21f31cfe213ec4f6d9afeed25537c772e08ef3f005f8729e" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339f799d8b549e3744c7ac7feb216383e4005d94bdb22561b3ab8f3b808ae9fb" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "substrate-bip39" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49eee6965196b32f882dd2ee85a92b1dbead41b04e53907f269de3b0dc04733c" +dependencies = [ + "hmac 0.11.0", + "pbkdf2 0.8.0", + "schnorrkel", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tiny-bip39" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc59cb9dfc85bb312c3a78fd6aa8a8582e310b0fa885d5bb877f6dcc601839d" +dependencies = [ + "anyhow", + "hmac 0.8.1", + "once_cell", + "pbkdf2 0.4.0", + "rand 0.7.3", + "rustc-hash", + "sha2 0.9.9", + "thiserror", + "unicode-normalization", + "wasm-bindgen", + "zeroize", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "tracing" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "tracing-log" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb65ea441fbb84f9f6748fd496cf7f63ec9af5bca94dd86456978d055e8eb28b" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "ansi_term", + "chrono", + "lazy_static", + "matchers", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "trie-db" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eac131e334e81b6b3be07399482042838adcd7957aa0010231d0813e39e02fa" +dependencies = [ + "hash-db", + "hashbrown", + "log", + "rustc-hex", + "smallvec", +] + +[[package]] +name = "trie-root" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "652931506d2c1244d7217a70b99f56718a7b4161b37f04e7cd868072a99f68cd" +dependencies = [ + "hash-db", +] + +[[package]] +name = "triehash" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1631b201eb031b563d2e85ca18ec8092508e262a3196ce9bd10a67ec87b9f5c" +dependencies = [ + "hash-db", + "rlp", +] + +[[package]] +name = "tt-call" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e66dcbec4290c69dd03c57e76c2469ea5c7ce109c6dd4351c13055cf71ea055" + +[[package]] +name = "twox-hash" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee73e6e4924fe940354b8d4d98cad5231175d615cd855b758adc658c0aac6a0" +dependencies = [ + "cfg-if", + "rand 0.8.4", + "static_assertions", +] + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "uint" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" + +[[package]] +name = "wasmi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca00c5147c319a8ec91ec1a0edbec31e566ce2c9cc93b3f9bb86a9efd0eb795d" +dependencies = [ + "downcast-rs", + "libc", + "memory_units", + "num-rational 0.2.4", + "num-traits", + "parity-wasm", + "wasmi-validation", +] + +[[package]] +name = "wasmi-validation" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165343ecd6c018fc09ebcae280752702c9a2ef3e6f8d02f1cfcbdb53ef6d7937" +dependencies = [ + "parity-wasm", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + +[[package]] +name = "zeroize" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc222aec311c323c717f56060324f32b82da1ce1dd81d9a09aa6a9030bfe08db" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e8f13fef10b63c06356d65d416b070798ddabcadc10d3ece0c5be9b3c7eddb" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..f5bc3bdd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[workspace] +members = [ + "frame/block-reward", + "frame/custom-signatures", + "frame/dapps-staking", + "precompiles/staking", +] + +exclude = [ + "vendor" +] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..91d264cd --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Astar Copyright (C) 2019 Stake Technologies Inc. + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 00000000..57c075f8 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +![astar-cover](https://user-images.githubusercontent.com/40356749/135799652-175e0d24-1255-4c26-87e8-447b192fd4b2.gif) + +
+ +[![Integration Action](https://github.com/PlasmNetwork/Astar/workflows/Integration/badge.svg)](https://github.com/AstarNetwork/astar-frame/actions) +[![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/PlasmNetwork/Astar)](https://github.com/AstarNetwork/astar-frame/tags) +[![Substrate version](https://img.shields.io/badge/Substrate-3.0.0-brightgreen?logo=Parity%20Substrate)](https://substrate.dev/) +[![License](https://img.shields.io/github/license/PlasmNetwork/Astar?color=green)](https://github.com/AstarNetwork/astar-frame/LICENSE) +
+[![Twitter URL](https://img.shields.io/twitter/follow/AstarNetwork?style=social)](https://twitter.com/AstarNetwork) +[![Twitter URL](https://img.shields.io/twitter/follow/ShidenNetwork?style=social)](https://twitter.com/ShidenNetwork) +[![YouTube](https://img.shields.io/youtube/channel/subscribers/UC36JgEF6gqatVSK9xlzzrvQ?style=social)](https://www.youtube.com/channel/UC36JgEF6gqatVSK9xlzzrvQ) +[![Docker](https://img.shields.io/docker/pulls/staketechnologies/astar-collator?logo=docker)](https://hub.docker.com/r/staketechnologies/astar-collator) +[![Discord](https://img.shields.io/badge/Discord-gray?logo=discord)](https://discord.gg/Z3nC9U4) +[![Telegram](https://img.shields.io/badge/Telegram-gray?logo=telegram)](https://t.me/PlasmOfficial) +[![Medium](https://img.shields.io/badge/Medium-gray?logo=medium)](https://medium.com/astar-network) + +
+ +Astar Network is an interoperable blockchain based the Substrate framework and the hub for dApps within the Polkadot Ecosystem. +With Astar Network and Shiden Network, people can stake their tokens to a Smart Contract for rewarding projects that provide value to the network. + +This repository only contains custom frame modules, for runtimes and node code please check [here](https://github.com/AstarNetwork/Astar/). + +For contributing to this project, please read our [Contribution Guideline](./CONTRIBUTING.md). + +## Versioning Schema +**TODO** + +## Further Reading + +* [Official Documentation](https://docs.astar.network/) +* [Whitepaper](https://github.com/PlasmNetwork/plasmdocs/blob/master/wp/en.pdf) +* [Whitepaper(JP)](https://github.com/PlasmNetwork/plasmdocs/blob/master/wp/jp.pdf) +* [Subtrate Developer Hub](https://substrate.dev/docs/en/) +* [Substrate Glossary](https://substrate.dev/docs/en/knowledgebase/getting-started/glossary) +* [Substrate Client Library Documentation](https://polkadot.js.org/docs/) diff --git a/frame/block-reward/Cargo.toml b/frame/block-reward/Cargo.toml new file mode 100644 index 00000000..27541eeb --- /dev/null +++ b/frame/block-reward/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "pallet-block-reward" +version = "0.2.0" +authors = ["Stake Technologies "] +edition = "2018" +license = "Apache-2.0" +homepage = "https://astar.network" +repository = "https://github.com/PlasmNetwork/Astar" +description = "FRAME pallet for manage block rewards" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "2.2.0", default-features = false, features = ["derive"] } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } + +[features] +default = ["std"] +std = [ + "codec/std", + "scale-info/std", + "frame-support/std", + "frame-system/std", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/block-reward/src/lib.rs b/frame/block-reward/src/lib.rs new file mode 100644 index 00000000..b37b775e --- /dev/null +++ b/frame/block-reward/src/lib.rs @@ -0,0 +1,66 @@ +//! # Block Reward Pallet +//! +//! - [`Config`] +//! +//! ## Overview +//! +//! Simple pallet that implements block reward mechanics. +//! +//! ## Interface +//! +//! This pallet implements the `OnTimestampSet` trait to handle block production. +//! Note: We assume that it's impossible to set timestamp two times in a block. +//! +//! ## Usage +//! +//! 1. Pallet should be set as a handler of `OnTimestampSet`. +//! 2. `OnBlockReward` handler should be defined as an impl of `OnUnbalanced` trait. For example: +//! ```nocompile +//! type NegativeImbalance = >::NegativeImbalance; +//! struct SaveOnTreasury; +//! impl OnUnbalanced for SaveOnTreasury { +//! fn on_nonzero_unbalanced(amount: NegativeImbalance) { +//! Balances::resolve_creating(&Treasury::pallet_id(), amount); +//! } +//! } +//! ``` +//! 3. Set `RewardAmount` to desiced block reward value in native currency. + +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use frame_support::pallet_prelude::*; + use frame_support::traits::{Currency, OnTimestampSet, OnUnbalanced}; + + /// The balance type of this pallet. + pub type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The currency trait. + type Currency: Currency; + + /// Handle block reward as imbalance. + type OnBlockReward: OnUnbalanced< + >::NegativeImbalance, + >; + + /// The amount of issuance for each block. + #[pallet::constant] + type RewardAmount: Get>; + } + + #[pallet::pallet] + pub struct Pallet(PhantomData); + + impl OnTimestampSet for Pallet { + fn on_timestamp_set(_moment: Moment) { + let inflation = T::Currency::issue(T::RewardAmount::get()); + T::OnBlockReward::on_unbalanced(inflation); + } + } +} diff --git a/frame/collator-selection/Cargo.toml b/frame/collator-selection/Cargo.toml new file mode 100644 index 00000000..790e80e0 --- /dev/null +++ b/frame/collator-selection/Cargo.toml @@ -0,0 +1,62 @@ +[package] +authors = ['Anonymous'] +description = 'Simple staking pallet with a fixed stake.' +edition = "2021" +homepage = 'https://substrate.io' +license = 'Apache-2.0' +name = 'pallet-collator-selection' +readme = 'README.md' +repository = 'https://github.com/paritytech/cumulus/' +version = '3.0.0' + +[package.metadata.docs.rs] +targets = ['x86_64-unknown-linux-gnu'] + +[dependencies] +log = { version = "0.4.0", default-features = false } +codec = { default-features = false, features = ['derive'], package = 'parity-scale-codec', version = '2.3.0' } +rand = { version = "0.7.2", default-features = false } +scale-info = { version = "1.0.0", default-features = false, features = ["derive"] } +serde = { version = "1.0.119", default-features = false } + +sp-std = { default-features = false, git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" } +sp-runtime = { default-features = false, git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" } +sp-staking = { default-features = false, git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" } +frame-support = { default-features = false, git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" } +frame-system = { default-features = false, git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" } +pallet-authorship = { default-features = false, git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" } +pallet-session = { default-features = false, git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" } + +frame-benchmarking = { default-features = false, optional = true, git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" } + +[dev-dependencies] +sp-core = { git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" } +sp-io = { git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" } +sp-tracing = { git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" } +sp-runtime = { git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" } +pallet-timestamp = { git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" } +sp-consensus-aura = { git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" } +pallet-balances = { git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" } +pallet-aura = { git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" } + +[features] +default = ['std'] +runtime-benchmarks = [ + 'frame-benchmarking', + 'frame-support/runtime-benchmarks', + 'frame-system/runtime-benchmarks', +] +std = [ + 'codec/std', + 'log/std', + 'scale-info/std', + 'rand/std', + 'sp-runtime/std', + 'sp-staking/std', + 'sp-std/std', + 'frame-support/std', + 'frame-system/std', + 'frame-benchmarking/std', + 'pallet-authorship/std', + 'pallet-session/std', +] diff --git a/frame/collator-selection/README.md b/frame/collator-selection/README.md new file mode 100644 index 00000000..9718db58 --- /dev/null +++ b/frame/collator-selection/README.md @@ -0,0 +1 @@ +License: Apache-2.0 \ No newline at end of file diff --git a/frame/collator-selection/src/benchmarking.rs b/frame/collator-selection/src/benchmarking.rs new file mode 100644 index 00000000..3e1063e3 --- /dev/null +++ b/frame/collator-selection/src/benchmarking.rs @@ -0,0 +1,265 @@ +// Copyright (C) 2021 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. + +//! Benchmarking setup for pallet-collator-selection + +use super::*; + +#[allow(unused)] +use crate::Pallet as CollatorSelection; +use frame_benchmarking::{account, benchmarks, impl_benchmark_test_suite, whitelisted_caller}; +use frame_support::{ + assert_ok, + codec::Decode, + traits::{Currency, EnsureOrigin, Get}, +}; +use frame_system::{EventRecord, RawOrigin}; +use pallet_authorship::EventHandler; +use pallet_session::{self as session, SessionManager}; +use sp_std::prelude::*; + +pub type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + +const SEED: u32 = 0; + +// TODO: remove if this is given in substrate commit. +macro_rules! whitelist { + ($acc:ident) => { + frame_benchmarking::benchmarking::add_to_whitelist( + frame_system::Account::::hashed_key_for(&$acc).into(), + ); + }; +} + +fn assert_last_event(generic_event: ::Event) { + let events = frame_system::Pallet::::events(); + let system_event: ::Event = generic_event.into(); + // compare to the last event record + let EventRecord { event, .. } = &events[events.len() - 1]; + assert_eq!(event, &system_event); +} + +fn create_funded_user( + string: &'static str, + n: u32, + balance_factor: u32, +) -> T::AccountId { + let user = account(string, n, SEED); + let balance = T::Currency::minimum_balance() * balance_factor.into(); + let _ = T::Currency::make_free_balance_be(&user, balance); + user +} + +fn keys(c: u32) -> ::Keys { + use rand::{RngCore, SeedableRng}; + + let keys = { + let mut keys = [0u8; 128]; + + if c > 0 { + let mut rng = rand::rngs::StdRng::seed_from_u64(c as u64); + rng.fill_bytes(&mut keys); + } + + keys + }; + + Decode::decode(&mut &keys[..]).unwrap() +} + +fn validator(c: u32) -> (T::AccountId, ::Keys) { + (create_funded_user::("candidate", c, 1000), keys::(c)) +} + +fn register_validators(count: u32) { + let validators = (0..count).map(|c| validator::(c)).collect::>(); + + for (who, keys) in validators { + >::set_keys(RawOrigin::Signed(who).into(), keys, Vec::new()).unwrap(); + } +} + +fn register_candidates(count: u32) { + let candidates = (0..count) + .map(|c| account("candidate", c, SEED)) + .collect::>(); + assert!( + >::get() > 0u32.into(), + "Bond cannot be zero!" + ); + + for who in candidates { + T::Currency::make_free_balance_be(&who, >::get() * 2u32.into()); + >::register_as_candidate(RawOrigin::Signed(who).into()).unwrap(); + } +} + +benchmarks! { + where_clause { where T: pallet_authorship::Config + session::Config } + + set_invulnerables { + let b in 1 .. T::MaxInvulnerables::get(); + let new_invulnerables = (0..b).map(|c| account("candidate", c, SEED)).collect::>(); + let origin = T::UpdateOrigin::successful_origin(); + }: { + assert_ok!( + >::set_invulnerables(origin, new_invulnerables.clone()) + ); + } + verify { + assert_last_event::(Event::NewInvulnerables(new_invulnerables).into()); + } + + set_desired_candidates { + let max: u32 = 999; + let origin = T::UpdateOrigin::successful_origin(); + }: { + assert_ok!( + >::set_desired_candidates(origin, max.clone()) + ); + } + verify { + assert_last_event::(Event::NewDesiredCandidates(max).into()); + } + + set_candidacy_bond { + let bond: BalanceOf = T::Currency::minimum_balance() * 10u32.into(); + let origin = T::UpdateOrigin::successful_origin(); + }: { + assert_ok!( + >::set_candidacy_bond(origin, bond.clone()) + ); + } + verify { + assert_last_event::(Event::NewCandidacyBond(bond).into()); + } + + // worse case is when we have all the max-candidate slots filled except one, and we fill that + // one. + register_as_candidate { + let c in 1 .. T::MaxCandidates::get(); + + >::put(T::Currency::minimum_balance()); + >::put(c + 1); + + register_validators::(c); + register_candidates::(c); + + let caller: T::AccountId = whitelisted_caller(); + let bond: BalanceOf = T::Currency::minimum_balance() * 2u32.into(); + T::Currency::make_free_balance_be(&caller, bond.clone()); + + >::set_keys( + RawOrigin::Signed(caller.clone()).into(), + keys::(c + 1), + Vec::new() + ).unwrap(); + + }: _(RawOrigin::Signed(caller.clone())) + verify { + assert_last_event::(Event::CandidateAdded(caller, bond / 2u32.into()).into()); + } + + // worse case is the last candidate leaving. + leave_intent { + let c in (T::MinCandidates::get() + 1) .. T::MaxCandidates::get(); + >::put(T::Currency::minimum_balance()); + >::put(c); + + register_validators::(c); + register_candidates::(c); + + let leaving = >::get().last().unwrap().who.clone(); + whitelist!(leaving); + }: _(RawOrigin::Signed(leaving.clone())) + verify { + assert_last_event::(Event::CandidateRemoved(leaving).into()); + } + + // worse case is paying a non-existing candidate account. + note_author { + >::put(T::Currency::minimum_balance()); + T::Currency::make_free_balance_be( + &>::account_id(), + T::Currency::minimum_balance() * 4u32.into(), + ); + let author = account("author", 0, SEED); + let new_block: T::BlockNumber = 10u32.into(); + + frame_system::Pallet::::set_block_number(new_block); + assert!(T::Currency::free_balance(&author) == 0u32.into()); + }: { + as EventHandler<_, _>>::note_author(author.clone()) + } verify { + assert!(T::Currency::free_balance(&author) > 0u32.into()); + assert_eq!(frame_system::Pallet::::block_number(), new_block); + } + + // worst case for new session. + new_session { + let r in 1 .. T::MaxCandidates::get(); + let c in 1 .. T::MaxCandidates::get(); + + >::put(T::Currency::minimum_balance()); + >::put(c); + frame_system::Pallet::::set_block_number(0u32.into()); + + register_validators::(c); + register_candidates::(c); + + let new_block: T::BlockNumber = 1800u32.into(); + let zero_block: T::BlockNumber = 0u32.into(); + let candidates = >::get(); + + let non_removals = c.saturating_sub(r); + + for i in 0..c { + >::insert(candidates[i as usize].who.clone(), zero_block); + } + + if non_removals > 0 { + for i in 0..non_removals { + >::insert(candidates[i as usize].who.clone(), new_block); + } + } else { + for i in 0..c { + >::insert(candidates[i as usize].who.clone(), new_block); + } + } + + let pre_length = >::get().len(); + + frame_system::Pallet::::set_block_number(new_block); + + assert!(>::get().len() == c as usize); + }: { + as SessionManager<_>>::new_session(0) + } verify { + if c > r && non_removals >= T::MinCandidates::get() { + assert!(>::get().len() < pre_length); + } else if c > r && non_removals < T::MinCandidates::get() { + assert!(>::get().len() == T::MinCandidates::get() as usize); + } else { + assert!(>::get().len() == pre_length); + } + } +} + +impl_benchmark_test_suite!( + CollatorSelection, + crate::mock::new_test_ext(), + crate::mock::Test, +); diff --git a/frame/collator-selection/src/lib.rs b/frame/collator-selection/src/lib.rs new file mode 100644 index 00000000..4dfbe6ce --- /dev/null +++ b/frame/collator-selection/src/lib.rs @@ -0,0 +1,518 @@ +// Copyright (C) 2021 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. + +//! Collator Selection pallet. +//! +//! A pallet to manage collators in a parachain. +//! +//! ## Overview +//! +//! The Collator Selection pallet manages the collators of a parachain. **Collation is _not_ a +//! secure activity** and this pallet does not implement any game-theoretic mechanisms to meet BFT +//! safety assumptions of the chosen set. +//! +//! ## Terminology +//! +//! - Collator: A parachain block producer. +//! - Bond: An amount of `Balance` _locked_ for candidate registration. +//! - Invulnerable: An account guaranteed to be in the collator set. +//! +//! ## Implementation +//! +//! The final [`Collators`] are aggregated from two individual lists: +//! +//! 1. [`Invulnerables`]: a set of collators appointed by governance. These accounts will always be +//! collators. +//! 2. [`Candidates`]: these are *candidates to the collation task* and may or may not be elected as +//! a final collator. +//! +//! The current implementation resolves congestion of [`Candidates`] in a first-come-first-serve +//! manner. +//! +//! Candidates will not be allowed to get kicked or leave_intent if the total number of candidates +//! fall below MinCandidates. This is for potential disaster recovery scenarios. +//! +//! ### Rewards +//! +//! The Collator Selection pallet maintains an on-chain account (the "Pot"). In each block, the +//! collator who authored it receives: +//! +//! - Half the value of the Pot. +//! - Half the value of the transaction fees within the block. The other half of the transaction +//! fees are deposited into the Pot. +//! +//! To initiate rewards an ED needs to be transferred to the pot address. +//! +//! Note: Eventually the Pot distribution may be modified as discussed in +//! [this issue](https://github.com/paritytech/statemint/issues/21#issuecomment-810481073). + +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; +pub mod weights; + +#[frame_support::pallet] +pub mod pallet { + pub use crate::weights::WeightInfo; + use core::ops::Div; + use frame_support::{ + dispatch::DispatchResultWithPostInfo, + inherent::Vec, + pallet_prelude::*, + sp_runtime::{ + traits::{AccountIdConversion, CheckedSub, Saturating, Zero}, + RuntimeDebug, + }, + traits::{ + Currency, EnsureOrigin, ExistenceRequirement::KeepAlive, LockIdentifier, + LockableCurrency, ValidatorRegistration, WithdrawReasons, + }, + weights::DispatchClass, + PalletId, + }; + use frame_system::{pallet_prelude::*, Config as SystemConfig}; + use pallet_session::SessionManager; + use sp_runtime::traits::Convert; + use sp_staking::SessionIndex; + + type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + + const COLLATOR_STAKING_ID: LockIdentifier = *b"colstake"; + + /// A convertor from collators id. Since this pallet does not have stash/controller, this is + /// just identity. + pub struct IdentityCollator; + impl sp_runtime::traits::Convert> for IdentityCollator { + fn convert(t: T) -> Option { + Some(t) + } + } + + /// Configure the pallet by specifying the parameters and types on which it depends. + #[pallet::config] + pub trait Config: frame_system::Config { + /// Overarching event type. + type Event: From> + IsType<::Event>; + + /// The currency mechanism. + type Currency: LockableCurrency; + + /// Origin that can dictate updating parameters of this pallet. + type UpdateOrigin: EnsureOrigin; + + /// Account Identifier from which the internal Pot is generated. + type PotId: Get; + + /// Maximum number of candidates that we should have. This is used for benchmarking and is not + /// enforced. + /// + /// This does not take into account the invulnerables. + type MaxCandidates: Get; + + /// Minimum number of candidates that we should have. This is used for disaster recovery. + /// + /// This does not take into account the invulnerables. + type MinCandidates: Get; + + /// Maximum number of invulnerables. + /// + /// Used only for benchmarking. + type MaxInvulnerables: Get; + + // Will be kicked if block is not produced in threshold. + type KickThreshold: Get; + + /// A stable ID for a validator. + type ValidatorId: Member + Parameter; + + /// A conversion from account ID to validator ID. + /// + /// Its cost must be at most one storage read. + type ValidatorIdOf: Convert>; + + /// Validate a user is registered + type ValidatorRegistration: ValidatorRegistration; + + /// The weight information of this pallet. + type WeightInfo: WeightInfo; + } + + /// Basic information about a collation candidate. + #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, scale_info::TypeInfo)] + pub struct CandidateInfo { + /// Account identifier. + pub who: AccountId, + /// Reserved deposit. + pub deposit: Balance, + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + /// The invulnerable, fixed collators. + #[pallet::storage] + #[pallet::getter(fn invulnerables)] + pub type Invulnerables = StorageValue<_, Vec, ValueQuery>; + + /// The (community, limited) collation candidates. + #[pallet::storage] + #[pallet::getter(fn candidates)] + pub type Candidates = + StorageValue<_, Vec>>, ValueQuery>; + + /// Last block authored by collator. + #[pallet::storage] + #[pallet::getter(fn last_authored_block)] + pub type LastAuthoredBlock = + StorageMap<_, Twox64Concat, T::AccountId, T::BlockNumber, ValueQuery>; + + /// Desired number of candidates. + /// + /// This should ideally always be less than [`Config::MaxCandidates`] for weights to be correct. + #[pallet::storage] + #[pallet::getter(fn desired_candidates)] + pub type DesiredCandidates = StorageValue<_, u32, ValueQuery>; + + /// Fixed deposit bond for each candidate. + #[pallet::storage] + #[pallet::getter(fn candidacy_bond)] + pub type CandidacyBond = StorageValue<_, BalanceOf, ValueQuery>; + + #[pallet::genesis_config] + pub struct GenesisConfig { + pub invulnerables: Vec, + pub candidacy_bond: BalanceOf, + pub desired_candidates: u32, + } + + #[cfg(feature = "std")] + impl Default for GenesisConfig { + fn default() -> Self { + Self { + invulnerables: Default::default(), + candidacy_bond: Default::default(), + desired_candidates: Default::default(), + } + } + } + + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) { + let duplicate_invulnerables = self + .invulnerables + .iter() + .collect::>(); + assert!( + duplicate_invulnerables.len() == self.invulnerables.len(), + "duplicate invulnerables in genesis." + ); + + assert!( + T::MaxInvulnerables::get() >= (self.invulnerables.len() as u32), + "genesis invulnerables are more than T::MaxInvulnerables", + ); + assert!( + T::MaxCandidates::get() >= self.desired_candidates, + "genesis desired_candidates are more than T::MaxCandidates", + ); + + >::put(&self.desired_candidates); + >::put(&self.candidacy_bond); + >::put(&self.invulnerables); + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + NewInvulnerables(Vec), + NewDesiredCandidates(u32), + NewCandidacyBond(BalanceOf), + CandidateAdded(T::AccountId, BalanceOf), + CandidateRemoved(T::AccountId), + } + + // Errors inform users that something went wrong. + #[pallet::error] + pub enum Error { + /// Too many candidates + TooManyCandidates, + /// Too few candidates + TooFewCandidates, + /// Unknown error + Unknown, + /// Permission issue + Permission, + /// User is already a candidate + AlreadyCandidate, + /// User is not a candidate + NotCandidate, + /// User is already an Invulnerable + AlreadyInvulnerable, + /// Account has no associated validator ID + NoAssociatedValidatorId, + /// Validator ID is not yet registered + ValidatorNotRegistered, + /// Free balance is too low for onboarding + TooLowFreeBalance, + } + + #[pallet::hooks] + impl Hooks> for Pallet {} + + #[pallet::call] + impl Pallet { + #[pallet::weight(T::WeightInfo::set_invulnerables(new.len() as u32))] + pub fn set_invulnerables( + origin: OriginFor, + new: Vec, + ) -> DispatchResultWithPostInfo { + T::UpdateOrigin::ensure_origin(origin)?; + // we trust origin calls, this is just a for more accurate benchmarking + if (new.len() as u32) > T::MaxInvulnerables::get() { + log::warn!( + "invulnerables > T::MaxInvulnerables; you might need to run benchmarks again" + ); + } + >::put(&new); + Self::deposit_event(Event::NewInvulnerables(new)); + Ok(().into()) + } + + #[pallet::weight(T::WeightInfo::set_desired_candidates())] + pub fn set_desired_candidates( + origin: OriginFor, + max: u32, + ) -> DispatchResultWithPostInfo { + T::UpdateOrigin::ensure_origin(origin)?; + // we trust origin calls, this is just a for more accurate benchmarking + if max > T::MaxCandidates::get() { + log::warn!("max > T::MaxCandidates; you might need to run benchmarks again"); + } + >::put(&max); + Self::deposit_event(Event::NewDesiredCandidates(max)); + Ok(().into()) + } + + #[pallet::weight(T::WeightInfo::set_candidacy_bond())] + pub fn set_candidacy_bond( + origin: OriginFor, + bond: BalanceOf, + ) -> DispatchResultWithPostInfo { + T::UpdateOrigin::ensure_origin(origin)?; + >::put(&bond); + Self::deposit_event(Event::NewCandidacyBond(bond)); + Ok(().into()) + } + + #[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCandidates::get()))] + pub fn register_as_candidate(origin: OriginFor) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + + // ensure we are below limit. + let length = >::decode_len().unwrap_or_default(); + ensure!( + (length as u32) < Self::desired_candidates(), + Error::::TooManyCandidates + ); + ensure!( + !Self::invulnerables().contains(&who), + Error::::AlreadyInvulnerable + ); + + let validator_key = T::ValidatorIdOf::convert(who.clone()) + .ok_or(Error::::NoAssociatedValidatorId)?; + ensure!( + T::ValidatorRegistration::is_registered(&validator_key), + Error::::ValidatorNotRegistered + ); + + let deposit = Self::candidacy_bond(); + + let free_balance = T::Currency::free_balance(&who); + ensure!(free_balance > deposit, Error::::TooLowFreeBalance); + + // First authored block is current block plus kick threshold to handle session delay + let incoming = CandidateInfo { + who: who.clone(), + deposit, + }; + + let current_count = + >::try_mutate(|candidates| -> Result { + if candidates.into_iter().any(|candidate| candidate.who == who) { + Err(Error::::AlreadyCandidate)? + } else { + T::Currency::set_lock( + COLLATOR_STAKING_ID, + &who, + deposit, + WithdrawReasons::all(), + ); + candidates.push(incoming); + >::insert( + who.clone(), + frame_system::Pallet::::block_number() + T::KickThreshold::get(), + ); + Ok(candidates.len()) + } + })?; + + Self::deposit_event(Event::CandidateAdded(who, deposit)); + Ok(Some(T::WeightInfo::register_as_candidate(current_count as u32)).into()) + } + + #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))] + pub fn leave_intent(origin: OriginFor) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + ensure!( + Self::candidates().len() as u32 > T::MinCandidates::get(), + Error::::TooFewCandidates + ); + let current_count = Self::try_remove_candidate(&who)?; + + Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into()) + } + } + + impl Pallet { + /// Get a unique, inaccessible account id from the `PotId`. + pub fn account_id() -> T::AccountId { + T::PotId::get().into_account() + } + /// Removes a candidate if they exist and sends them back their deposit + fn try_remove_candidate(who: &T::AccountId) -> Result { + let current_count = + >::try_mutate(|candidates| -> Result { + let index = candidates + .iter() + .position(|candidate| candidate.who == *who) + .ok_or(Error::::NotCandidate)?; + T::Currency::remove_lock(COLLATOR_STAKING_ID, &who); + candidates.remove(index); + >::remove(who.clone()); + Ok(candidates.len()) + })?; + Self::deposit_event(Event::CandidateRemoved(who.clone())); + Ok(current_count) + } + + /// Assemble the current set of candidates and invulnerables into the next collator set. + /// + /// This is done on the fly, as frequent as we are told to do so, as the session manager. + pub fn assemble_collators(candidates: Vec) -> Vec { + let mut collators = Self::invulnerables(); + collators.extend(candidates.into_iter().collect::>()); + collators + } + /// Kicks out and candidates that did not produce a block in the kick threshold. + pub fn kick_stale_candidates( + candidates: Vec>>, + ) -> Vec { + let now = frame_system::Pallet::::block_number(); + let kick_threshold = T::KickThreshold::get(); + let new_candidates = candidates + .into_iter() + .filter_map(|c| { + let last_block = >::get(c.who.clone()); + let since_last = now.saturating_sub(last_block); + if since_last < kick_threshold + || Self::candidates().len() as u32 <= T::MinCandidates::get() + { + Some(c.who) + } else { + let outcome = Self::try_remove_candidate(&c.who); + if let Err(why) = outcome { + log::warn!("Failed to remove candidate {:?}", why); + debug_assert!(false, "failed to remove candidate {:?}", why); + } + None + } + }) + .collect::>(); + new_candidates + } + } + + /// Keep track of number of authored blocks per authority, uncles are counted as well since + /// they're a valid proof of being online. + impl + pallet_authorship::EventHandler for Pallet + { + fn note_author(author: T::AccountId) { + let pot = Self::account_id(); + // assumes an ED will be sent to pot. + let reward = T::Currency::free_balance(&pot) + .checked_sub(&T::Currency::minimum_balance()) + .unwrap_or_else(Zero::zero) + .div(2u32.into()); + // `reward` is half of pot account minus ED, this should never fail. + let _success = T::Currency::transfer(&pot, &author, reward, KeepAlive); + debug_assert!(_success.is_ok()); + >::insert(author, frame_system::Pallet::::block_number()); + + frame_system::Pallet::::register_extra_weight_unchecked( + T::WeightInfo::note_author(), + DispatchClass::Mandatory, + ); + } + + fn note_uncle(_author: T::AccountId, _age: T::BlockNumber) { + //TODO can we ignore this? + } + } + + /// Play the role of the session manager. + impl SessionManager for Pallet { + fn new_session(index: SessionIndex) -> Option> { + log::info!( + "assembling new collators for new session {} at #{:?}", + index, + >::block_number(), + ); + + let candidates = Self::candidates(); + let candidates_len_before = candidates.len(); + let active_candidates = Self::kick_stale_candidates(candidates); + let active_candidates_len = active_candidates.len(); + let result = Self::assemble_collators(active_candidates); + let removed = candidates_len_before - active_candidates_len; + + frame_system::Pallet::::register_extra_weight_unchecked( + T::WeightInfo::new_session(candidates_len_before as u32, removed as u32), + DispatchClass::Mandatory, + ); + Some(result) + } + fn start_session(_: SessionIndex) { + // we don't care. + } + fn end_session(_: SessionIndex) { + // we don't care. + } + } +} diff --git a/frame/collator-selection/src/mock.rs b/frame/collator-selection/src/mock.rs new file mode 100644 index 00000000..c856817b --- /dev/null +++ b/frame/collator-selection/src/mock.rs @@ -0,0 +1,264 @@ +// Copyright (C) 2021 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 super::*; +use crate as collator_selection; +use frame_support::{ + ord_parameter_types, parameter_types, + traits::{FindAuthor, GenesisBuild, ValidatorRegistration}, + PalletId, +}; +use frame_system as system; +use frame_system::EnsureSignedBy; +use sp_core::H256; +use sp_runtime::{ + testing::{Header, UintAuthorityId}, + traits::{BlakeTwo256, IdentityLookup, OpaqueKeys}, + RuntimeAppPublic, +}; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent}, + Session: pallet_session::{Pallet, Call, Storage, Event, Config}, + Aura: pallet_aura::{Pallet, Storage, Config}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + CollatorSelection: collator_selection::{Pallet, Call, Storage, Event}, + Authorship: pallet_authorship::{Pallet, Call, Storage, Inherent}, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} + +impl system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); +} + +parameter_types! { + pub const ExistentialDeposit: u64 = 5; + pub const MaxReserves: u32 = 50; +} + +impl pallet_balances::Config for Test { + type Balance = u64; + type Event = Event; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = (); + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 8]; +} + +pub struct Author4; +impl FindAuthor for Author4 { + fn find_author<'a, I>(_digests: I) -> Option + where + I: 'a + IntoIterator, + { + Some(4) + } +} + +impl pallet_authorship::Config for Test { + type FindAuthor = Author4; + type UncleGenerations = (); + type FilterUncle = (); + type EventHandler = CollatorSelection; +} + +parameter_types! { + pub const MinimumPeriod: u64 = 1; +} + +impl pallet_timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = Aura; + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +impl pallet_aura::Config for Test { + type AuthorityId = sp_consensus_aura::sr25519::AuthorityId; + type MaxAuthorities = MaxAuthorities; + type DisabledValidators = (); +} + +sp_runtime::impl_opaque_keys! { + pub struct MockSessionKeys { + // a key for aura authoring + pub aura: UintAuthorityId, + } +} + +impl From for MockSessionKeys { + fn from(aura: sp_runtime::testing::UintAuthorityId) -> Self { + Self { aura } + } +} + +parameter_types! { + pub static SessionHandlerCollators: Vec = Vec::new(); + pub static SessionChangeBlock: u64 = 0; +} + +pub struct TestSessionHandler; +impl pallet_session::SessionHandler for TestSessionHandler { + const KEY_TYPE_IDS: &'static [sp_runtime::KeyTypeId] = &[UintAuthorityId::ID]; + fn on_genesis_session(keys: &[(u64, Ks)]) { + SessionHandlerCollators::set(keys.into_iter().map(|(a, _)| *a).collect::>()) + } + fn on_new_session(_: bool, keys: &[(u64, Ks)], _: &[(u64, Ks)]) { + SessionChangeBlock::set(System::block_number()); + dbg!(keys.len()); + SessionHandlerCollators::set(keys.into_iter().map(|(a, _)| *a).collect::>()) + } + fn on_before_session_ending() {} + fn on_disabled(_: u32) {} +} + +parameter_types! { + pub const Offset: u64 = 0; + pub const Period: u64 = 10; +} + +impl pallet_session::Config for Test { + type Event = Event; + type ValidatorId = ::AccountId; + // we don't have stash and controller, thus we don't need the convert as well. + type ValidatorIdOf = IdentityCollator; + type ShouldEndSession = pallet_session::PeriodicSessions; + type NextSessionRotation = pallet_session::PeriodicSessions; + type SessionManager = CollatorSelection; + type SessionHandler = TestSessionHandler; + type Keys = MockSessionKeys; + type WeightInfo = (); +} + +ord_parameter_types! { + pub const RootAccount: u64 = 777; +} + +parameter_types! { + pub const PotId: PalletId = PalletId(*b"PotStake"); + pub const MaxCandidates: u32 = 20; + pub const MaxInvulnerables: u32 = 20; + pub const MinCandidates: u32 = 1; + pub const MaxAuthorities: u32 = 100_000; +} + +pub struct IsRegistered; +impl ValidatorRegistration for IsRegistered { + fn is_registered(id: &u64) -> bool { + if *id == 7u64 { + false + } else { + true + } + } +} + +impl Config for Test { + type Event = Event; + type Currency = Balances; + type UpdateOrigin = EnsureSignedBy; + type PotId = PotId; + type MaxCandidates = MaxCandidates; + type MinCandidates = MinCandidates; + type MaxInvulnerables = MaxInvulnerables; + type KickThreshold = Period; + type ValidatorId = ::AccountId; + type ValidatorIdOf = IdentityCollator; + type ValidatorRegistration = IsRegistered; + type WeightInfo = (); +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + sp_tracing::try_init_simple(); + let mut t = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + let invulnerables = vec![1, 2]; + let keys = invulnerables + .iter() + .map(|i| { + ( + *i, + *i, + MockSessionKeys { + aura: UintAuthorityId(*i), + }, + ) + }) + .collect::>(); + + let balances = pallet_balances::GenesisConfig:: { + balances: vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100)], + }; + let collator_selection = collator_selection::GenesisConfig:: { + desired_candidates: 2, + candidacy_bond: 10, + invulnerables, + }; + let session = pallet_session::GenesisConfig:: { keys }; + balances.assimilate_storage(&mut t).unwrap(); + // collator selection must be initialized before session. + collator_selection.assimilate_storage(&mut t).unwrap(); + session.assimilate_storage(&mut t).unwrap(); + + t.into() +} + +pub fn initialize_to_block(n: u64) { + for i in System::block_number() + 1..=n { + System::set_block_number(i); + >::on_initialize(i); + } +} diff --git a/frame/collator-selection/src/tests.rs b/frame/collator-selection/src/tests.rs new file mode 100644 index 00000000..bbad7e20 --- /dev/null +++ b/frame/collator-selection/src/tests.rs @@ -0,0 +1,409 @@ +// Copyright (C) 2021 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 crate as collator_selection; +use crate::{mock::*, CandidateInfo, Error}; +use frame_support::{ + assert_noop, assert_ok, + traits::{Currency, GenesisBuild, OnInitialize}, +}; +use sp_runtime::traits::BadOrigin; + +#[test] +fn basic_setup_works() { + new_test_ext().execute_with(|| { + assert_eq!(CollatorSelection::desired_candidates(), 2); + assert_eq!(CollatorSelection::candidacy_bond(), 10); + + assert!(CollatorSelection::candidates().is_empty()); + assert_eq!(CollatorSelection::invulnerables(), vec![1, 2]); + }); +} + +#[test] +fn it_should_set_invulnerables() { + new_test_ext().execute_with(|| { + let new_set = vec![1, 2, 3, 4]; + assert_ok!(CollatorSelection::set_invulnerables( + Origin::signed(RootAccount::get()), + new_set.clone() + )); + assert_eq!(CollatorSelection::invulnerables(), new_set); + + // cannot set with non-root. + assert_noop!( + CollatorSelection::set_invulnerables(Origin::signed(1), new_set.clone()), + BadOrigin + ); + }); +} + +#[test] +fn set_desired_candidates_works() { + new_test_ext().execute_with(|| { + // given + assert_eq!(CollatorSelection::desired_candidates(), 2); + + // can set + assert_ok!(CollatorSelection::set_desired_candidates( + Origin::signed(RootAccount::get()), + 7 + )); + assert_eq!(CollatorSelection::desired_candidates(), 7); + + // rejects bad origin + assert_noop!( + CollatorSelection::set_desired_candidates(Origin::signed(1), 8), + BadOrigin + ); + }); +} + +#[test] +fn set_candidacy_bond() { + new_test_ext().execute_with(|| { + // given + assert_eq!(CollatorSelection::candidacy_bond(), 10); + + // can set + assert_ok!(CollatorSelection::set_candidacy_bond( + Origin::signed(RootAccount::get()), + 7 + )); + assert_eq!(CollatorSelection::candidacy_bond(), 7); + + // rejects bad origin. + assert_noop!( + CollatorSelection::set_candidacy_bond(Origin::signed(1), 8), + BadOrigin + ); + }); +} + +#[test] +fn cannot_register_candidate_if_too_many() { + new_test_ext().execute_with(|| { + // reset desired candidates: + >::put(0); + + // can't accept anyone anymore. + assert_noop!( + CollatorSelection::register_as_candidate(Origin::signed(3)), + Error::::TooManyCandidates, + ); + + // reset desired candidates: + >::put(1); + assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(4))); + + // but no more + assert_noop!( + CollatorSelection::register_as_candidate(Origin::signed(5)), + Error::::TooManyCandidates, + ); + }) +} + +#[test] +fn cannot_unregister_candidate_if_too_few() { + new_test_ext().execute_with(|| { + // reset desired candidates: + >::put(1); + assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(4))); + + // can not remove too few + assert_noop!( + CollatorSelection::leave_intent(Origin::signed(4)), + Error::::TooFewCandidates, + ); + }) +} + +#[test] +fn cannot_register_as_candidate_if_invulnerable() { + new_test_ext().execute_with(|| { + assert_eq!(CollatorSelection::invulnerables(), vec![1, 2]); + + // can't 1 because it is invulnerable. + assert_noop!( + CollatorSelection::register_as_candidate(Origin::signed(1)), + Error::::AlreadyInvulnerable, + ); + }) +} + +#[test] +fn cannot_register_as_candidate_if_keys_not_registered() { + new_test_ext().execute_with(|| { + // can't 7 because keys not registered. + assert_noop!( + CollatorSelection::register_as_candidate(Origin::signed(7)), + Error::::ValidatorNotRegistered + ); + }) +} + +#[test] +fn cannot_register_dupe_candidate() { + new_test_ext().execute_with(|| { + // can add 3 as candidate + assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(3))); + let addition = CandidateInfo { + who: 3, + deposit: 10, + }; + assert_eq!(CollatorSelection::candidates(), vec![addition]); + assert_eq!(CollatorSelection::last_authored_block(3), 10); + assert_eq!(Balances::usable_balance(3), 90); + + // but no more + assert_noop!( + CollatorSelection::register_as_candidate(Origin::signed(3)), + Error::::AlreadyCandidate, + ); + }) +} + +#[test] +fn cannot_register_as_candidate_if_poor() { + new_test_ext().execute_with(|| { + assert_eq!(Balances::usable_balance(&3), 100); + assert_eq!(Balances::usable_balance(&33), 0); + + // works + assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(3))); + + // poor + assert_noop!( + CollatorSelection::register_as_candidate(Origin::signed(33)), + Error::::TooLowFreeBalance, + ); + }); +} + +#[test] +fn register_as_candidate_works() { + new_test_ext().execute_with(|| { + // given + assert_eq!(CollatorSelection::desired_candidates(), 2); + assert_eq!(CollatorSelection::candidacy_bond(), 10); + assert_eq!(CollatorSelection::candidates(), Vec::new()); + assert_eq!(CollatorSelection::invulnerables(), vec![1, 2]); + + // take two endowed, non-invulnerables accounts. + assert_eq!(Balances::usable_balance(&3), 100); + assert_eq!(Balances::usable_balance(&4), 100); + + assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(3))); + assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(4))); + + assert_eq!(Balances::usable_balance(&3), 90); + assert_eq!(Balances::usable_balance(&4), 90); + + assert_eq!(CollatorSelection::candidates().len(), 2); + }); +} + +#[test] +fn leave_intent() { + new_test_ext().execute_with(|| { + // register a candidate. + assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(3))); + assert_eq!(Balances::usable_balance(3), 90); + + // register too so can leave above min candidates + assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(5))); + assert_eq!(Balances::usable_balance(5), 90); + + // cannot leave if not candidate. + assert_noop!( + CollatorSelection::leave_intent(Origin::signed(4)), + Error::::NotCandidate + ); + + // bond is returned + assert_ok!(CollatorSelection::leave_intent(Origin::signed(3))); + assert_eq!(Balances::usable_balance(3), 100); + assert_eq!(CollatorSelection::last_authored_block(3), 0); + }); +} + +#[test] +fn authorship_event_handler() { + new_test_ext().execute_with(|| { + // put 100 in the pot + 5 for ED + Balances::make_free_balance_be(&CollatorSelection::account_id(), 105); + + // 4 is the default author. + assert_eq!(Balances::usable_balance(4), 100); + assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(4))); + // triggers `note_author` + Authorship::on_initialize(1); + + let collator = CandidateInfo { + who: 4, + deposit: 10, + }; + + assert_eq!(CollatorSelection::candidates(), vec![collator]); + assert_eq!(CollatorSelection::last_authored_block(4), 0); + + // half of the pot goes to the collator who's the author (4 in tests). + assert_eq!(Balances::usable_balance(4), 140); + // half + ED stays. + assert_eq!( + Balances::usable_balance(CollatorSelection::account_id()), + 55 + ); + }); +} + +#[test] +fn fees_edgecases() { + new_test_ext().execute_with(|| { + // Nothing panics, no reward when no ED in balance + Authorship::on_initialize(1); + // put some money into the pot at ED + Balances::make_free_balance_be(&CollatorSelection::account_id(), 5); + // 4 is the default author. + assert_eq!(Balances::usable_balance(4), 100); + assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(4))); + // triggers `note_author` + Authorship::on_initialize(1); + + let collator = CandidateInfo { + who: 4, + deposit: 10, + }; + + assert_eq!(CollatorSelection::candidates(), vec![collator]); + assert_eq!(CollatorSelection::last_authored_block(4), 0); + // Nothing received + assert_eq!(Balances::usable_balance(4), 90); + // all fee stays + assert_eq!(Balances::usable_balance(CollatorSelection::account_id()), 5); + }); +} + +#[test] +fn session_management_works() { + new_test_ext().execute_with(|| { + initialize_to_block(1); + + assert_eq!(SessionChangeBlock::get(), 0); + assert_eq!(SessionHandlerCollators::get(), vec![1, 2]); + + initialize_to_block(4); + + assert_eq!(SessionChangeBlock::get(), 0); + assert_eq!(SessionHandlerCollators::get(), vec![1, 2]); + + // add a new collator + assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(3))); + + // session won't see this. + assert_eq!(SessionHandlerCollators::get(), vec![1, 2]); + // but we have a new candidate. + assert_eq!(CollatorSelection::candidates().len(), 1); + + initialize_to_block(10); + assert_eq!(SessionChangeBlock::get(), 10); + // pallet-session has 1 session delay; current validators are the same. + assert_eq!(Session::validators(), vec![1, 2]); + // queued ones are changed, and now we have 3. + assert_eq!(Session::queued_keys().len(), 3); + // session handlers (aura, et. al.) cannot see this yet. + assert_eq!(SessionHandlerCollators::get(), vec![1, 2]); + + initialize_to_block(20); + assert_eq!(SessionChangeBlock::get(), 20); + // changed are now reflected to session handlers. + assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 3]); + }); +} + +#[test] +fn kick_mechanism() { + new_test_ext().execute_with(|| { + // add a new collator + assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(3))); + assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(4))); + initialize_to_block(10); + assert_eq!(CollatorSelection::candidates().len(), 2); + initialize_to_block(20); + assert_eq!(SessionChangeBlock::get(), 20); + // 4 authored this block, gets to stay 3 was kicked + assert_eq!(CollatorSelection::candidates().len(), 1); + // 3 will be kicked after 1 session delay + assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 3, 4]); + let collator = CandidateInfo { + who: 4, + deposit: 10, + }; + assert_eq!(CollatorSelection::candidates(), vec![collator]); + assert_eq!(CollatorSelection::last_authored_block(4), 20); + initialize_to_block(30); + // 3 gets kicked after 1 session delay + assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 4]); + // kicked collator gets funds back + assert_eq!(Balances::usable_balance(3), 100); + }); +} + +#[test] +fn should_not_kick_mechanism_too_few() { + new_test_ext().execute_with(|| { + // add a new collator + assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(3))); + assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(5))); + initialize_to_block(10); + assert_eq!(CollatorSelection::candidates().len(), 2); + initialize_to_block(20); + assert_eq!(SessionChangeBlock::get(), 20); + // 4 authored this block, 5 gets to stay too few 3 was kicked + assert_eq!(CollatorSelection::candidates().len(), 1); + // 3 will be kicked after 1 session delay + assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 3, 5]); + let collator = CandidateInfo { + who: 5, + deposit: 10, + }; + assert_eq!(CollatorSelection::candidates(), vec![collator]); + assert_eq!(CollatorSelection::last_authored_block(4), 20); + initialize_to_block(30); + // 3 gets kicked after 1 session delay + assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 5]); + // kicked collator gets funds back + assert_eq!(Balances::usable_balance(3), 100); + }); +} + +#[test] +#[should_panic = "duplicate invulnerables in genesis."] +fn cannot_set_genesis_value_twice() { + sp_tracing::try_init_simple(); + let mut t = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + let invulnerables = vec![1, 1]; + + let collator_selection = collator_selection::GenesisConfig:: { + desired_candidates: 2, + candidacy_bond: 10, + invulnerables, + }; + // collator selection must be initialized before session. + collator_selection.assimilate_storage(&mut t).unwrap(); +} diff --git a/frame/collator-selection/src/weights.rs b/frame/collator-selection/src/weights.rs new file mode 100644 index 00000000..e2732776 --- /dev/null +++ b/frame/collator-selection/src/weights.rs @@ -0,0 +1,129 @@ +// This file is part of Substrate. + +// Copyright (C) 2021 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. + +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{ + traits::Get, + weights::{constants::RocksDbWeight, Weight}, +}; +use sp_std::marker::PhantomData; + +// The weight info trait for `pallet_collator_selection`. +pub trait WeightInfo { + fn set_invulnerables(_b: u32) -> Weight; + fn set_desired_candidates() -> Weight; + fn set_candidacy_bond() -> Weight; + fn register_as_candidate(_c: u32) -> Weight; + fn leave_intent(_c: u32) -> Weight; + fn note_author() -> Weight; + fn new_session(_c: u32, _r: u32) -> Weight; +} + +/// Weights for pallet_collator_selection using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + fn set_invulnerables(b: u32) -> Weight { + (18_563_000 as Weight) + // Standard Error: 0 + .saturating_add((68_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + fn set_desired_candidates() -> Weight { + (16_363_000 as Weight).saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + fn set_candidacy_bond() -> Weight { + (16_840_000 as Weight).saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + fn register_as_candidate(c: u32) -> Weight { + (71_196_000 as Weight) + // Standard Error: 0 + .saturating_add((198_000 as Weight).saturating_mul(c as Weight)) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + fn leave_intent(c: u32) -> Weight { + (55_336_000 as Weight) + // Standard Error: 0 + .saturating_add((151_000 as Weight).saturating_mul(c as Weight)) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + fn note_author() -> Weight { + (71_461_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + fn new_session(r: u32, c: u32) -> Weight { + (0 as Weight) + // Standard Error: 1_010_000 + .saturating_add((109_961_000 as Weight).saturating_mul(r as Weight)) + // Standard Error: 1_010_000 + .saturating_add((151_952_000 as Weight).saturating_mul(c as Weight)) + .saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(r as Weight))) + .saturating_add(T::DbWeight::get().reads((2 as Weight).saturating_mul(c as Weight))) + .saturating_add(T::DbWeight::get().writes((2 as Weight).saturating_mul(r as Weight))) + .saturating_add(T::DbWeight::get().writes((2 as Weight).saturating_mul(c as Weight))) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + fn set_invulnerables(b: u32) -> Weight { + (18_563_000 as Weight) + // Standard Error: 0 + .saturating_add((68_000 as Weight).saturating_mul(b as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + fn set_desired_candidates() -> Weight { + (16_363_000 as Weight).saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + fn set_candidacy_bond() -> Weight { + (16_840_000 as Weight).saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + fn register_as_candidate(c: u32) -> Weight { + (71_196_000 as Weight) + // Standard Error: 0 + .saturating_add((198_000 as Weight).saturating_mul(c as Weight)) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + fn leave_intent(c: u32) -> Weight { + (55_336_000 as Weight) + // Standard Error: 0 + .saturating_add((151_000 as Weight).saturating_mul(c as Weight)) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + fn note_author() -> Weight { + (71_461_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + } + fn new_session(r: u32, c: u32) -> Weight { + (0 as Weight) + // Standard Error: 1_010_000 + .saturating_add((109_961_000 as Weight).saturating_mul(r as Weight)) + // Standard Error: 1_010_000 + .saturating_add((151_952_000 as Weight).saturating_mul(c as Weight)) + .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(r as Weight))) + .saturating_add(RocksDbWeight::get().reads((2 as Weight).saturating_mul(c as Weight))) + .saturating_add(RocksDbWeight::get().writes((2 as Weight).saturating_mul(r as Weight))) + .saturating_add(RocksDbWeight::get().writes((2 as Weight).saturating_mul(c as Weight))) + } +} diff --git a/frame/custom-signatures/Cargo.toml b/frame/custom-signatures/Cargo.toml new file mode 100644 index 00000000..6c563073 --- /dev/null +++ b/frame/custom-signatures/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "pallet-custom-signatures" +version = "4.3.0" +authors = ["Stake Technologies "] +edition = "2018" +license = "Apache-2.0" +homepage = "https://docs.plasmnet.io/" +repository = "https://github.com/staketechnologies/Plasm/" +description = "FRAME pallet for user defined extrinsic signatures" + +[dependencies] +codec = { package = "parity-scale-codec", version = "2.2.0", features = ["derive"], default-features = false } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } +serde = { version = "1.0.106", features = ["derive"], optional = true } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } + +[dev-dependencies] +pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } +libsecp256k1 = "0.6.0" +hex-literal = "0.2.1" +sp-keyring = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } + +[features] +default = ["std"] +std = [ + "serde", + "codec/std", + "scale-info/std", + "sp-io/std", + "sp-std/std", + "sp-core/std", + "sp-runtime/std", + "frame-support/std", + "frame-system/std", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/custom-signatures/src/ethereum.rs b/frame/custom-signatures/src/ethereum.rs new file mode 100644 index 00000000..21de7e81 --- /dev/null +++ b/frame/custom-signatures/src/ethereum.rs @@ -0,0 +1,83 @@ +//! Ethereum prefixed signatures compatibility instances. + +use codec::{Decode, Encode}; +use sp_core::ecdsa; +use sp_io::{crypto::secp256k1_ecdsa_recover_compressed, hashing::keccak_256}; +use sp_runtime::traits::{IdentifyAccount, Lazy, Verify}; +use sp_runtime::MultiSignature; +use sp_std::prelude::*; + +/// Ethereum-compatible signature type. +#[derive(Encode, Decode, PartialEq, Eq, Clone, scale_info::TypeInfo)] +pub struct EthereumSignature(pub [u8; 65]); + +impl sp_std::fmt::Debug for EthereumSignature { + fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result { + write!(f, "EthereumSignature({:?})", &self.0[..]) + } +} + +impl From for EthereumSignature { + fn from(signature: ecdsa::Signature) -> Self { + Self(signature.into()) + } +} + +impl sp_std::convert::TryFrom> for EthereumSignature { + type Error = (); + + fn try_from(data: Vec) -> Result { + if data.len() == 65 { + let mut inner = [0u8; 65]; + inner.copy_from_slice(&data[..]); + Ok(EthereumSignature(inner)) + } else { + Err(()) + } + } +} + +/// Constructs the message that Ethereum RPC's `personal_sign` and `eth_sign` would sign. +/// +/// Note: sign message hash to escape of message length estimation. +pub fn signable_message(what: &[u8]) -> Vec { + let hash = keccak_256(what); + let mut v = b"\x19Ethereum Signed Message:\n32".to_vec(); + v.extend_from_slice(&hash[..]); + v +} + +/// Attempts to recover the Ethereum public key from a message signature signed by using +/// the Ethereum RPC's `personal_sign` and `eth_sign`. +impl Verify for EthereumSignature { + type Signer = ::Signer; + + fn verify>( + &self, + mut msg: L, + account: &::AccountId, + ) -> bool { + let msg = keccak_256(&signable_message(msg.get())); + match secp256k1_ecdsa_recover_compressed(&self.0, &msg).ok() { + Some(public) => { + let signer = Self::Signer::from(ecdsa::Public::from_raw(public)); + *account == signer.into_account() + } + None => false, + } + } +} + +#[test] +fn verify_should_works() { + use hex_literal::hex; + use sp_core::{ecdsa, Pair}; + + let msg = "test eth signed message"; + let pair = ecdsa::Pair::from_seed(&hex![ + "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + ]); + let account = ::Signer::from(pair.public()).into_account(); + let signature = EthereumSignature(hex!["f5d5cc953828e3fb0d81f3176d88fa5c73d3ad3dc4bc7a8061b03a6db2cd73337778df75a1443e8c642f6ceae0db39b90c321ac270ad7836695cae76f703f3031c"]); + assert_eq!(signature.verify(msg.as_ref(), &account), true); +} diff --git a/frame/custom-signatures/src/lib.rs b/frame/custom-signatures/src/lib.rs new file mode 100644 index 00000000..3d91f36a --- /dev/null +++ b/frame/custom-signatures/src/lib.rs @@ -0,0 +1,200 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; + +/// Ethereum-compatible signatures (eth_sign API call). +pub mod ethereum; + +#[cfg(test)] +mod tests; + +#[frame_support::pallet] +pub mod pallet { + use frame_support::{ + pallet_prelude::*, + traits::{ + Currency, ExistenceRequirement, Get, OnUnbalanced, UnfilteredDispatchable, + WithdrawReasons, + }, + weights::GetDispatchInfo, + }; + use frame_system::{ensure_none, pallet_prelude::*}; + use sp_runtime::traits::{IdentifyAccount, Verify}; + use sp_std::{convert::TryFrom, prelude::*}; + + #[pallet::pallet] + pub struct Pallet(_); + + /// The balance type of this pallet. + pub type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type Event: From> + IsType<::Event>; + + /// A signable call. + type Call: Parameter + UnfilteredDispatchable + GetDispatchInfo; + + /// User defined signature type. + type Signature: Parameter + Verify + TryFrom>; + + /// User defined signer type. + type Signer: IdentifyAccount; + + /// The currency trait. + type Currency: Currency; + + /// The call fee destination. + type OnChargeTransaction: OnUnbalanced< + >::NegativeImbalance, + >; + + /// The call processing fee amount. + #[pallet::constant] + type CallFee: Get>; + + /// The call magic number. + #[pallet::constant] + type CallMagicNumber: Get; + + /// A configuration for base priority of unsigned transactions. + /// + /// This is exposed so that it can be tuned for particular runtime, when + /// multiple pallets send unsigned transactions. + type UnsignedPriority: Get; + } + + #[pallet::error] + pub enum Error { + /// Signature decode fails. + DecodeFailure, + /// Signature and account mismatched. + InvalidSignature, + /// Bad nonce parameter. + BadNonce, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A call just executed. \[result\] + Executed(T::AccountId, DispatchResult), + } + + #[pallet::call] + impl Pallet { + /// # + /// - O(1). + /// - Limited storage reads. + /// - One DB write (event). + /// - Weight of derivative `call` execution + 10,000. + /// # + #[pallet::weight({ + let dispatch_info = call.get_dispatch_info(); + (dispatch_info.weight.saturating_add(10_000), dispatch_info.class) + })] + pub fn call( + origin: OriginFor, + call: Box<::Call>, + signer: T::AccountId, + signature: Vec, + #[pallet::compact] nonce: T::Index, + ) -> DispatchResultWithPostInfo { + ensure_none(origin)?; + + // Ensure that transaction isn't stale + ensure!( + nonce == frame_system::Pallet::::account_nonce(signer.clone()), + Error::::BadNonce, + ); + + let signature = ::Signature::try_from(signature) + .map_err(|_| Error::::DecodeFailure)?; + + // Ensure that transaction signature is valid + ensure!( + Self::valid_signature(&call, &signer, &signature, &nonce), + Error::::InvalidSignature + ); + + // Increment account nonce + frame_system::Pallet::::inc_account_nonce(signer.clone()); + + // Processing fee + let tx_fee = T::Currency::withdraw( + &signer, + T::CallFee::get(), + WithdrawReasons::FEE, + ExistenceRequirement::AllowDeath, + )?; + T::OnChargeTransaction::on_unbalanced(tx_fee); + + // Dispatch call + let new_origin = frame_system::RawOrigin::Signed(signer.clone()).into(); + let res = call.dispatch_bypass_filter(new_origin).map(|_| ()); + Self::deposit_event(Event::Executed(signer, res.map_err(|e| e.error))); + + // Fee already charged + Ok(Pays::No.into()) + } + } + + impl Pallet { + /// Verify custom signature and returns `true` if correct. + pub fn valid_signature( + call: &Box<::Call>, + signer: &T::AccountId, + signature: &T::Signature, + nonce: &T::Index, + ) -> bool { + let payload = (T::CallMagicNumber::get(), *nonce, call.clone()); + signature.verify(&payload.encode()[..], signer) + } + } + + pub(crate) const SIGNATURE_DECODE_FAILURE: u8 = 1; + + #[pallet::validate_unsigned] + impl frame_support::unsigned::ValidateUnsigned for Pallet { + type Call = Call; + + fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { + // Call decomposition (we have only one possible value here) + let (call, signer, signature, nonce) = match call { + Call::call { + call, + signer, + signature, + nonce, + } => (call, signer, signature, nonce), + _ => return InvalidTransaction::Call.into(), + }; + + // Check that tx isn't stale + if *nonce != frame_system::Pallet::::account_nonce(signer.clone()) { + return InvalidTransaction::Stale.into(); + } + + // Check signature encoding + if let Ok(signature) = ::Signature::try_from(signature.clone()) { + // Verify signature + if Self::valid_signature(call, signer, &signature, nonce) { + ValidTransaction::with_tag_prefix("CustomSignatures") + .priority(T::UnsignedPriority::get()) + .and_provides((call, signer, nonce)) + .longevity(64_u64) + .propagate(true) + .build() + } else { + // Signature mismatched to given signer + InvalidTransaction::BadProof.into() + } + } else { + // Signature encoding broken + InvalidTransaction::Custom(SIGNATURE_DECODE_FAILURE).into() + } + } + } +} diff --git a/frame/custom-signatures/src/tests.rs b/frame/custom-signatures/src/tests.rs new file mode 100644 index 00000000..303858fa --- /dev/null +++ b/frame/custom-signatures/src/tests.rs @@ -0,0 +1,243 @@ +use crate as custom_signatures; +use codec::Encode; +use custom_signatures::*; +use frame_support::{assert_err, assert_ok, parameter_types}; +use hex_literal::hex; +use sp_core::{ecdsa, Pair}; +use sp_io::hashing::keccak_256; +use sp_keyring::AccountKeyring as Keyring; +use sp_runtime::{ + testing::{Header, H256}, + traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, Verify}, + transaction_validity::TransactionPriority, + MultiSignature, MultiSigner, +}; + +pub const ECDSA_SEED: [u8; 32] = + hex_literal::hex!["7e9c7ad85df5cdc88659f53e06fb2eb9bab3ebc59083a3190eaf2c730332529c"]; + +type Balance = u128; +type BlockNumber = u64; +type Signature = MultiSignature; +type AccountId = <::Signer as IdentifyAccount>::AccountId; +type Block = frame_system::mocking::MockBlock; +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; + +frame_support::construct_runtime!( + pub enum Runtime where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + System: frame_system::{Pallet, Call, Config, Storage, Event}, + CustomSignatures: custom_signatures::{Pallet, Call, Event}, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; +} + +impl frame_system::Config for Runtime { + type Origin = Origin; + type BaseCallFilter = frame_support::traits::Everything; + type Index = u32; + type BlockNumber = BlockNumber; + type Call = Call; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type DbWeight = (); + type SystemWeightInfo = (); + type BlockWeights = (); + type BlockLength = (); + type SS58Prefix = (); + type OnSetCode = (); +} + +parameter_types! { + pub const ExistentialDeposit: Balance = 1; +} + +impl pallet_balances::Config for Runtime { + type Balance = Balance; + type Event = Event; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = frame_system::Pallet; + type WeightInfo = (); + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = (); +} + +parameter_types! { + pub const Priority: TransactionPriority = TransactionPriority::max_value(); + pub const CallFee: Balance = 42; + pub const CallMagicNumber: u16 = 0xff50; +} + +impl Config for Runtime { + type Event = Event; + type Call = Call; + type Signature = ethereum::EthereumSignature; + type Signer = ::Signer; + type CallMagicNumber = CallMagicNumber; + type Currency = Balances; + type CallFee = CallFee; + type OnChargeTransaction = (); + type UnsignedPriority = Priority; +} + +fn new_test_ext() -> sp_io::TestExternalities { + let mut storage = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + let pair = ecdsa::Pair::from_seed(&ECDSA_SEED); + let account = MultiSigner::from(pair.public()).into_account(); + let _ = pallet_balances::GenesisConfig:: { + balances: vec![(account, 1_000_000_000)], + } + .assimilate_storage(&mut storage); + storage.into() +} + +/// Simple `eth_sign` implementation, should be equal to exported by RPC +fn eth_sign(seed: &[u8; 32], data: &[u8]) -> Vec { + let call_msg = ethereum::signable_message(data); + let ecdsa_msg = libsecp256k1::Message::parse(&keccak_256(&call_msg)); + let secret = libsecp256k1::SecretKey::parse(&seed).expect("valid seed"); + let (signature, recovery_id) = libsecp256k1::sign(&ecdsa_msg, &secret); + let mut out = Vec::new(); + out.extend_from_slice(&signature.serialize()[..]); + // Fix recovery ID: Ethereum uses 27/28 notation + out.push(recovery_id.serialize() + 27); + out +} + +#[test] +fn eth_sign_works() { + let seed = hex!["ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"]; + let text = b"Hello Astar"; + let signature = hex!["0cc6d5de6db06727fe43a260e7c9a417be3daab9b0e4e65e276f543e5c2f3de67e9e26d903d5301181e13033f61692db2dca67c1f8992b62476eaf8cb3a597101c"]; + assert_eq!(eth_sign(&seed, &text[..]), signature); +} + +#[test] +fn invalid_signature() { + let bob: ::AccountId = Keyring::Bob.into(); + let alice: ::AccountId = Keyring::Alice.into(); + let call = pallet_balances::Call::::transfer { + dest: alice.clone(), + value: 1_000, + } + .into(); + let signature = Vec::from(&hex!["dd0992d40e5cdf99db76bed162808508ac65acd7ae2fdc8573594f03ed9c939773e813181788fc02c3c68f3fdc592759b35f6354484343e18cb5317d34dab6c61b"][..]); + new_test_ext().execute_with(|| { + assert_err!( + CustomSignatures::call(Origin::none(), Box::new(call), bob, signature, 0), + Error::::InvalidSignature, + ); + }); +} + +#[test] +fn balance_transfer() { + new_test_ext().execute_with(|| { + let pair = ecdsa::Pair::from_seed(&ECDSA_SEED); + let account = MultiSigner::from(pair.public()).into_account(); + + let alice: ::AccountId = Keyring::Alice.into(); + assert_eq!(System::account(alice.clone()).data.free, 0); + + let call: Call = pallet_balances::Call::::transfer { + dest: alice.clone(), + value: 1_000, + } + .into(); + let payload = (0xff50u16, 0u32, call.clone()); + let signature = eth_sign(&ECDSA_SEED, payload.encode().as_ref()).into(); + + assert_eq!(System::account(account.clone()).nonce, 0); + assert_ok!(CustomSignatures::call( + Origin::none(), + Box::new(call.clone()), + account.clone(), + signature, + 0, + )); + assert_eq!(System::account(alice.clone()).data.free, 1_000); + assert_eq!(System::account(account.clone()).nonce, 1); + assert_eq!(System::account(account.clone()).data.free, 999_998_958); + + let signature = eth_sign(&ECDSA_SEED, payload.encode().as_ref()).into(); + assert_err!( + CustomSignatures::call( + Origin::none(), + Box::new(call.clone()), + account.clone(), + signature, + 0, + ), + Error::::BadNonce, + ); + + let payload = (0xff50u16, 1u32, call.clone()); + let signature = eth_sign(&ECDSA_SEED, payload.encode().as_ref()).into(); + assert_eq!(System::account(account.clone()).nonce, 1); + assert_ok!(CustomSignatures::call( + Origin::none(), + Box::new(call.clone()), + account.clone(), + signature, + 1, + )); + assert_eq!(System::account(alice).data.free, 2_000); + assert_eq!(System::account(account.clone()).nonce, 2); + assert_eq!(System::account(account.clone()).data.free, 999_997_916); + }) +} + +#[test] +fn call_fixtures() { + use sp_core::crypto::Ss58Codec; + + let seed = hex!["ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"]; + let pair = ecdsa::Pair::from_seed(&seed); + assert_eq!( + MultiSigner::from(pair.public()) + .into_account() + .to_ss58check(), + "5EGynCAEvv8NLeHx8vDMvb8hTcEcMYUMWCDQEEncNEfNWB2W", + ); + + let dest = + AccountId::from_ss58check("5GVwcV6EzxxYbXBm7H6dtxc9TCgL4oepMXtgqWYEc3VXJoaf").unwrap(); + let call: Call = pallet_balances::Call::::transfer { dest, value: 1000 }.into(); + assert_eq!( + call.encode(), + hex!["0000c4305fb88b6ccb43d6552dc11d18e7b0ee3185247adcc6e885eb284adf6c563da10f"], + ); + + let payload = (0xff50u16, 0u32, call.clone()); + assert_eq!( + payload.encode(), + hex![ + "50ff000000000000c4305fb88b6ccb43d6552dc11d18e7b0ee3185247adcc6e885eb284adf6c563da10f" + ], + ); + + let signature = hex!["6ecb474240df46ee5cde8f51cf5ccf4c75d15ac3c1772aea6c8189604263c98b16350883438c4eaa447ebcb6889d516f70351fd704bb3521072cd2fccc7c99dc1c"]; + assert_eq!(eth_sign(&seed, payload.encode().as_ref()), signature) +} diff --git a/frame/dapps-staking/Cargo.toml b/frame/dapps-staking/Cargo.toml new file mode 100644 index 00000000..ee58620c --- /dev/null +++ b/frame/dapps-staking/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "pallet-dapps-staking" +version = "1.1.2" +authors = ["Stake Technologies "] +edition = "2018" +homepage = "https://astar.network/" +repository = "https://github.com/PlasmNetwork/Astar/tree/development/dapp-staking" +description = "FRAME pallet to staking for dapps" +license = "PolyForm-Noncommercial-1.0.0" + +[dependencies] +codec = { package = "parity-scale-codec", version = "2.2.0", features = ["derive"], default-features = false } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } +serde = { version = "1.0.106", features = ["derive"], optional = true } +num-traits = { version = "0.2", default-features = false } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +sp-arithmetic = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +pallet-session = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +pallet-timestamp = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +sp-staking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } + +frame-benchmarking = { git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.13', default-features = false, optional = true } + +[features] +default = ["std"] +std = [ + "serde", + "codec/std", + "scale-info/std", + "num-traits/std", + "sp-core/std", + "sp-runtime/std", + "sp-arithmetic/std", + "sp-io/std", + "sp-std/std", + "frame-support/std", + "frame-system/std", + "pallet-balances/std", + "pallet-session/std", + "pallet-timestamp/std", + "sp-staking/std", +] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/dapps-staking/README.md b/frame/dapps-staking/README.md new file mode 100644 index 00000000..9e1e5e3a --- /dev/null +++ b/frame/dapps-staking/README.md @@ -0,0 +1,281 @@ +# Pallet dapps-staking RPC API +This document describes the interface for the pallet-dapps-staking. + +Table of Contents: +1. [Terminology](#Terminology) +1. [Types](#Types) +1. [Events](#Events) +1. [Errors](#Errors) +1. [Calls](#Calls) +1. [Storage](#Storage) +1. [Referent implementatio](#Referent) +1. [FAQ](#FAQ) + +## Terminology +### Actors in dApps Staking + +- `developer`: a developer or organization who deploys the smart contract +- `staker`: any Astar user who stakes tokens on the developer's smart contract + + +### Abbreviations and Terminology +- `dApp`: decentralized application, is an application that runs on a distributed network. +- `smart contract`: on-chain part of the dApp +- `contract`: short for smart contract +- `EVM`: Ethereum Virtual Machine. Solidity Smart contract runs on it. +- `ink!`: Smart Contract written in Rust, compiled to WASM. +- `era`: Period of time. After it ends, rewards can be claimed. It is defined by the number of produced blocks. Duration of an era for this pallet is around 1 day. The exact duration depends on block production duration. +- `claim`: Claim ownership of the rewards from the contract's reward pool. +- `bond`: Freeze funds to gain rewards. +- `stake`: In this pallet a staker stakes bonded funds on a smart contract . +- `unstake`: Unfreeze bonded funds and stop gaining rewards. +- `wasm`: Web Assembly. +- `contracts's reward pool`: Sum of unclaimed rewards on the contract. Including developer and staker parts. + + +--- + +## Types +### SmartContract + +``` +SmartContract: { + _enum: { + Evm: 'H160', + Wasm: 'AccountId' + }, +} +``` +### EraIndex + +`EraIndex: 'u32'` + +### EraStakingPoints + +``` +EraStakingPoints: { + total: 'Balance', + stakers: 'BTreeMap', + _formerStakedEra: 'EraIndex', + claimedRewards: 'Balance' +} +``` +### EraRewardAndStake +``` +EraRewardAndStake { + rewards: 'Balance', + staked: 'Balance' +} +``` + + + +--- +## Events + +* `BondAndStake(AccountId, SmartContract, Balance):` Account has bonded and staked funds on a smart contract. +* `UnbondUnstakeAndWithdraw(AccountId, SmartContract, Balance):` Account has unbonded, unstaked and withdrawn funds. +* `NewContract(AccountId, SmartContract):` New contract added for staking. +* `ContractRemoved(AccountId, SmartContract):` Contract removed from dapps staking. +* `NewDappStakingEra(EraIndex):` New dapps staking era. Distribute era rewards to contracts. +* `ContractClaimed(SmartContract, EraIndex, Balance):` The contract's reward has been claimed for an era +* `Reward(AccountId, SmartContract, EraIndex, Balance):` Reward paid to staker or developer. + + +--- +## Errors +* `StakingWithNoValue` Can not stake with zero value. +* `InsufficientValue`, Can not stake with value less than minimum staking value. +* `MaxNumberOfStakersExceeded`, Number of stakers per contract exceeded. +* `NotOperatedContract`, Targets must be operated contracts +* `NotStakedContract`, Contract isn't staked. +* `UnstakingWithNoValue`, Unstaking a contract with zero value. +* `AlreadyRegisteredContract`, The contract is already registered by other account. +* `ContractIsNotValid`, User attempts to register with address which is not contract. +* `AlreadyUsedDeveloperAccount`, This account was already used to register contract. +* `NotOwnedContract`, Contract not owned by the account. +* `UnknownEraReward`, Report issue on github if this is ever emitted. +* `NotStaked`, Contract hasn't been staked on in this era. +* `AlreadyClaimedInThisEra`, Contract already claimed in this era and reward is distributed. +* `EraOutOfBounds`, Era parameter is out of bounds. +* `RequiredContractPreApproval`, To register a contract, pre-approval is needed for this address. +* `AlreadyPreApprovedDeveloper`, Developer's account is already part of pre-approved list. + +--- +## Calls +### Register +`register(origin: OriginFor, contract_id: T::AccountId) -> DispatchResult {}` +1. Registers contract as a staking target. +1. The dispatch origin for this call must be _Signed_ by the developers's account. +3. Prior to registering, a contract needs to be deployed on the network. The contract address where the contract is deployed is used as the argument in this call. +4. The `dapps-staking` pallet supports both contract types, EVM and Wasm. The Shiden Network supports only EVM at the moment. +5. The type for contract address will be `SmartContract`, which abstracts EVM and Wasm address types. +6. The Developer who is registering the contract has to reserve `RegisterDeposit`. +7. There will be a pre-approved list of developers. This pre-approval could be enabled or disabled. The pre-approval requires sudo call. + +Event: +* `NewContract(developer's account, contract_id)` + +Errors: +* AlreadyRegisteredContract +* AlreadyUsedDeveloperAccount +* ContractIsNotValid +* RequiredContractPreApproval + +### Unregister +`register(origin: OriginFor, contract_id: T::AccountId) -> DispatchResult {}` +1. Unregisters contract from dapps staking. +1. The dispatch origin for this call must be _Signed_ by the developers's account. +3. Prior to unregistering, all rewards for that contract must be claimed. +4. The`RegisterDeposit` is returned to the developer. + +Event: +* `ContractRemoved(developer's account, contract_id)` + +Errors: +* NotOwnedContract +* ContractIsNotValid + +--- +### Bonding and Staking Funds +``` +pub fn bond_and_stake( + origin: OriginFor, + contract_id: SmartContract, + #[pallet::compact] value: BalanceOf, + ) -> DispatchResultWithPostInfo {} +``` +1. The dispatch origin for this call must be _Signed_ by the staker's account. +2. Staked funds will be considered for reward after the end of the current era. +3. The Staker shall use one address for this call. +4. This call is used for both initial staking and for possible additional stakings. +5. The Staker shall stake on only one contract per call +6. The Staker can stake on an unlimited number of contracts but one at the time. +7. The number of stakers per contract is limited to `MaxNumberOfStakersPerContract` +8. Staking will always leave a predefined minimum transferable amount on users account. + + +Events: +``` +BondAndStake( + staker, + contract_id, + value_to_stake + ) +``` + +Errors: +* NotOperatedContract +* StakingWithNoValue +* MaxNumberOfStakersExceeded +* InsufficientValue + +--- +### Unbonding, Unstaking and Funds Withdrawal +``` +pub fn unbond_unstake_and_withdraw( + origin: OriginFor, + contract_id: SmartContract, + value: BalanceOf, +) -> DispatchResultWithPostInfo {} +``` +1. The dispatch origin for this call must be _Signed_ by the staker. +2. The unbonded funds shall be available for withdrawal after `UnbondingDuration` of eras. + +:::info +:bulb: **info:** initially unbonding will be immediate +`UnbondingDuration = 0 EraIndex` +::: + +Events: +`UnbondUnstakeAndWithdraw( + staker, + contract_id, + value_to_unstake + )` + +Errors: +* NotOperatedContract +* UnstakingWithNoValue +* NotStakedContract + +--- +### Claim Rewards +``` +pub fn claim( + origin: OriginFor, + contract_id: T::SmartContract, + era: EraIndex, +) -> DispatchResultWithPostInfo {} +``` +1. Any account can initiate this call. +1. All stakers and the developer of this contract_id will be paid out. +1. The rewards are paid out, they are transferable and they are NOT automatically re-staked. +1. If an era for a contract is out of bounds `[CurrentEra - HistoryDepth, CurrentEra-1]` then error `EraOutOfBounds` is emitted +1. The event `Reward` shall be emitted for each staker in this era and for the developer +1. The event `ContractClaimed` shall be emitted after all stakers and the developer are paid out for this era. + +Event: +`ContractClaimed( + contract_id, + claimer, + era, + )` + +Error: +* NothingToClaim +* AlreadyClaimedInThisEra +* EraOutOfBounds +* Reward +* ContractClaimed + +--- +## Storage +* `Ledger = StorageMap( key:AccountId, value:Balance)`: Bonded amount for the staker +* `CurrentEra = StorageValue( EraIndex )`: The current era index. +* `BlockRewardAccumulator = StorageValue( Balance )`: Accumulator for block rewards during an era. It is reset at every new era. +* `RegisteredDevelopers = StorageMap( key:AccountId, value:SmartContract )`: Registered developer accounts points to coresponding contract. +* `RegisteredDapps = StorageMap( key:SmartContract, value:AccountId )`: Registered dapp points to the developer who registered it. +* `EraRewardsAndStakes = StorageMap( key:EraIndex, value:EraRewardAndStake)`: Total block rewards for the pallet per era and total staked funds. +* `ContractEraStake = StorageDoubleMap( key1: SmartContract, key2:EraIndex, value:EraStakingPoints )`: Stores amount staked and stakers for a contract per era. + +--- +## Referent API implementation +https://github.com/PlasmNetwork/astar-apps + +--- +## FAQ + +### When do the projects/developers get their rewards? +The earned rewards need to be claimed by calling claim() function. Once the claim() function is called all stakers on the contract and the developer of the contract get their rewards. This function can be called from any account. Recommended is that it is called by the projects/developers on a daily or at most weekly basis. + +### What happens if nobody calls the claim function for longer than 'history_depth' days? +The un-claimed rewards older than 'history_depth' days will be burnt. + +### When developers register their dApp, which has no contract yet, what kind of address do they need to input? +There has to be a contract. Registration can’t be done without the contract. + +### Can projects/developers change contract address once it is registered for dApps staking? +The contract address can't be changed for the dApps staking. However, if the project needs to deploy new version of the contract, they can still use old (registered) contract address for dApp staking purposes. + +### How do projects/developers (who joins dApps staking) get their stakers' address and the amount staked? +``` +ContractEraStake(contract_id, era).stakers +``` +This will give the vector of all staker' accounts and how much they have staked. + +### What is the maximum numbers of stakers per dapps? +Please check in the source code constant `MaxNumberOfStakersPerContract`. + +### What is the minimum numbers of stakers per dapps? +Please check in the source code constant `MinimumStakingAmount`. + +### When developers register their dApp, can they registar WASM contract? (If not, can they update it in the future?) +The developers can register several dApps. But they need to use separate accounts and separate contract addresses. +The rule is + +```1 developer <=> 1 contract``` + +### Does dApps staking supports Wasm contracts? +Yes. +Once the Wasm contracts are enabled on a parachain, Wasm contract could be used for dApps staking. diff --git a/frame/dapps-staking/src/benchmarking.rs b/frame/dapps-staking/src/benchmarking.rs new file mode 100644 index 00000000..58536acc --- /dev/null +++ b/frame/dapps-staking/src/benchmarking.rs @@ -0,0 +1,192 @@ +#![cfg(feature = "runtime-benchmarks")] + +use super::*; +use crate::Pallet as DappsStaking; + +use frame_benchmarking::{account, benchmarks, impl_benchmark_test_suite, whitelisted_caller}; +use frame_support::traits::{Get, OnFinalize, OnInitialize, OnUnbalanced}; +use frame_system::{Pallet as System, RawOrigin}; +use sp_runtime::traits::{Bounded, One}; + +const SEED: u32 = 9000; +const BLOCK_REWARD: u32 = 1000u32; + +/// Used to prepare Dapps staking for testing. +/// Resets all existing storage ensuring a clean run for the code that follows. +/// +/// Also initializes the first block which should start a new era. +fn initialize() { + // Remove everything from storage. + Ledger::::remove_all(None); + RegisteredDevelopers::::remove_all(None); + RegisteredDapps::::remove_all(None); + EraRewardsAndStakes::::remove_all(None); + ContractEraStake::::remove_all(None); + CurrentEra::::kill(); + BlockRewardAccumulator::::kill(); + PreApprovalIsEnabled::::kill(); + + // Initialize the first block. + DappsStaking::::on_unbalanced(T::Currency::issue(BLOCK_REWARD.into())); + DappsStaking::::on_initialize(1u32.into()); +} + +/// Assert that the last event equals the provided one. +fn assert_last_event(generic_event: ::Event) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +} + +/// Advance to the specified era, block by block. +fn advance_to_era(n: EraIndex) { + while DappsStaking::::current_era() < n { + DappsStaking::::on_finalize(System::::block_number()); + System::::set_block_number(System::::block_number() + One::one()); + // This is performed outside of dapps staking but we expect it before on_initialize + DappsStaking::::on_unbalanced(T::Currency::issue(BLOCK_REWARD.into())); + DappsStaking::::on_initialize(System::::block_number()); + } +} + +/// Used to register a contract by a developer account. +/// +/// Registered contract is returned. +fn register_contract() -> Result<(T::AccountId, T::SmartContract), &'static str> { + let developer: T::AccountId = account("developer", 10000, SEED); + T::Currency::make_free_balance_be(&developer, BalanceOf::::max_value()); + let contract_id = T::SmartContract::default(); + DappsStaking::::register( + RawOrigin::Signed(developer.clone()).into(), + contract_id.clone(), + )?; + + Ok((developer, contract_id)) +} + +/// Used to bond_and_stake the given contract with the specified amount of stakers. +/// Method will create new staker accounts using the provided seed. +/// +/// Returns all created staker accounts in a vector. +fn prepare_bond_and_stake( + number_of_stakers: u32, + contract_id: &T::SmartContract, + seed: u32, +) -> Result, &'static str> { + let stake_balance = T::MinimumStakingAmount::get(); // maybe make this an argument? + let mut stakers = Vec::new(); + + for id in 0..number_of_stakers { + let staker_acc: T::AccountId = account("pre_staker", id, seed); + stakers.push(staker_acc.clone()); + T::Currency::make_free_balance_be(&staker_acc, BalanceOf::::max_value()); + + DappsStaking::::bond_and_stake( + RawOrigin::Signed(staker_acc).into(), + contract_id.clone(), + stake_balance.clone(), + )?; + } + + Ok(stakers) +} + +benchmarks! { + + register { + initialize::(); + let developer_id = whitelisted_caller(); + let contract_id = T::SmartContract::default(); + T::Currency::make_free_balance_be(&developer_id, BalanceOf::::max_value()); + }: _(RawOrigin::Signed(developer_id.clone()), contract_id.clone()) + verify { + assert_last_event::(Event::::NewContract(developer_id, contract_id).into()); + } + + unregister { + let n in 0 .. T::MaxNumberOfStakersPerContract::get(); + initialize::(); + let (developer_id, contract_id) = register_contract::()?; + prepare_bond_and_stake::(n, &contract_id, SEED)?; + for id in 0..n { + let claimer_id: T::AccountId = account("claimer", id, SEED); + let balance: BalanceOf = 100000u32.into(); + } + + }: _(RawOrigin::Signed(developer_id.clone()), contract_id.clone()) + verify { + assert_last_event::(Event::::ContractRemoved(developer_id, contract_id).into()); + } + + enable_developer_pre_approval { + let pre_approval_enabled = true; + }: _(RawOrigin::Root, pre_approval_enabled) + verify { + assert!(PreApprovalIsEnabled::::get()); + } + + developer_pre_approval { + let pre_approved_id: T::AccountId = account("pre_approved", 100, SEED); + }: _(RawOrigin::Root, pre_approved_id.clone()) + verify { + assert!(PreApprovedDevelopers::::contains_key(&pre_approved_id)); + } + + bond_and_stake { + initialize::(); + + let (_, contract_id) = register_contract::()?; + prepare_bond_and_stake::(T::MaxNumberOfStakersPerContract::get() - 1, &contract_id, SEED)?; + + let staker = whitelisted_caller(); + let _ = T::Currency::make_free_balance_be(&staker, BalanceOf::::max_value()); + let amount = BalanceOf::::max_value() / 2u32.into(); + + }: _(RawOrigin::Signed(staker.clone()), contract_id.clone(), amount.clone()) + verify { + assert_last_event::(Event::::BondAndStake(staker, contract_id, amount).into()); + } + + unbond_unstake_and_withdraw { + initialize::(); + + let (_, contract_id) = register_contract::()?; + prepare_bond_and_stake::(T::MaxNumberOfStakersPerContract::get() - 1, &contract_id, SEED)?; + + let staker = whitelisted_caller(); + let _ = T::Currency::make_free_balance_be(&staker, BalanceOf::::max_value()); + let amount = BalanceOf::::max_value() / 2u32.into(); + + DappsStaking::::bond_and_stake(RawOrigin::Signed(staker.clone()).into(), contract_id.clone(), amount.clone())?; + advance_to_era::(2); + + }: _(RawOrigin::Signed(staker.clone()), contract_id.clone(), amount.clone()) + verify { + assert_last_event::(Event::::UnbondUnstakeAndWithdraw(staker, contract_id, amount).into()); + } + + claim { + let n in 2 .. T::MaxNumberOfStakersPerContract::get(); + + initialize::(); + let (developer_id, contract_id) = register_contract::()?; + + let number_of_stakers = n - 1; + let claim_era = DappsStaking::::current_era(); + prepare_bond_and_stake::(number_of_stakers, &contract_id, SEED)?; + + advance_to_era::(claim_era + 1u32); + + let reward = DappsStaking::::era_reward_and_stake(&claim_era).unwrap().rewards; + + let claimer: T::AccountId = whitelisted_caller(); + }: _(RawOrigin::Signed(claimer.clone()), contract_id.clone(), claim_era) + + force_new_era { + }: _(RawOrigin::Root) + +} + +impl_benchmark_test_suite!( + DappsStaking, + crate::tests::new_test_ext(), + crate::tests::Test, +); diff --git a/frame/dapps-staking/src/lib.rs b/frame/dapps-staking/src/lib.rs new file mode 100644 index 00000000..d449ab60 --- /dev/null +++ b/frame/dapps-staking/src/lib.rs @@ -0,0 +1,81 @@ +//! # dApps Staking Module +//! +//! The dApps staking module manages era, total amounts of rewards and how to distribute. +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Decode, Encode, HasCompact}; +use frame_support::traits::Currency; +use frame_system::{self as system}; +use scale_info::TypeInfo; +use sp_runtime::RuntimeDebug; +use sp_std::{collections::btree_map::BTreeMap, prelude::*}; + +pub mod pallet; +pub mod traits; +pub mod weights; +pub use traits::*; + +#[cfg(any(feature = "runtime-benchmarks"))] +pub mod benchmarking; +#[cfg(test)] +mod mock; +#[cfg(test)] +mod testing_utils; +#[cfg(test)] +mod tests; + +pub use pallet::pallet::*; +pub use sp_staking::SessionIndex; +pub use weights::WeightInfo; + +pub type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + +/// Counter for the number of eras that have passed. +pub type EraIndex = u32; + +/// Mode of era-forcing. +#[derive(Copy, Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo)] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +pub enum Forcing { + /// Not forcing anything - just let whatever happen. + NotForcing, + /// Force a new era, then reset to `NotForcing` as soon as it is done. + /// Note that this will force to trigger an election until a new era is triggered, if the + /// election failed, the next session end will trigger a new election again, until success. + ForceNew, + /// Avoid a new era indefinitely. + ForceNone, + /// Force a new era at the end of all sessions indefinitely. + ForceAlways, +} + +impl Default for Forcing { + fn default() -> Self { + Forcing::NotForcing + } +} + +/// A record for total rewards and total amount staked for an era +#[derive(PartialEq, Eq, Clone, Default, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct EraRewardAndStake { + /// Total amount of rewards for an era + rewards: Balance, + /// Total staked amount for an era + staked: Balance, +} + +/// Used to split total EraPayout among contracts. +/// Each tuple (contract, era) has this structure. +/// This will be used to reward contracts developer and his stakers. +#[derive(Clone, PartialEq, Encode, Decode, Default, RuntimeDebug, TypeInfo)] +pub struct EraStakingPoints { + /// Total staked amount. + total: Balance, + /// The map of stakers and the amount they staked. + stakers: BTreeMap, + // TODO: Get rid of this + _former_staked_era: EraIndex, + /// Accrued and claimed rewards on this contract both for stakers and the developer + claimed_rewards: Balance, +} diff --git a/frame/dapps-staking/src/mock.rs b/frame/dapps-staking/src/mock.rs new file mode 100644 index 00000000..07896944 --- /dev/null +++ b/frame/dapps-staking/src/mock.rs @@ -0,0 +1,263 @@ +use crate::{self as pallet_dapps_staking, weights}; + +use frame_support::{ + construct_runtime, parameter_types, + traits::{Currency, OnFinalize, OnInitialize, OnUnbalanced}, + PalletId, +}; +use sp_core::{H160, H256}; + +use codec::{Decode, Encode}; +use sp_io::TestExternalities; +use sp_runtime::{ + testing::Header, + traits::{AccountIdConversion, BlakeTwo256, IdentityLookup}, + Perbill, +}; + +pub(crate) type AccountId = u64; +pub(crate) type BlockNumber = u64; +pub(crate) type Balance = u128; +pub(crate) type EraIndex = u32; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +/// Value shouldn't be less than 2 for testing purposes, otherwise we cannot test certain corner cases. +pub(crate) const EXISTENTIAL_DEPOSIT: Balance = 2; +pub(crate) const MAX_NUMBER_OF_STAKERS: u32 = 4; +/// Value shouldn't be less than 2 for testing purposes, otherwise we cannot test certain corner cases. +pub(crate) const MINIMUM_STAKING_AMOUNT: Balance = 10; +pub(crate) const DEVELOPER_REWARD_PERCENTAGE: u32 = 80; +pub(crate) const MINIMUM_REMAINING_AMOUNT: Balance = 1; +pub(crate) const HISTORY_DEPTH: u32 = 30; + +// Do note that this needs to at least be 3 for tests to be valid. It can be greater but not smaller. +pub(crate) const BLOCKS_PER_ERA: BlockNumber = 3; + +pub(crate) const REGISTER_DEPOSIT: Balance = 10; + +// ignore MILLIAST for easier test handling. +// reward for dapps-staking will be BLOCK_REWARD/2 = 1000 +pub(crate) const BLOCK_REWARD: Balance = 1000; + +construct_runtime!( + pub enum TestRuntime where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent}, + DappsStaking: pallet_dapps_staking::{Pallet, Call, Storage, Event}, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(1024); +} + +impl frame_system::Config for TestRuntime { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type Origin = Origin; + type Index = u64; + type Call = Call; + type BlockNumber = BlockNumber; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); +} + +parameter_types! { + pub const MaxLocks: u32 = 4; + pub const ExistentialDeposit: Balance = EXISTENTIAL_DEPOSIT; +} + +impl pallet_balances::Config for TestRuntime { + type MaxLocks = MaxLocks; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = Balance; + type Event = Event; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); +} + +parameter_types! { + pub const MinimumPeriod: u64 = 3; +} + +impl pallet_timestamp::Config for TestRuntime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +parameter_types! { + pub const RegisterDeposit: Balance = REGISTER_DEPOSIT; + pub const BlockPerEra: BlockNumber = BLOCKS_PER_ERA; + pub const MaxNumberOfStakersPerContract: u32 = MAX_NUMBER_OF_STAKERS; + pub const MinimumStakingAmount: Balance = MINIMUM_STAKING_AMOUNT; + pub const HistoryDepth: u32 = HISTORY_DEPTH; + pub const DeveloperRewardPercentage: Perbill = Perbill::from_percent(DEVELOPER_REWARD_PERCENTAGE); + pub const DappsStakingPalletId: PalletId = PalletId(*b"mokdpstk"); + pub const MinimumRemainingAmount: Balance = MINIMUM_REMAINING_AMOUNT; + pub const BonusEraDuration: u32 = 3; +} + +impl pallet_dapps_staking::Config for TestRuntime { + type Event = Event; + type Currency = Balances; + type BlockPerEra = BlockPerEra; + type RegisterDeposit = RegisterDeposit; + type DeveloperRewardPercentage = DeveloperRewardPercentage; + type SmartContract = MockSmartContract; + type WeightInfo = weights::SubstrateWeight; + type MaxNumberOfStakersPerContract = MaxNumberOfStakersPerContract; + type HistoryDepth = HistoryDepth; + type BonusEraDuration = BonusEraDuration; + type MinimumStakingAmount = MinimumStakingAmount; + type PalletId = DappsStakingPalletId; + type MinimumRemainingAmount = MinimumRemainingAmount; +} + +#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, scale_info::TypeInfo)] +pub enum MockSmartContract { + Evm(sp_core::H160), + Wasm(AccountId), +} + +impl Default for MockSmartContract { + fn default() -> Self { + MockSmartContract::Evm(H160::repeat_byte(0x01)) + } +} + +impl pallet_dapps_staking::IsContract for MockSmartContract { + fn is_valid(&self) -> bool { + match self { + MockSmartContract::Wasm(_account) => false, + MockSmartContract::Evm(_account) => true, + } + } +} + +pub struct ExternalityBuilder; + +impl ExternalityBuilder { + pub fn build() -> TestExternalities { + let mut storage = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + pallet_balances::GenesisConfig:: { + balances: vec![ + (1, 9000), + (2, 800), + (3, 10000), + (4, 4900), + (5, 3800), + (6, 10), + (7, 1000), + (8, 2000), + (9, 10000), + (10, 300), + (20, 10), + (540, EXISTENTIAL_DEPOSIT), + (1337, 1_000_000_000_000), + ], + } + .assimilate_storage(&mut storage) + .ok(); + + let mut ext = TestExternalities::from(storage); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} + +/// Used to run to the specified block number +pub fn run_to_block(n: u64) { + while System::block_number() < n { + DappsStaking::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + // This is performed outside of dapps staking but we expect it before on_initialize + DappsStaking::on_unbalanced(Balances::issue(BLOCK_REWARD)); + DappsStaking::on_initialize(System::block_number()); + } +} + +/// Used to run the specified number of blocks +pub fn run_for_blocks(n: u64) { + run_to_block(System::block_number() + n); +} + +/// Advance blocks to the beginning of an era. +/// +/// Function has no effect if era is already passed. +pub fn advance_to_era(n: EraIndex) { + while DappsStaking::current_era() < n { + run_for_blocks(1); + } +} + +/// Initialize first block. +/// This method should only be called once in a UT otherwise the first block will get initialized multiple times. +pub fn initialize_first_block() { + // This assert prevents method misuse + assert_eq!(System::block_number(), 1 as BlockNumber); + + // We need to beef up the pallet account balance in case of bonus rewards + let starting_balance = + BLOCK_REWARD * BLOCKS_PER_ERA as Balance * crate::pallet::REWARD_SCALING as Balance; + let _ = Balances::deposit_creating( + &::PalletId::get().into_account(), + starting_balance, + ); + + // This is performed outside of dapps staking but we expect it before on_initialize + DappsStaking::on_unbalanced(Balances::issue(BLOCK_REWARD)); + DappsStaking::on_initialize(System::block_number()); + run_to_block(2); +} + +// Clears all events +pub fn clear_all_events() { + System::reset_events(); +} + +// Used to get a vec of all dapps staking events +pub fn dapps_staking_events() -> Vec> { + System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| { + if let Event::DappsStaking(inner) = e { + Some(inner) + } else { + None + } + }) + .collect() +} diff --git a/frame/dapps-staking/src/pallet/mod.rs b/frame/dapps-staking/src/pallet/mod.rs new file mode 100644 index 00000000..aca8cc05 --- /dev/null +++ b/frame/dapps-staking/src/pallet/mod.rs @@ -0,0 +1,735 @@ +//! Dapps staking FRAME Pallet. + +use super::*; +use frame_support::{ + dispatch::DispatchResult, + ensure, + pallet_prelude::*, + traits::{ + Currency, ExistenceRequirement, Get, Imbalance, LockIdentifier, LockableCurrency, + OnUnbalanced, ReservableCurrency, WithdrawReasons, + }, + weights::Weight, + PalletId, +}; +use frame_system::{ensure_root, ensure_signed, pallet_prelude::*}; +use sp_runtime::{ + traits::{AccountIdConversion, CheckedAdd, Saturating, Zero}, + ArithmeticError, Perbill, +}; +use sp_std::convert::From; + +const STAKING_ID: LockIdentifier = *b"dapstake"; + +pub(crate) const REWARD_SCALING: u32 = 2; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + /// The balance type of this pallet. + pub type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + + #[pallet::pallet] + #[pallet::generate_store(pub(crate) trait Store)] + pub struct Pallet(PhantomData); + + // Negative imbalance type of this pallet. + type NegativeImbalanceOf = <::Currency as Currency< + ::AccountId, + >>::NegativeImbalance; + + impl OnUnbalanced> for Pallet { + fn on_nonzero_unbalanced(block_reward: NegativeImbalanceOf) { + BlockRewardAccumulator::::mutate(|accumulated_reward| { + *accumulated_reward = accumulated_reward.saturating_add(block_reward.peek()); + }); + T::Currency::resolve_creating(&Self::account_id(), block_reward); + } + } + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The staking balance. + type Currency: LockableCurrency + + ReservableCurrency; + + // type used for Accounts on EVM and on Substrate + type SmartContract: IsContract + Parameter + Member; + + /// Number of blocks per era. + #[pallet::constant] + type BlockPerEra: Get>; + + /// Minimum bonded deposit for new contract registration. + #[pallet::constant] + type RegisterDeposit: Get>; + + /// Percentage of reward paid to developer. + #[pallet::constant] + type DeveloperRewardPercentage: Get; + + /// Maximum number of unique stakers per contract. + #[pallet::constant] + type MaxNumberOfStakersPerContract: Get; + + /// Minimum amount user must stake on contract. + /// User can stake less if they already have the minimum staking amount staked on that particular contract. + #[pallet::constant] + type MinimumStakingAmount: Get>; + + /// Number of eras that are valid when claiming rewards. + /// + /// All the rest will be either claimed by the treasury or discarded. + #[pallet::constant] + type HistoryDepth: Get; + + /// Number of eras of doubled claim rewards. + #[pallet::constant] + type BonusEraDuration: Get; + + /// Dapps staking pallet Id + #[pallet::constant] + type PalletId: Get; + + /// Minimum amount that should be left on staker account after staking. + #[pallet::constant] + type MinimumRemainingAmount: Get>; + + /// The overarching event type. + type Event: From> + IsType<::Event>; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + /// Bonded amount for the staker + #[pallet::storage] + #[pallet::getter(fn ledger)] + pub(crate) type Ledger = + StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf, ValueQuery>; + + /// The current era index. + #[pallet::storage] + #[pallet::getter(fn current_era)] + pub type CurrentEra = StorageValue<_, EraIndex, ValueQuery>; + + /// Accumulator for block rewards during an era. It is reset at every new era + #[pallet::storage] + #[pallet::getter(fn block_reward_accumulator)] + pub type BlockRewardAccumulator = StorageValue<_, BalanceOf, ValueQuery>; + + #[pallet::type_value] + pub fn ForceEraOnEmpty() -> Forcing { + Forcing::ForceNone + } + + /// Mode of era forcing. + #[pallet::storage] + #[pallet::getter(fn force_era)] + pub type ForceEra = StorageValue<_, Forcing, ValueQuery, ForceEraOnEmpty>; + + /// Registered developer accounts points to coresponding contract + #[pallet::storage] + #[pallet::getter(fn registered_contract)] + pub(crate) type RegisteredDevelopers = + StorageMap<_, Blake2_128Concat, T::AccountId, T::SmartContract>; + + /// Registered dapp points to the developer who registered it + #[pallet::storage] + #[pallet::getter(fn registered_developer)] + pub(crate) type RegisteredDapps = + StorageMap<_, Blake2_128Concat, T::SmartContract, T::AccountId>; + + /// Total block rewards for the pallet per era and total staked funds + #[pallet::storage] + #[pallet::getter(fn era_reward_and_stake)] + pub(crate) type EraRewardsAndStakes = + StorageMap<_, Twox64Concat, EraIndex, EraRewardAndStake>>; + + /// Stores amount staked and stakers for a contract per era + #[pallet::storage] + #[pallet::getter(fn contract_era_stake)] + pub(crate) type ContractEraStake = StorageDoubleMap< + _, + Blake2_128Concat, + T::SmartContract, + Twox64Concat, + EraIndex, + EraStakingPoints>, + >; + + #[pallet::type_value] + pub(crate) fn PreApprovalOnEmpty() -> bool { + false + } + + /// Enable or disable pre-approval list for new contract registration + #[pallet::storage] + #[pallet::getter(fn pre_approval_is_enabled)] + pub(crate) type PreApprovalIsEnabled = StorageValue<_, bool, ValueQuery, PreApprovalOnEmpty>; + + /// List of pre-approved developers + #[pallet::storage] + #[pallet::getter(fn pre_approved_developers)] + pub(crate) type PreApprovedDevelopers = + StorageMap<_, Twox64Concat, T::AccountId, (), ValueQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// Account has bonded and staked funds on a smart contract. + BondAndStake(T::AccountId, T::SmartContract, BalanceOf), + /// Account has unbonded, unstaked and withdrawn funds. + UnbondUnstakeAndWithdraw(T::AccountId, T::SmartContract, BalanceOf), + /// New contract added for staking. + NewContract(T::AccountId, T::SmartContract), + /// Contract removed from dapps staking. + ContractRemoved(T::AccountId, T::SmartContract), + /// New dapps staking era. Distribute era rewards to contracts. + NewDappStakingEra(EraIndex), + /// Reward paid to staker or developer. + Reward(T::AccountId, T::SmartContract, EraIndex, BalanceOf), + } + + #[pallet::error] + pub enum Error { + /// Can not stake with zero value. + StakingWithNoValue, + /// Can not stake with value less than minimum staking value + InsufficientValue, + /// Number of stakers per contract exceeded. + MaxNumberOfStakersExceeded, + /// Targets must be operated contracts + NotOperatedContract, + /// Contract isn't staked. + NotStakedContract, + /// Unstaking a contract with zero value + UnstakingWithNoValue, + /// The contract is already registered by other account + AlreadyRegisteredContract, + /// User attempts to register with address which is not contract + ContractIsNotValid, + /// This account was already used to register contract + AlreadyUsedDeveloperAccount, + /// Smart contract not owned by the account id. + NotOwnedContract, + /// Report issue on github if this is ever emitted + UnknownEraReward, + /// Contract hasn't been staked on in this era. + NotStaked, + /// Contract already claimed in this era and reward is distributed + AlreadyClaimedInThisEra, + /// Era parameter is out of bounds + EraOutOfBounds, + /// To register a contract, pre-approval is needed for this address + RequiredContractPreApproval, + /// Developer's account is already part of pre-approved list + AlreadyPreApprovedDeveloper, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(now: BlockNumberFor) -> Weight { + let force_new_era = Self::force_era().eq(&Forcing::ForceNew); + let blocks_per_era = T::BlockPerEra::get(); + let previous_era = Self::current_era(); + + // Value is compared to 1 since genesis block is ignored + if now % blocks_per_era == BlockNumberFor::::from(1u32) + || force_new_era + || previous_era.is_zero() + { + let next_era = previous_era + 1; + CurrentEra::::put(next_era); + + let reward = BlockRewardAccumulator::::take(); + Self::reward_balance_snapshoot(previous_era, reward); + + if force_new_era { + ForceEra::::put(Forcing::ForceNone); + } + + Self::deposit_event(Event::::NewDappStakingEra(next_era)); + } + + T::DbWeight::get().writes(5) + } + } + + #[pallet::call] + impl Pallet { + /// register contract into staking targets. + /// contract_id should be ink! or evm contract. + /// + /// Any user can call this function. + /// However, caller have to have deposit amount. + #[pallet::weight(T::WeightInfo::register())] + pub fn register( + origin: OriginFor, + contract_id: T::SmartContract, + ) -> DispatchResultWithPostInfo { + let developer = ensure_signed(origin)?; + + ensure!( + !RegisteredDevelopers::::contains_key(&developer), + Error::::AlreadyUsedDeveloperAccount, + ); + ensure!( + !RegisteredDapps::::contains_key(&contract_id), + Error::::AlreadyRegisteredContract, + ); + ensure!(contract_id.is_valid(), Error::::ContractIsNotValid); + + if Self::pre_approval_is_enabled() { + ensure!( + PreApprovedDevelopers::::contains_key(&developer), + Error::::RequiredContractPreApproval, + ); + } + + T::Currency::reserve(&developer, T::RegisterDeposit::get())?; + + RegisteredDapps::::insert(contract_id.clone(), developer.clone()); + RegisteredDevelopers::::insert(&developer, contract_id.clone()); + + Self::deposit_event(Event::::NewContract(developer, contract_id)); + + Ok(().into()) + } + + /// Unregister existing contract from dapps staking + /// + /// This must be called by the developer who registered the contract. + /// + /// Warning: After this action contract can not be assigned again. + #[pallet::weight(T::WeightInfo::unregister(T::MaxNumberOfStakersPerContract::get()))] + pub fn unregister( + origin: OriginFor, + contract_id: T::SmartContract, + ) -> DispatchResultWithPostInfo { + let developer = ensure_signed(origin)?; + + let registered_contract = + RegisteredDevelopers::::get(&developer).ok_or(Error::::NotOwnedContract)?; + + // This is a sanity check for the unregistration since it requires the caller + // to input the correct contract address. + ensure!( + registered_contract == contract_id, + Error::::NotOwnedContract, + ); + + // We need to unstake all funds that are currently staked + let current_era = Self::current_era(); + let staking_info = Self::staking_info(&contract_id, current_era); + for (staker, amount) in staking_info.stakers.iter() { + let ledger = Self::ledger(staker); + Self::update_ledger(staker, ledger.saturating_sub(*amount)); + } + + // Need to update total amount staked + let staking_total = staking_info.total; + EraRewardsAndStakes::::mutate( + ¤t_era, + // XXX: RewardsAndStakes should be set by `on_initialize` for each era + |value| { + if let Some(x) = value { + x.staked = x.staked.saturating_sub(staking_total) + } + }, + ); + + // Nett to update staking data for next era + let empty_staking_info = EraStakingPoints::>::default(); + ContractEraStake::::insert(contract_id.clone(), current_era, empty_staking_info); + + // Developer account released but contract can not be released more. + T::Currency::unreserve(&developer, T::RegisterDeposit::get()); + RegisteredDevelopers::::remove(&developer); + + Self::deposit_event(Event::::ContractRemoved(developer, contract_id)); + + let number_of_stakers = staking_info.stakers.len(); + Ok(Some(T::WeightInfo::unregister(number_of_stakers as u32)).into()) + } + + /// Lock up and stake balance of the origin account. + /// + /// `value` must be more than the `minimum_balance` specified by `T::Currency` + /// unless account already has bonded value equal or more than 'minimum_balance'. + /// + /// The dispatch origin for this call must be _Signed_ by the staker's account. + /// + /// Effects of staking will be felt at the beginning of the next era. + /// + #[pallet::weight(T::WeightInfo::bond_and_stake())] + pub fn bond_and_stake( + origin: OriginFor, + contract_id: T::SmartContract, + #[pallet::compact] value: BalanceOf, + ) -> DispatchResultWithPostInfo { + let staker = ensure_signed(origin)?; + + // Check that contract is ready for staking. + ensure!( + Self::is_active(&contract_id), + Error::::NotOperatedContract + ); + + // Get the staking ledger or create an entry if it doesn't exist. + let mut ledger = Self::ledger(&staker); + + // Ensure that staker has enough balance to bond & stake. + let free_balance = + T::Currency::free_balance(&staker).saturating_sub(T::MinimumRemainingAmount::get()); + + // Remove already locked funds from the free balance + let available_balance = free_balance.saturating_sub(ledger); + let value_to_stake = value.min(available_balance); + ensure!( + value_to_stake > Zero::zero(), + Error::::StakingWithNoValue + ); + + // Get the latest era staking point info or create it if contract hasn't been staked yet so far. + let current_era = Self::current_era(); + let mut staking_info = Self::staking_info(&contract_id, current_era); + + // Ensure that we can add additional staker for the contract. + if !staking_info.stakers.contains_key(&staker) { + ensure!( + staking_info.stakers.len() < T::MaxNumberOfStakersPerContract::get() as usize, + Error::::MaxNumberOfStakersExceeded, + ); + } + + // Increment ledger and total staker value for contract. Overflow shouldn't be possible but the check is here just for safety. + ledger = ledger + .checked_add(&value_to_stake) + .ok_or(ArithmeticError::Overflow)?; + staking_info.total = staking_info + .total + .checked_add(&value_to_stake) + .ok_or(ArithmeticError::Overflow)?; + + // Increment personal staking amount. + let entry = staking_info.stakers.entry(staker.clone()).or_default(); + *entry = entry + .checked_add(&value_to_stake) + .ok_or(ArithmeticError::Overflow)?; + + ensure!( + *entry >= T::MinimumStakingAmount::get(), + Error::::InsufficientValue, + ); + + // Update total staked value in era. + EraRewardsAndStakes::::mutate(¤t_era, |value| { + if let Some(x) = value { + x.staked = x.staked.saturating_add(value_to_stake) + } + }); + + // Update ledger and payee + Self::update_ledger(&staker, ledger); + + // Update staked information for contract in current era + ContractEraStake::::insert(contract_id.clone(), current_era, staking_info); + + Self::deposit_event(Event::::BondAndStake( + staker, + contract_id, + value_to_stake, + )); + Ok(Some(T::WeightInfo::bond_and_stake()).into()) + } + + /// Unbond, unstake and withdraw balance from the contract. + /// + /// Value will be unlocked for the user. + /// + /// In case remaining staked balance on contract is below minimum staking amount, + /// entire stake for that contract will be unstaked. + /// + #[pallet::weight(T::WeightInfo::unbond_unstake_and_withdraw())] + pub fn unbond_unstake_and_withdraw( + origin: OriginFor, + contract_id: T::SmartContract, + #[pallet::compact] value: BalanceOf, + ) -> DispatchResultWithPostInfo { + let staker = ensure_signed(origin)?; + + ensure!(value > Zero::zero(), Error::::UnstakingWithNoValue); + ensure!( + Self::is_active(&contract_id), + Error::::NotOperatedContract, + ); + + // Get the latest era staking points for the contract. + let current_era = Self::current_era(); + let mut staking_info = Self::staking_info(&contract_id, current_era); + + ensure!( + staking_info.stakers.contains_key(&staker), + Error::::NotStakedContract, + ); + let staked_value = staking_info.stakers[&staker]; + + ensure!(value <= staked_value, Error::::InsufficientValue); + + // Calculate the value which will be unstaked. + let remaining = staked_value.saturating_sub(value); + let value_to_unstake = if remaining < T::MinimumStakingAmount::get() { + staking_info.stakers.remove(&staker); + staked_value + } else { + staking_info.stakers.insert(staker.clone(), remaining); + value + }; + + // Get the staking ledger and update it + let ledger = Self::ledger(&staker); + Self::update_ledger(&staker, ledger.saturating_sub(value_to_unstake)); + + // Update total staked value in era. + EraRewardsAndStakes::::mutate(¤t_era, |value| { + if let Some(x) = value { + x.staked = x.staked.saturating_sub(value_to_unstake) + } + }); + + // Update the era staking points + staking_info.total = staking_info.total.saturating_sub(value_to_unstake); + ContractEraStake::::insert(contract_id.clone(), current_era, staking_info); + + Self::deposit_event(Event::::UnbondUnstakeAndWithdraw( + staker, + contract_id, + value_to_unstake, + )); + + Ok(Some(T::WeightInfo::unbond_unstake_and_withdraw()).into()) + } + + /// claim the rewards earned by contract_id. + /// All stakers and developer for this contract will be paid out with single call. + /// claim is valid for all unclaimed eras but not longer than history_depth(). + /// Any reward older than history_depth() will go to Treasury. + /// Any user can call this function. + #[pallet::weight(T::WeightInfo::claim(T::MaxNumberOfStakersPerContract::get() + 1))] + pub fn claim( + origin: OriginFor, + contract_id: T::SmartContract, + era: EraIndex, + ) -> DispatchResultWithPostInfo { + let _ = ensure_signed(origin)?; + + let developer = + RegisteredDapps::::get(&contract_id).ok_or(Error::::NotOperatedContract)?; + + let current_era = Self::current_era(); + let era_low_bound = current_era.saturating_sub(T::HistoryDepth::get()); + + ensure!( + era < current_era && era >= era_low_bound, + Error::::EraOutOfBounds, + ); + + let mut staking_info = Self::staking_info(&contract_id, era); + + ensure!( + staking_info.claimed_rewards.is_zero(), + Error::::AlreadyClaimedInThisEra, + ); + + ensure!(!staking_info.stakers.is_empty(), Error::::NotStaked,); + + let reward_and_stake = + Self::era_reward_and_stake(era).ok_or(Error::::UnknownEraReward)?; + + // Calculate the contract reward for this era. + let reward_ratio = Perbill::from_rational(staking_info.total, reward_and_stake.staked); + let contract_reward = if era < T::BonusEraDuration::get() { + // Double reward as a bonus. + reward_ratio + * reward_and_stake + .rewards + .saturating_mul(REWARD_SCALING.into()) + } else { + reward_ratio * reward_and_stake.rewards + }; + + // Withdraw reward funds from the dapps staking + let reward_pool = T::Currency::withdraw( + &Self::account_id(), + contract_reward, + WithdrawReasons::TRANSFER, + ExistenceRequirement::AllowDeath, + )?; + + // Divide reward between stakers and the developer of the contract + let (developer_reward, mut stakers_reward) = + reward_pool.split(T::DeveloperRewardPercentage::get() * contract_reward); + + Self::deposit_event(Event::::Reward( + developer.clone(), + contract_id.clone(), + era, + developer_reward.peek(), + )); + T::Currency::resolve_creating(&developer, developer_reward); + + // Calculate & pay rewards for all stakers + let stakers_total_reward = stakers_reward.peek(); + for (staker, staked_balance) in &staking_info.stakers { + let ratio = Perbill::from_rational(*staked_balance, staking_info.total); + let (reward, new_stakers_reward) = + stakers_reward.split(ratio * stakers_total_reward); + stakers_reward = new_stakers_reward; + + Self::deposit_event(Event::::Reward( + staker.clone(), + contract_id.clone(), + era, + reward.peek(), + )); + T::Currency::resolve_creating(staker, reward); + } + + let number_of_payees = staking_info.stakers.len() + 1; + + // updated counter for total rewards paid to the contract + staking_info.claimed_rewards = contract_reward; + >::insert(&contract_id, era, staking_info); + + Ok(Some(T::WeightInfo::claim(number_of_payees as u32)).into()) + } + + /// Force there to be a new era at the end of the next block. After this, it will be + /// reset to normal (non-forced) behaviour. + /// + /// The dispatch origin must be Root. + /// + /// + /// # + /// - No arguments. + /// - Weight: O(1) + /// - Write ForceEra + /// # + #[pallet::weight(T::WeightInfo::force_new_era())] + pub fn force_new_era(origin: OriginFor) -> DispatchResult { + ensure_root(origin)?; + ForceEra::::put(Forcing::ForceNew); + Ok(()) + } + + /// add contract address to the pre-approved list. + /// contract_id should be ink! or evm contract. + /// + /// Sudo call is required + #[pallet::weight(T::WeightInfo::developer_pre_approval())] + pub fn developer_pre_approval( + origin: OriginFor, + developer: T::AccountId, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + + ensure!( + !PreApprovedDevelopers::::contains_key(&developer), + Error::::AlreadyPreApprovedDeveloper + ); + PreApprovedDevelopers::::insert(developer, ()); + + Ok(().into()) + } + + /// Enable or disable adding new contracts to the pre-approved list + /// + /// Sudo call is required + #[pallet::weight(T::WeightInfo::enable_developer_pre_approval())] + pub fn enable_developer_pre_approval( + origin: OriginFor, + enabled: bool, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + PreApprovalIsEnabled::::put(enabled); + Ok(().into()) + } + } + + impl Pallet { + /// Get AccountId assigned to the pallet. + fn account_id() -> T::AccountId { + T::PalletId::get().into_account() + } + + /// Update the ledger for a staker. This will also update the stash lock. + /// This lock will lock the entire funds except paying for further transactions. + fn update_ledger(staker: &T::AccountId, ledger: BalanceOf) { + if ledger.is_zero() { + Ledger::::remove(&staker); + T::Currency::remove_lock(STAKING_ID, &staker); + } else { + T::Currency::set_lock(STAKING_ID, &staker, ledger, WithdrawReasons::all()); + Ledger::::insert(staker, ledger); + } + } + + /// The block rewards are accumulated on the pallets's account during an era. + /// This function takes a snapshot of the pallet's balance accrued during current era + /// and stores it for future distribution + /// + /// This is called just at the beginning of an era. + fn reward_balance_snapshoot(era: EraIndex, reward: BalanceOf) { + // Get the reward and stake information for previous era + let mut reward_and_stake = Self::era_reward_and_stake(era).unwrap_or_default(); + + // Prepare info for the next era + EraRewardsAndStakes::::insert( + era + 1, + EraRewardAndStake { + rewards: Zero::zero(), + staked: reward_and_stake.staked.clone(), + }, + ); + + // Set the reward for the previous era. + reward_and_stake.rewards = reward; + EraRewardsAndStakes::::insert(era, reward_and_stake); + } + + /// This helper returns `EraStakingPoints` for given era if possible or latest stored data + /// or finally default value if storage have no data for it. + pub(crate) fn staking_info( + contract_id: &T::SmartContract, + era: EraIndex, + ) -> EraStakingPoints> { + if let Some(staking_info) = ContractEraStake::::get(contract_id, era) { + staking_info + } else { + let avail_era = ContractEraStake::::iter_key_prefix(&contract_id) + .filter(|x| *x <= era) + .max() + .unwrap_or(Zero::zero()); + + let mut staking_points = + ContractEraStake::::get(contract_id, avail_era).unwrap_or_default(); + // Needs to be reset since otherwise it might seem as if rewards were already claimed for this era. + staking_points.claimed_rewards = Zero::zero(); + staking_points + } + } + + /// Check that contract have active developer linkage. + fn is_active(contract_id: &T::SmartContract) -> bool { + if let Some(developer) = RegisteredDapps::::get(contract_id) { + if let Some(r_contract_id) = RegisteredDevelopers::::get(&developer) { + return r_contract_id == *contract_id; + } + } + false + } + } +} diff --git a/frame/dapps-staking/src/testing_utils.rs b/frame/dapps-staking/src/testing_utils.rs new file mode 100644 index 00000000..8a6bc470 --- /dev/null +++ b/frame/dapps-staking/src/testing_utils.rs @@ -0,0 +1,208 @@ +use super::*; +use frame_support::assert_ok; +use mock::{EraIndex, *}; +use sp_runtime::{traits::AccountIdConversion, Perbill}; + +/// Used to fetch the free balance of dapps staking account +pub(crate) fn free_balance_of_dapps_staking_account() -> Balance { + ::Currency::free_balance( + &::PalletId::get().into_account(), + ) +} + +/// Used to register contract for staking and assert success. +pub(crate) fn register_contract(developer: AccountId, contract: &MockSmartContract) { + assert_ok!(DappsStaking::enable_developer_pre_approval( + Origin::root(), + false + )); + assert_ok!(DappsStaking::register( + Origin::signed(developer), + contract.clone() + )); +} + +/// Used to get total dapps reward for an era. +pub(crate) fn get_total_reward_per_era() -> Balance { + BLOCK_REWARD * BLOCKS_PER_ERA as Balance +} + +/// Used to perform bond_and_stake with success assertion. +pub(crate) fn bond_and_stake_with_verification( + staker_id: AccountId, + contract_id: &MockSmartContract, + value: Balance, +) { + assert_ok!(DappsStaking::bond_and_stake( + Origin::signed(staker_id), + contract_id.clone(), + value, + )); +} + +/// Used to perform unbond_unstake_and_withdraw with success assertion. +pub(crate) fn unbond_unstake_and_withdraw_with_verification( + staker_id: AccountId, + contract_id: &MockSmartContract, + value: Balance, +) { + assert_ok!(DappsStaking::unbond_unstake_and_withdraw( + Origin::signed(staker_id), + contract_id.clone(), + value, + )); +} + +/// Used to verify ledger content. +pub(crate) fn verify_ledger(staker_id: AccountId, staked_value: Balance) { + // Verify that ledger storage values are as expected. + let ledger = Ledger::::get(staker_id); + assert_eq!(staked_value, ledger); +} + +/// Used to verify era staking points content. Note that this requires era staking points for the specified era to exist. +pub(crate) fn verify_era_staking_points( + contract_id: &MockSmartContract, + total_staked_value: Balance, + era: crate::EraIndex, + stakers: Vec<(AccountId, Balance)>, +) { + // Verify that era staking points are as expected for the contract + let era_staking_points = ContractEraStake::::get(&contract_id, era).unwrap(); + assert_eq!(total_staked_value, era_staking_points.total); + assert_eq!(stakers.len(), era_staking_points.stakers.len()); + + for (staker_id, staked_value) in stakers { + assert_eq!( + staked_value, + *era_staking_points.stakers.get(&staker_id).unwrap() + ); + } +} + +/// Used to verify pallet era staked value. +pub(crate) fn verify_pallet_era_staked(era: crate::EraIndex, total_staked_value: Balance) { + // Verify that total staked amount in era is as expected + let era_rewards = EraRewardsAndStakes::::get(era).unwrap(); + assert_eq!(total_staked_value, era_rewards.staked); +} + +/// Used to verify pallet era staked and reward values. +pub(crate) fn verify_pallet_era_staked_and_reward( + era: crate::EraIndex, + total_staked_value: Balance, + total_reward_value: Balance, +) { + // Verify that total staked amount in era is as expected + let era_rewards = EraRewardsAndStakes::::get(era).unwrap(); + assert_eq!(total_staked_value, era_rewards.staked); + assert_eq!(total_reward_value, era_rewards.rewards); +} + +/// Used to perform claim with success assertion +pub(crate) fn claim_with_verification( + claimer: AccountId, + contract: MockSmartContract, + claim_era: EraIndex, +) { + // Clear all events so we can check all the emitted events from claim + clear_all_events(); + + assert_ok!(DappsStaking::claim( + Origin::signed(claimer), + contract, + claim_era + )); + + // Calculated expected reward that will be distributed for the contract. + let rewards_and_stakes = DappsStaking::era_reward_and_stake(&claim_era).unwrap(); + let staking_points = DappsStaking::contract_era_stake(&contract, &claim_era).unwrap(); + let calculated_reward = Perbill::from_rational(staking_points.total, rewards_and_stakes.staked) + * rewards_and_stakes.rewards + * reward_scaling_factor(claim_era); + + // Collect all Reward events and sum up all the rewards. + let emitted_rewards: Balance = dapps_staking_events() + .iter() + .filter_map(|e| { + if let crate::Event::Reward(_, _, _, single_reward) = e { + Some(*single_reward as Balance) + } else { + None + } + }) + .sum(); + + assert_eq!(calculated_reward, emitted_rewards); +} + +// Get reward scaling factor for the given era +pub(crate) fn reward_scaling_factor(era: EraIndex) -> Balance { + if era < BonusEraDuration::get() { + pallet::REWARD_SCALING as Balance + } else { + 1 as Balance + } +} + +/// Used to calculate the expected reward for the staker +pub(crate) fn calc_expected_staker_reward( + claim_era: EraIndex, + contract_stake: Balance, + staker_stake: Balance, +) -> Balance { + let rewards_and_stakes = DappsStaking::era_reward_and_stake(&claim_era).unwrap(); + let contract_reward = Perbill::from_rational(contract_stake, rewards_and_stakes.staked) + * rewards_and_stakes.rewards + * reward_scaling_factor(claim_era); + let contract_reward_staker_part = + Perbill::from_percent(100 - DEVELOPER_REWARD_PERCENTAGE) * contract_reward; + + Perbill::from_rational(staker_stake, contract_stake) * contract_reward_staker_part +} + +/// Used to calculate the expected reward for the developer +pub(crate) fn calc_expected_developer_reward( + claim_era: EraIndex, + contract_stake: Balance, +) -> Balance { + let rewards_and_stakes = DappsStaking::era_reward_and_stake(&claim_era).unwrap(); + let contract_reward = Perbill::from_rational(contract_stake, rewards_and_stakes.staked) + * rewards_and_stakes.rewards + * reward_scaling_factor(claim_era); + Perbill::from_percent(DEVELOPER_REWARD_PERCENTAGE) * contract_reward +} + +/// Check staker/dev Balance after reward distribution. +/// Check that claimed rewards for staker/dev are updated. +pub(crate) fn check_rewards_on_balance_and_storage( + user: &AccountId, + free_balance: Balance, + expected_era_reward: Balance, +) { + assert_eq!( + ::Currency::free_balance(user), + free_balance + expected_era_reward + ); +} + +/// Check that claimed rewards on this contract are updated +pub(crate) fn check_paidout_rewards_for_contract( + contract: &MockSmartContract, + era: EraIndex, + expected_rewards: Balance, +) { + let contract_staking_info = DappsStaking::contract_era_stake(contract, era).unwrap_or_default(); + assert_eq!(contract_staking_info.claimed_rewards, expected_rewards,) +} + +/// Used to verify that storage is cleared of all contract related values after unregistration. +pub(crate) fn verify_storage_after_unregister( + developer: &AccountId, + contract_id: &MockSmartContract, +) { + assert!(RegisteredDapps::::contains_key(contract_id)); + assert!(!RegisteredDevelopers::::contains_key( + developer + )); +} diff --git a/frame/dapps-staking/src/tests.rs b/frame/dapps-staking/src/tests.rs new file mode 100644 index 00000000..bd65e25a --- /dev/null +++ b/frame/dapps-staking/src/tests.rs @@ -0,0 +1,1691 @@ +use super::{pallet::pallet::Error, Event, *}; +use frame_support::{ + assert_noop, assert_ok, + traits::{OnInitialize, OnUnbalanced}, +}; +use mock::{Balances, MockSmartContract, *}; +use sp_core::H160; +use sp_runtime::traits::Zero; + +use testing_utils::*; + +#[test] +fn on_unbalanced_is_ok() { + ExternalityBuilder::build().execute_with(|| { + // At the beginning, both should be 0 + assert!(BlockRewardAccumulator::::get().is_zero()); + assert!(free_balance_of_dapps_staking_account().is_zero()); + + // After handling imbalance, accumulator and account should be updated + DappsStaking::on_unbalanced(Balances::issue(BLOCK_REWARD)); + assert_eq!(BLOCK_REWARD, BlockRewardAccumulator::::get()); + assert_eq!(BLOCK_REWARD, free_balance_of_dapps_staking_account()); + + // After triggering a new era, accumulator should be set to 0 but account shouldn't consume any new imbalance + DappsStaking::on_initialize(System::block_number()); + assert!(BlockRewardAccumulator::::get().is_zero()); + assert_eq!(BLOCK_REWARD, free_balance_of_dapps_staking_account()); + }) +} + +#[test] +fn on_initialize_is_ok() { + ExternalityBuilder::build().execute_with(|| { + // Before we start, era is zero + assert!(DappsStaking::current_era().is_zero()); + + // We initialize the first block and advance to second one. New era must be triggered. + initialize_first_block(); + let current_era = DappsStaking::current_era(); + assert_eq!(1, current_era); + + // Now advance by history limit. Ensure that rewards for era 1 still exist. + let previous_era = current_era; + advance_to_era(previous_era + HistoryDepth::get() + 1); + + // Check that all reward&stakes are as expected + let current_era = DappsStaking::current_era(); + for era in 1..current_era { + let era_rewards_and_stakes = EraRewardsAndStakes::::get(era).unwrap(); + assert_eq!(get_total_reward_per_era(), era_rewards_and_stakes.rewards); + } + // Current era rewards should be 0 + verify_pallet_era_staked_and_reward(current_era, 0, 0); + }) +} + +#[test] +fn staking_info_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let contract_id = MockSmartContract::Evm(H160::repeat_byte(0x01)); + register_contract(10, &contract_id); + + let staker_1 = 1; + let staker_2 = 2; + let staker_3 = 3; + let amount = 100; + + // Prepare a little scenario. + // staker_1 --> stakes starting era, doesn't unstake + // staker_2 --> stakes starting era, unstakes everything before final era + // staker_3 --> stakes after starting era, doesn't unstake + + let starting_era = 3; + advance_to_era(starting_era); + bond_and_stake_with_verification(staker_1, &contract_id, amount); + bond_and_stake_with_verification(staker_2, &contract_id, amount); + + let mid_era = 7; + advance_to_era(mid_era); + unbond_unstake_and_withdraw_with_verification(staker_2, &contract_id, amount); + bond_and_stake_with_verification(staker_3, &contract_id, amount); + + let final_era = 12; + advance_to_era(final_era); + + // Checks + + // Check first interval + for era in starting_era..mid_era { + let staking_info = DappsStaking::staking_info(&contract_id, era); + assert_eq!(2_usize, staking_info.stakers.len()); + assert!(staking_info.stakers.contains_key(&staker_1)); + assert!(staking_info.stakers.contains_key(&staker_1)); + } + + // Check second interval + for era in mid_era..=final_era { + let staking_info = DappsStaking::staking_info(&contract_id, era); + assert_eq!(2_usize, staking_info.stakers.len()); + assert!(staking_info.stakers.contains_key(&staker_1)); + assert!(staking_info.stakers.contains_key(&staker_3)); + } + + // Check that before starting era nothing exists + let staking_info = DappsStaking::staking_info(&contract_id, starting_era - 1); + assert!(staking_info.stakers.is_empty()); + + // TODO: Do we want such behavior? + // Era hasn't happened yet but value is returned as if it has happened + let staking_info = DappsStaking::staking_info(&contract_id, final_era + 1); + assert_eq!(2_usize, staking_info.stakers.len()); + assert!(staking_info.stakers.contains_key(&staker_1)); + assert!(staking_info.stakers.contains_key(&staker_3)); + }) +} + +#[test] +fn register_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let developer = 1; + let ok_contract = MockSmartContract::Evm(H160::repeat_byte(0x01)); + + assert!(::Currency::reserved_balance(&developer).is_zero()); + register_contract(developer, &ok_contract); + System::assert_last_event(mock::Event::DappsStaking(Event::NewContract( + developer, + ok_contract, + ))); + + assert_eq!( + RegisterDeposit::get(), + ::Currency::reserved_balance(&developer) + ); + }) +} + +#[test] +fn register_twice_with_same_account_not_works() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let developer = 1; + let contract1 = MockSmartContract::Evm(H160::repeat_byte(0x01)); + let contract2 = MockSmartContract::Evm(H160::repeat_byte(0x02)); + + register_contract(developer, &contract1); + + System::assert_last_event(mock::Event::DappsStaking(Event::NewContract( + developer, contract1, + ))); + + // now register different contract with same account + assert_noop!( + DappsStaking::register(Origin::signed(developer), contract2), + Error::::AlreadyUsedDeveloperAccount + ); + }) +} + +#[test] +fn register_same_contract_twice_not_works() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let developer1 = 1; + let developer2 = 2; + let contract = MockSmartContract::Evm(H160::repeat_byte(0x01)); + + register_contract(developer1, &contract); + + System::assert_last_event(mock::Event::DappsStaking(Event::NewContract( + developer1, contract, + ))); + + // now register same contract by different developer + assert_noop!( + DappsStaking::register(Origin::signed(developer2), contract), + Error::::AlreadyRegisteredContract + ); + }) +} + +#[test] +fn register_with_pre_approve_enabled() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + let developer = 1; + let contract = MockSmartContract::Evm(H160::repeat_byte(0x01)); + + // enable pre-approval for the developers + assert_ok!(DappsStaking::enable_developer_pre_approval( + Origin::root(), + true + )); + assert!(DappsStaking::pre_approval_is_enabled()); + + // register new developer without pre-approval, should fail + assert_noop!( + DappsStaking::register(Origin::signed(developer), contract.clone()), + Error::::RequiredContractPreApproval, + ); + + // preapprove developer + assert_ok!(DappsStaking::developer_pre_approval( + Origin::root(), + developer.clone() + )); + + // try to pre-approve again same developer, should fail + assert_noop!( + DappsStaking::developer_pre_approval(Origin::root(), developer.clone()), + Error::::AlreadyPreApprovedDeveloper + ); + + // register new contract by pre-approved developer + assert_ok!(DappsStaking::register( + Origin::signed(developer), + contract.clone() + )); + System::assert_last_event(mock::Event::DappsStaking(Event::NewContract( + developer, contract, + ))); + + // disable pre_approval and register contract2 + let developer2 = 2; + let contract2 = MockSmartContract::Evm(H160::repeat_byte(0x02)); + assert_ok!(DappsStaking::enable_developer_pre_approval( + Origin::root(), + false + )); + assert_ok!(DappsStaking::register( + Origin::signed(developer2), + contract2.clone() + )); + System::assert_last_event(mock::Event::DappsStaking(Event::NewContract( + developer2, contract2, + ))); + }) +} + +#[test] +fn unregister_after_register_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let developer = 1; + let contract_id = MockSmartContract::Evm(H160::repeat_byte(0x01)); + + register_contract(developer, &contract_id); + + // Ensure that contract can be unregistered + assert_ok!(DappsStaking::unregister( + Origin::signed(developer), + contract_id.clone() + )); + System::assert_last_event(mock::Event::DappsStaking(Event::ContractRemoved( + developer, + contract_id, + ))); + verify_storage_after_unregister(&developer, &contract_id); + + assert!(::Currency::reserved_balance(&developer).is_zero()); + }) +} + +#[test] +fn unregister_with_staked_contracts_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let developer = 1; + let dummy_developer = 2; + let staker_1 = 3; + let staker_2 = 4; + let staked_value_1 = 150; + let staked_value_2 = 330; + let contract_id = MockSmartContract::Evm(H160::repeat_byte(0x01)); + let dummy_contract_id = MockSmartContract::Evm(H160::repeat_byte(0x05)); + + // Register both contracts and stake them + register_contract(developer, &contract_id); + register_contract(dummy_developer, &dummy_contract_id); + bond_and_stake_with_verification(staker_1, &contract_id, staked_value_1); + bond_and_stake_with_verification(staker_2, &contract_id, staked_value_2); + + // This contract will just exist so it helps us with testing ledger content + bond_and_stake_with_verification(staker_1, &dummy_contract_id, staked_value_1); + bond_and_stake_with_verification(staker_2, &dummy_contract_id, staked_value_2); + + // Advance eras. This will accumulate some rewards. + advance_to_era(5); + let current_era = DappsStaking::current_era(); + + // Ensure that era reward&stake are as expected. Later we will verify that this value is reduced. + assert_eq!( + (staked_value_1 + staked_value_2) * 2, + DappsStaking::era_reward_and_stake(¤t_era) + .unwrap() + .staked + ); + + // Ensure that contract can be unregistered + assert_ok!(DappsStaking::unregister( + Origin::signed(developer), + contract_id.clone() + )); + System::assert_last_event(mock::Event::DappsStaking(Event::ContractRemoved( + developer, + contract_id, + ))); + verify_storage_after_unregister(&developer, &contract_id); + + // Ensure ledger contains expected stake values. We have a single staked contract remaining. + assert_eq!(staked_value_1, DappsStaking::ledger(&staker_1)); + assert_eq!(staked_value_2, DappsStaking::ledger(&staker_2)); + + // Ensure that era reward&stake has been updated + assert_eq!( + staked_value_1 + staked_value_2, + DappsStaking::era_reward_and_stake(¤t_era) + .unwrap() + .staked + ); + }) +} + +#[test] +fn unregister_with_incorrect_contract_does_not_work() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let developer_1 = 1; + let developer_2 = 2; + let first_contract_id = MockSmartContract::Evm(H160::repeat_byte(0x01)); + let second_contract_id = MockSmartContract::Evm(H160::repeat_byte(0x02)); + + register_contract(developer_1, &first_contract_id); + + // Try to unregister contract with developer who hasn't registered any contract + assert_noop!( + DappsStaking::unregister(Origin::signed(developer_2), first_contract_id.clone()), + Error::::NotOwnedContract + ); + + // Register second contract with second dev and then try to unregister it using the first developer + register_contract(developer_2, &second_contract_id); + assert_noop!( + DappsStaking::unregister(Origin::signed(developer_1), second_contract_id.clone()), + Error::::NotOwnedContract + ); + }) +} + +#[test] +fn unregister_stake_and_unstake_is_not_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let developer = 1; + let staker = 2; + let contract_id = MockSmartContract::Evm(H160::repeat_byte(0x01)); + + // Register contract, stake it, unstake a bit + register_contract(developer, &contract_id); + bond_and_stake_with_verification(staker, &contract_id, 100); + unbond_unstake_and_withdraw_with_verification(staker, &contract_id, 10); + + // Unregister contract and verify that stake & unstake no longer work + assert_ok!(DappsStaking::unregister( + Origin::signed(developer), + contract_id.clone() + )); + + assert_noop!( + DappsStaking::bond_and_stake(Origin::signed(staker), contract_id.clone(), 100), + Error::::NotOperatedContract + ); + assert_noop!( + DappsStaking::unbond_unstake_and_withdraw( + Origin::signed(staker), + contract_id.clone(), + 100 + ), + Error::::NotOperatedContract + ); + }) +} + +#[test] +fn on_initialize_when_dapp_staking_enabled_in_mid_of_an_era_is_ok() { + ExternalityBuilder::build().execute_with(|| { + // Set a block number in mid of an era + System::set_block_number(2); + + // Verify that current era is 0 since dapps staking hasn't been initialized yet + assert_eq!(0u32, DappsStaking::current_era()); + + // Call on initialize in the mid of an era (according to block number calculation) + // but since no era was initialized before, it will trigger a new era init. + DappsStaking::on_initialize(System::block_number()); + assert_eq!(1u32, DappsStaking::current_era()); + }) +} + +#[test] +fn bond_and_stake_different_eras_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let staker_id = 1; + let first_stake_value = 100; + let contract_id = MockSmartContract::Evm(H160::repeat_byte(0x01)); + + let current_era = DappsStaking::current_era(); + + // Insert a contract under registered contracts. + register_contract(20, &contract_id); + + // initially, storage values should be None + assert!(ContractEraStake::::get(&contract_id, current_era).is_none()); + + /////////////////////////////////////////////////////////// + //////////// FIRST BOND AND STAKE + /////////////////////////////////////////////////////////// + // Bond and stake on a single contract and ensure it went ok. + assert_ok!(DappsStaking::bond_and_stake( + Origin::signed(staker_id), + contract_id.clone(), + first_stake_value, + )); + System::assert_last_event(mock::Event::DappsStaking(Event::BondAndStake( + staker_id, + contract_id.clone(), + first_stake_value, + ))); + + // Verify storage values to see if contract was successfully bonded and staked. + verify_ledger(staker_id, first_stake_value); + verify_era_staking_points( + &contract_id, + first_stake_value, + current_era, + vec![(staker_id, first_stake_value)], + ); + verify_pallet_era_staked(current_era, first_stake_value); + + // Prepare new values and advance some eras. + let second_stake_value = 300; + let total_stake_value = first_stake_value + second_stake_value; + + advance_to_era(current_era + 2); + let current_era = DappsStaking::current_era(); + + /////////////////////////////////////////////////////////// + //////////// SECOND BOND AND STAKE + /////////////////////////////////////////////////////////// + // Stake and bond again on the same contract but using a different amount. + assert_ok!(DappsStaking::bond_and_stake( + Origin::signed(staker_id), + contract_id.clone(), + second_stake_value, + )); + System::assert_last_event(mock::Event::DappsStaking(Event::BondAndStake( + staker_id, + contract_id.clone(), + second_stake_value, + ))); + + // Verify that storage values are as expected + verify_ledger(staker_id, total_stake_value); + verify_era_staking_points( + &contract_id, + total_stake_value, + current_era, + vec![(staker_id, total_stake_value)], + ); + verify_pallet_era_staked(current_era, total_stake_value); + }) +} + +#[test] +fn bond_and_stake_two_different_contracts_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let staker_id = 1; + let first_stake_value = 100; + let second_stake_value = 300; + let first_contract_id = MockSmartContract::Evm(H160::repeat_byte(0x01)); + let second_contract_id = MockSmartContract::Evm(H160::repeat_byte(0x02)); + let current_era = DappsStaking::current_era(); + + // Insert contracts under registered contracts. Don't use the staker Id. + register_contract(5, &first_contract_id); + register_contract(6, &second_contract_id); + + // Stake on both contracts. + assert_ok!(DappsStaking::bond_and_stake( + Origin::signed(staker_id), + first_contract_id.clone(), + first_stake_value + )); + assert_ok!(DappsStaking::bond_and_stake( + Origin::signed(staker_id), + second_contract_id.clone(), + second_stake_value + )); + let total_stake_value = first_stake_value + second_stake_value; + + // Verify storage values to see if funds were successfully bonded + verify_ledger(staker_id, total_stake_value); + verify_era_staking_points( + &first_contract_id, + first_stake_value, + current_era, + vec![(staker_id, first_stake_value)], + ); + verify_era_staking_points( + &second_contract_id, + second_stake_value, + current_era, + vec![(staker_id, second_stake_value)], + ); + assert_eq!( + EraRewardsAndStakes::::get(current_era) + .unwrap() + .staked, + total_stake_value, + ); + }) +} + +#[test] +fn bond_and_stake_two_stakers_one_contract_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let first_staker_id = 1; + let second_staker_id = 2; + let first_stake_value = 50; + let second_stake_value = 235; + let contract_id = MockSmartContract::Evm(H160::repeat_byte(0x01)); + let current_era = DappsStaking::current_era(); + + // Insert a contract under registered contracts. + register_contract(10, &contract_id); + + // Both stakers stake on the same contract, expect a pass. + assert_ok!(DappsStaking::bond_and_stake( + Origin::signed(first_staker_id), + contract_id.clone(), + first_stake_value + )); + assert_ok!(DappsStaking::bond_and_stake( + Origin::signed(second_staker_id), + contract_id.clone(), + second_stake_value + )); + let total_stake_value = first_stake_value + second_stake_value; + + // Verify storage values to see if funds were successfully bonded + verify_ledger(first_staker_id, first_stake_value); + verify_ledger(second_staker_id, second_stake_value); + verify_era_staking_points( + &contract_id, + total_stake_value, + current_era, + vec![ + (first_staker_id, first_stake_value), + (second_staker_id, second_stake_value), + ], + ); + assert_eq!( + EraRewardsAndStakes::::get(current_era) + .unwrap() + .staked, + total_stake_value, + ); + }) +} + +#[test] +fn bond_and_stake_different_value_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let staker_id = 1; + let contract_id = MockSmartContract::Evm(H160::repeat_byte(0x01)); + + // Insert a contract under registered contracts. + register_contract(20, &contract_id); + + // Bond&stake almost the entire available balance of the staker. + let staker_free_balance = + Balances::free_balance(&staker_id).saturating_sub(MINIMUM_REMAINING_AMOUNT); + assert_ok!(DappsStaking::bond_and_stake( + Origin::signed(staker_id), + contract_id.clone(), + staker_free_balance - 1 + )); + System::assert_last_event(mock::Event::DappsStaking(Event::BondAndStake( + staker_id, + contract_id.clone(), + staker_free_balance - 1, + ))); + + // Bond&stake again with less than existential deposit but this time expect a pass + // since we're only increasing the already staked amount. + assert_ok!(DappsStaking::bond_and_stake( + Origin::signed(staker_id), + contract_id, + 1 + )); + System::assert_last_event(mock::Event::DappsStaking(Event::BondAndStake( + staker_id, + contract_id.clone(), + 1, + ))); + + // Bond&stake more than what's available in funds. Verify that only what's available is bonded&staked. + let staker_id = 2; + let staker_free_balance = Balances::free_balance(&staker_id); + assert_ok!(DappsStaking::bond_and_stake( + Origin::signed(staker_id), + contract_id, + staker_free_balance + 1 + )); + System::assert_last_event(mock::Event::DappsStaking(Event::BondAndStake( + staker_id, + contract_id.clone(), + staker_free_balance.saturating_sub(MINIMUM_REMAINING_AMOUNT), + ))); + // Verify the minimum transferable amount of stakers account + let transferable_balance = + Balances::free_balance(&staker_id) - Ledger::::get(staker_id); + assert_eq!(MINIMUM_REMAINING_AMOUNT, transferable_balance); + + // Bond&stake some amount, a bit less than free balance + let staker_id = 3; + let staker_free_balance = + Balances::free_balance(&staker_id).saturating_sub(MINIMUM_REMAINING_AMOUNT); + assert_ok!(DappsStaking::bond_and_stake( + Origin::signed(staker_id), + contract_id, + staker_free_balance - 200 + )); + System::assert_last_event(mock::Event::DappsStaking(Event::BondAndStake( + staker_id, + contract_id.clone(), + staker_free_balance - 200, + ))); + + // Try to bond&stake more than we have available (since we already locked most of the free balance). + assert_ok!(DappsStaking::bond_and_stake( + Origin::signed(staker_id), + contract_id, + 500 + )); + System::assert_last_event(mock::Event::DappsStaking(Event::BondAndStake( + staker_id, + contract_id.clone(), + 200, + ))); + }) +} + +#[test] +fn bond_and_stake_history_depth_has_passed_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let developer = 1; + let staker_id = 2; + let contract_id = MockSmartContract::Evm(H160::repeat_byte(0x01)); + + let start_era = DappsStaking::current_era(); + register_contract(developer, &contract_id); + + // Do the first bond&stake + let first_staking_amount = 200; + bond_and_stake_with_verification(staker_id, &contract_id, first_staking_amount); + + // Advance eras beyond history depth + let history_depth = HistoryDepth::get(); + advance_to_era(start_era + history_depth + 1); + + // Bond&stake again + let second_staking_amount = 350; + bond_and_stake_with_verification(staker_id, &contract_id, second_staking_amount); + + // Verify storage content + let total_staked = first_staking_amount + second_staking_amount; + let current_era = DappsStaking::current_era(); + + // Verify storage values related to the current era + verify_ledger(staker_id, total_staked); + verify_era_staking_points( + &contract_id, + total_staked, + current_era, + vec![(staker_id, total_staked)], + ); + assert_eq!( + EraRewardsAndStakes::::get(current_era) + .unwrap() + .staked, + total_staked, + ); + + // Also ensure that former values still exists even if they're beyond 'history depth' + verify_era_staking_points( + &contract_id, + first_staking_amount, + start_era, + vec![(staker_id, first_staking_amount)], + ); + assert_eq!( + EraRewardsAndStakes::::get(current_era) + .unwrap() + .staked, + total_staked, + ); + }) +} + +#[test] +fn bond_and_stake_on_unregistered_contract_not_works() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let staker_id = 1; + let stake_value = 100; + + // Check not registered contract. Expect an error. + let evm_contract = MockSmartContract::Evm(H160::repeat_byte(0x01)); + assert_noop!( + DappsStaking::bond_and_stake(Origin::signed(staker_id), evm_contract, stake_value), + Error::::NotOperatedContract + ); + }) +} + +#[test] +fn bond_and_stake_insufficient_value() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + let staker_id = 1; + let contract_id = MockSmartContract::Evm(H160::repeat_byte(0x01)); + + // Insert a contract under registered contracts. + register_contract(20, &contract_id); + + // If user tries to make an initial bond&stake with less than minimum amount, raise an error. + assert_noop!( + DappsStaking::bond_and_stake( + Origin::signed(staker_id), + contract_id.clone(), + MINIMUM_STAKING_AMOUNT - 1 + ), + Error::::InsufficientValue + ); + + // Now bond&stake the entire stash so we lock all the available funds. + let staker_free_balance = Balances::free_balance(&staker_id); + assert_ok!(DappsStaking::bond_and_stake( + Origin::signed(staker_id), + contract_id, + staker_free_balance + )); + + // Now try to bond&stake some additional funds and expect an error since we cannot bond&stake 0. + assert_noop!( + DappsStaking::bond_and_stake(Origin::signed(staker_id), contract_id.clone(), 1), + Error::::StakingWithNoValue + ); + }) +} + +#[test] +fn bond_and_stake_too_many_stakers_per_contract() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let contract_id = MockSmartContract::Evm(H160::repeat_byte(0x01)); + // Insert a contract under registered contracts. + register_contract(10, &contract_id); + + // Stake with MAX_NUMBER_OF_STAKERS on the same contract. It must work. + for staker_id in 1..=MAX_NUMBER_OF_STAKERS { + assert_ok!(DappsStaking::bond_and_stake( + Origin::signed(staker_id.into()), + contract_id.clone(), + 100, + )); + } + + // Now try to stake with an additional staker and expect an error. + assert_noop!( + DappsStaking::bond_and_stake( + Origin::signed((1 + MAX_NUMBER_OF_STAKERS).into()), + contract_id.clone(), + 100 + ), + Error::::MaxNumberOfStakersExceeded + ); + }) +} + +#[test] +fn unbond_unstake_and_withdraw_multiple_time_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let staker_id = 1; + let contract_id = MockSmartContract::Evm(H160::repeat_byte(0x01)); + let original_staked_value = 300 + MINIMUM_STAKING_AMOUNT; + let old_era = DappsStaking::current_era(); + + // Insert a contract under registered contracts, bond&stake it. + register_contract(10, &contract_id); + bond_and_stake_with_verification(staker_id, &contract_id, original_staked_value); + advance_to_era(old_era + 1); + let new_era = DappsStaking::current_era(); + + // Unstake such an amount so there will remain staked funds on the contract + let unstaked_value = 100; + assert_ok!(DappsStaking::unbond_unstake_and_withdraw( + Origin::signed(staker_id), + contract_id.clone(), + unstaked_value + )); + System::assert_last_event(mock::Event::DappsStaking(Event::UnbondUnstakeAndWithdraw( + staker_id, + contract_id.clone(), + unstaked_value, + ))); + + let new_staked_value = original_staked_value - unstaked_value; + + // Verify that storage values for the current are as expected. + verify_ledger(staker_id, new_staked_value); + verify_era_staking_points( + &contract_id, + new_staked_value, + new_era, + vec![(staker_id, new_staked_value)], + ); + verify_pallet_era_staked(new_era, new_staked_value); + + // Also verify that the storage values for the old era haven't been changed due to unstaking + verify_era_staking_points( + &contract_id, + original_staked_value, + old_era, + vec![(staker_id, original_staked_value)], + ); + + // Unbond yet again, but don't advance era + // Unstake such an amount so there will remain staked funds on the contract + let unstaked_value = 50; + assert_ok!(DappsStaking::unbond_unstake_and_withdraw( + Origin::signed(staker_id), + contract_id.clone(), + unstaked_value + )); + System::assert_last_event(mock::Event::DappsStaking(Event::UnbondUnstakeAndWithdraw( + staker_id, + contract_id.clone(), + unstaked_value, + ))); + + let new_staked_value = new_staked_value - unstaked_value; + + // Verify that storage values for the current are have been changed as expected. + verify_ledger(staker_id, new_staked_value); + verify_era_staking_points( + &contract_id, + new_staked_value, + new_era, + vec![(staker_id, new_staked_value)], + ); + verify_pallet_era_staked(new_era, new_staked_value); + }) +} + +#[test] +fn unbond_unstake_and_withdraw_value_below_staking_threshold() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let staker_id = 1; + let contract_id = MockSmartContract::Evm(H160::repeat_byte(0x01)); + let first_value_to_unstake = 300; + let staked_value = first_value_to_unstake + MINIMUM_STAKING_AMOUNT; + + let current_era = DappsStaking::current_era(); + + // Insert a contract under registered contracts, bond&stake it. + register_contract(10, &contract_id); + bond_and_stake_with_verification(staker_id, &contract_id, staked_value); + + // Unstake such an amount that exactly minimum staking amount will remain staked. + assert_ok!(DappsStaking::unbond_unstake_and_withdraw( + Origin::signed(staker_id), + contract_id.clone(), + first_value_to_unstake + )); + System::assert_last_event(mock::Event::DappsStaking(Event::UnbondUnstakeAndWithdraw( + staker_id, + contract_id.clone(), + first_value_to_unstake, + ))); + + // Unstake 1 token and expect that the entire staked amount will be unstaked. + assert_ok!(DappsStaking::unbond_unstake_and_withdraw( + Origin::signed(staker_id), + contract_id.clone(), + 1 + )); + System::assert_last_event(mock::Event::DappsStaking(Event::UnbondUnstakeAndWithdraw( + staker_id, + contract_id.clone(), + MINIMUM_STAKING_AMOUNT, + ))); + assert!(!Ledger::::contains_key(staker_id)); + + verify_era_staking_points(&contract_id, Zero::zero(), current_era, vec![]); + assert_eq!( + EraRewardsAndStakes::::get(current_era) + .unwrap() + .staked, + Zero::zero(), + ); + }) +} + +#[test] +fn unbond_unstake_and_withdraw_in_different_eras() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let first_staker_id = 1; + let second_staker_id = 2; + let contract_id = MockSmartContract::Evm(H160::repeat_byte(0x01)); + let staked_value = 500; + + // Insert a contract under registered contracts, bond&stake it with two different stakers. + register_contract(10, &contract_id); + bond_and_stake_with_verification(first_staker_id, &contract_id, staked_value); + bond_and_stake_with_verification(second_staker_id, &contract_id, staked_value); + let total_staked_value = 2 * staked_value; + + // Advance era, unbond&withdraw with first staker, verify that it was successful + let current_era = DappsStaking::current_era(); + advance_to_era(current_era + 10); + let current_era = DappsStaking::current_era(); + + let first_unstake_value = 100; + assert_ok!(DappsStaking::unbond_unstake_and_withdraw( + Origin::signed(first_staker_id), + contract_id.clone(), + first_unstake_value + )); + System::assert_last_event(mock::Event::DappsStaking(Event::UnbondUnstakeAndWithdraw( + first_staker_id, + contract_id.clone(), + first_unstake_value, + ))); + + // Verify that storage values are as expected for both stakers and total staked value + let new_total_staked = total_staked_value - first_unstake_value; + let first_staked_value = staked_value - first_unstake_value; + verify_era_staking_points( + &contract_id, + new_total_staked, + current_era, + vec![ + (first_staker_id, first_staked_value), + (second_staker_id, staked_value), + ], + ); + verify_pallet_era_staked(current_era, new_total_staked); + + // Advance era, unbond with second staker and verify storage values are as expected + advance_to_era(current_era + 10); + let current_era = DappsStaking::current_era(); + + let second_unstake_value = 333; + assert_ok!(DappsStaking::unbond_unstake_and_withdraw( + Origin::signed(second_staker_id), + contract_id.clone(), + second_unstake_value + )); + System::assert_last_event(mock::Event::DappsStaking(Event::UnbondUnstakeAndWithdraw( + second_staker_id, + contract_id.clone(), + second_unstake_value, + ))); + + // Verify that storage values are as expected for both stakers and total staked value + let new_total_staked = new_total_staked - second_unstake_value; + let second_staked_value = staked_value - second_unstake_value; + verify_era_staking_points( + &contract_id, + new_total_staked, + current_era, + vec![ + (first_staker_id, first_staked_value), + (second_staker_id, second_staked_value), + ], + ); + verify_pallet_era_staked(current_era, new_total_staked); + }) +} + +#[test] +fn unbond_unstake_and_withdraw_history_depth_has_passed_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let developer = 1; + let staker_id = 2; + let contract_id = MockSmartContract::Evm(H160::repeat_byte(0x01)); + + ////////////////////////////////////////////// + ///// FIRST ERA + ////////////////////////////////////////////// + + let start_era = DappsStaking::current_era(); + register_contract(developer, &contract_id); + + // Do the first bond&stake + let first_staking_amount = 200; + bond_and_stake_with_verification(staker_id, &contract_id, first_staking_amount); + + ////////////////////////////////////////////// + ///// FIRST ERA ADVANCEMENT + ////////////////////////////////////////////// + + // Advance eras beyond history depth + let history_depth = HistoryDepth::get(); + advance_to_era(start_era + history_depth + 1); + + let first_unstake_amount = 30; + unbond_unstake_and_withdraw_with_verification( + staker_id, + &contract_id, + first_unstake_amount, + ); + + // Verify storage content + let mut total_staked = first_staking_amount - first_unstake_amount; + let current_era = DappsStaking::current_era(); + + // Verify storage values related to the current era + verify_ledger(staker_id, total_staked); + verify_era_staking_points( + &contract_id, + total_staked, + current_era, + vec![(staker_id, total_staked)], + ); + assert_eq!( + EraRewardsAndStakes::::get(current_era) + .unwrap() + .staked, + total_staked, + ); + + ////////////////////////////////////////////// + ///// SECOND ERA ADVANCEMENT + ////////////////////////////////////////////// + + // Advance era again beyond the history depth + advance_to_era(current_era + history_depth + 10); + let current_era = DappsStaking::current_era(); + + let second_unstake_amount = 30; + unbond_unstake_and_withdraw_with_verification( + staker_id, + &contract_id, + second_unstake_amount, + ); + + // Verify storage content + total_staked -= second_unstake_amount; + + // Verify storage values related to the current era + verify_ledger(staker_id, total_staked); + verify_era_staking_points( + &contract_id, + total_staked, + current_era, + vec![(staker_id, total_staked)], + ); + assert_eq!( + EraRewardsAndStakes::::get(current_era) + .unwrap() + .staked, + total_staked, + ); + }) +} + +#[test] +fn unbond_unstake_and_withdraw_contract_is_not_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let staker_id = 1; + let unstake_value = 100; + + // Contract isn't registered, expect an error. + let evm_contract = MockSmartContract::Evm(H160::repeat_byte(0x01)); + assert_noop!( + DappsStaking::bond_and_stake(Origin::signed(staker_id), evm_contract, unstake_value), + Error::::NotOperatedContract + ); + }) +} + +#[test] +fn unbond_unstake_and_withdraw_unstake_not_possible() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let first_staker_id = 1; + let first_contract_id = MockSmartContract::Evm(H160::repeat_byte(0x01)); + let original_staked_value = 100 + MINIMUM_STAKING_AMOUNT; + + // Insert a contract under registered contracts, bond&stake it. + register_contract(10, &first_contract_id); + + // Try to unstake with 0, expect an error. + assert_noop!( + DappsStaking::unbond_unstake_and_withdraw( + Origin::signed(first_staker_id), + first_contract_id.clone(), + Zero::zero() + ), + Error::::UnstakingWithNoValue + ); + + // Try to unstake contract which hasn't been staked by anyone + assert_noop!( + DappsStaking::unbond_unstake_and_withdraw( + Origin::signed(first_staker_id), + first_contract_id.clone(), + original_staked_value + ), + Error::::NotStakedContract, + ); + + // Now we finally stake the contract + bond_and_stake_with_verification( + first_staker_id, + &first_contract_id, + original_staked_value, + ); + + // Try to unbond and withdraw using a different staker, one that hasn't staked on this one. Expect an error. + let second_staker_id = 2; + assert_noop!( + DappsStaking::unbond_unstake_and_withdraw( + Origin::signed(second_staker_id), + first_contract_id.clone(), + original_staked_value + ), + Error::::NotStakedContract + ); + + // Bond a second contract using the second staker. Ensure that second staker still cannot unbond&withdraw funds from the first contract + let second_contract_id = MockSmartContract::Evm(H160::repeat_byte(0x02)); + register_contract(20, &second_contract_id); + bond_and_stake_with_verification( + second_staker_id, + &second_contract_id, + original_staked_value, + ); + assert_noop!( + DappsStaking::unbond_unstake_and_withdraw( + Origin::signed(second_staker_id), + first_contract_id.clone(), + original_staked_value + ), + Error::::NotStakedContract + ); + }) +} + +#[test] +fn new_era_is_ok() { + ExternalityBuilder::build().execute_with(|| { + // set initial era index + advance_to_era(DappsStaking::current_era() + 10); + let starting_era = DappsStaking::current_era(); + + // verify that block reward is zero at the beginning of an era + assert!(DappsStaking::block_reward_accumulator().is_zero()); + + // Increment block by setting it to the first block in era value + run_for_blocks(1); + let current_era = DappsStaking::current_era(); + assert_eq!(starting_era, current_era); + + // verify that block reward is added to the block_reward_accumulator + let block_reward = DappsStaking::block_reward_accumulator(); + assert_eq!(BLOCK_REWARD, block_reward); + + // register and bond to verify storage item + let staker = 2; + let developer = 3; + let staked_amount = 100; + let contract = MockSmartContract::Evm(H160::repeat_byte(0x01)); + register_contract(developer, &contract); + bond_and_stake_with_verification(staker, &contract, staked_amount); + + // CurrentEra should be incremented + // block_reward_accumulator should be reset to 0 + advance_to_era(DappsStaking::current_era() + 1); + + let current_era = DappsStaking::current_era(); + assert_eq!(starting_era + 1, current_era); + System::assert_last_event(mock::Event::DappsStaking(Event::NewDappStakingEra( + starting_era + 1, + ))); + + // verify that block reward accumulator is reset to 0 + let block_reward = DappsStaking::block_reward_accumulator(); + assert!(block_reward.is_zero()); + + let expected_era_reward = get_total_reward_per_era(); + // verify that .staked is copied and .reward is added + verify_pallet_era_staked_and_reward(starting_era, staked_amount, expected_era_reward); + }) +} + +#[test] +fn new_era_forcing() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + advance_to_era(3); + let starting_era = mock::DappsStaking::current_era(); + + // call on_initilize. It is not last block in the era, but it should increment the era + >::put(Forcing::ForceNew); + run_for_blocks(1); + + // check that era is incremented + let current = mock::DappsStaking::current_era(); + assert_eq!(starting_era + 1, current); + + // check that forcing is cleared + assert_eq!(mock::DappsStaking::force_era(), Forcing::ForceNone); + + // check the event for the new era + System::assert_last_event(mock::Event::DappsStaking(Event::NewDappStakingEra( + starting_era + 1, + ))); + }) +} + +#[test] +fn claim_contract_not_registered() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let claimer = 2; + let contract = MockSmartContract::Evm(H160::repeat_byte(0x01)); + + assert_noop!( + DappsStaking::claim(Origin::signed(claimer), contract, 1), + Error::::NotOperatedContract + ); + }) +} + +#[test] +fn claim_invalid_eras() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let developer1 = 1; + let claimer = 2; + let contract = MockSmartContract::Evm(H160::repeat_byte(0x01)); + + register_contract(developer1, &contract); + + // Advance way past the history depth + advance_to_era(HistoryDepth::get() * 2); + + let too_old_era = DappsStaking::current_era() - HistoryDepth::get() - 1; + assert_noop!( + DappsStaking::claim(Origin::signed(claimer), contract, too_old_era), + Error::::EraOutOfBounds, + ); + + let future_era = DappsStaking::current_era() + 1; + assert_noop!( + DappsStaking::claim(Origin::signed(claimer), contract, future_era), + Error::::EraOutOfBounds, + ); + + let current_era = DappsStaking::current_era(); + assert_noop!( + DappsStaking::claim(Origin::signed(claimer), contract, current_era,), + Error::::EraOutOfBounds, + ); + + let non_staked_era = current_era - 1; + assert_noop!( + DappsStaking::claim(Origin::signed(claimer), contract, non_staked_era,), + Error::::NotStaked, + ); + }) +} + +#[test] +fn claim_twice_in_same_era() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let developer = 1; + let claimer = 2; + let contract = MockSmartContract::Evm(H160::repeat_byte(0x01)); + + register_contract(developer, &contract); + bond_and_stake_with_verification(claimer, &contract, 100); + + advance_to_era(DappsStaking::current_era() + 1); + + let claim_era = DappsStaking::current_era() - 1; + claim_with_verification(claimer, contract, claim_era); + + assert_noop!( + DappsStaking::claim(Origin::signed(claimer), contract, claim_era), + Error::::AlreadyClaimedInThisEra + ); + }) +} + +#[test] +fn claim_for_all_valid_history_eras_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let developer1 = 1; + let claimer = 2; + let contract = MockSmartContract::Evm(H160::repeat_byte(0x01)); + + register_contract(developer1, &contract); + bond_and_stake_with_verification(claimer, &contract, 100); + + // Advance past the history depth + advance_to_era(DappsStaking::current_era() + HistoryDepth::get() + 1); + let current_era = DappsStaking::current_era(); + + // All eras must be claimable + for era in (current_era - HistoryDepth::get())..current_era { + claim_with_verification(claimer, contract.clone(), era); + } + }) +} + +#[test] +fn claim_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let developer = 1; + let claimer = 2; + let contract = MockSmartContract::Evm(H160::repeat_byte(0x01)); + + let start_era = DappsStaking::current_era(); + + register_contract(developer, &contract); + bond_and_stake_with_verification(claimer, &contract, 100); + + advance_to_era(start_era + 3); + + let issuance_before_claim = ::Currency::total_issuance(); + let claim_era = DappsStaking::current_era() - 1; + + claim_with_verification(claimer, contract, claim_era); + + // Claim shouldn't mint new tokens, instead it should just transfer from the dapps staking pallet account + let issuance_after_claim = ::Currency::total_issuance(); + assert_eq!(issuance_before_claim, issuance_after_claim); + }) +} + +#[test] +fn claim_after_unregister_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + let developer = 1; + let staker = 2; + let stake_amount_1 = 100; + let contract = MockSmartContract::Evm(H160::repeat_byte(0x01)); + + // Register contract, stake it + register_contract(developer, &contract); + bond_and_stake_with_verification(staker, &contract, stake_amount_1); + + // Advance by some eras + advance_to_era(5); + + // Unregister contract, without claiming it! + assert_ok!(DappsStaking::unregister( + Origin::signed(developer), + contract.clone() + )); + let unregistered_era = DappsStaking::current_era(); + + // Ensure that contract can still be claimed. + let current_era = DappsStaking::current_era(); + for era in 1..current_era { + claim_with_verification(staker, contract.clone(), era); + } + + // Advance some more eras + advance_to_era(unregistered_era + 5); + let current_era = DappsStaking::current_era(); + for era in unregistered_era..current_era { + assert_noop!( + DappsStaking::claim(Origin::signed(developer), contract.clone(), era), + Error::::NotStaked, + ); + } + }) +} + +#[test] +fn claim_one_contract_one_staker() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let developer = 1; + let staker1 = 2; + + // We use a small amount so staked amount is less than rewards + let stake_amount_1 = 50; + let initial_stake = 50; + let contract = MockSmartContract::Evm(H160::repeat_byte(0x01)); + + // Store initial free balaces of the developer and the stakers + let free_balance_staker1 = ::Currency::free_balance(&staker1); + + // Register contracts, bond&stake them with two stakers on the contract. + let start_era = DappsStaking::current_era(); + register_contract(developer, &contract); + let free_developer_balance = ::Currency::free_balance(&developer); + bond_and_stake_with_verification(staker1, &contract, stake_amount_1); + + // Advance some eras to be able to claim rewards. Verify storage is consolidated + advance_to_era(start_era + 1); + let claim_era = DappsStaking::current_era() - 1; + claim_with_verification(staker1, contract, claim_era); + // calculate reward per stakers + let expected_staker1_reward = + calc_expected_staker_reward(claim_era, initial_stake, stake_amount_1); + + // calculate reward per developer + let expected_developer_reward = calc_expected_developer_reward(claim_era, initial_stake); + + // check balances to see if the rewards are paid out + check_rewards_on_balance_and_storage( + &staker1, + free_balance_staker1, + expected_staker1_reward, + ); + check_rewards_on_balance_and_storage( + &developer, + free_developer_balance, + expected_developer_reward, + ); + + let expected_contract_reward = expected_staker1_reward + expected_developer_reward; + check_paidout_rewards_for_contract(&contract, claim_era, expected_contract_reward); + }) +} + +#[test] +fn claim_one_contract_two_stakers() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let developer = 1; + let staker1 = 2; + let staker2 = 3; + + let stake_amount_1 = 400; + let stake_amount_2 = 600; + let initial_stake = stake_amount_1 + stake_amount_2; + let contract = MockSmartContract::Evm(H160::repeat_byte(0x01)); + + // Store initial free balaces of the developer and the stakers + let free_balance_staker1 = ::Currency::free_balance(&staker1); + let free_balance_staker2 = ::Currency::free_balance(&staker2); + + // Register contracts, bond&stake them with two stakers on the contract. + let start_era = DappsStaking::current_era(); + register_contract(developer, &contract); + let free_developer_balance = ::Currency::free_balance(&developer); + bond_and_stake_with_verification(staker1, &contract, stake_amount_1); + bond_and_stake_with_verification(staker2, &contract, stake_amount_2); + + // Advance some eras to be able to claim rewards. Verify storage is consolidated + advance_to_era(start_era + 3); + let claim_era = DappsStaking::current_era() - 1; + claim_with_verification(staker1, contract, claim_era); + + // calculate reward per stakers + let expected_staker1_reward = + calc_expected_staker_reward(claim_era, initial_stake, stake_amount_1); + let expected_staker2_reward = + calc_expected_staker_reward(claim_era, initial_stake, stake_amount_2); + + // calculate reward per developer + let expected_developer_reward = calc_expected_developer_reward(claim_era, initial_stake); + + // check balances to see if the rewards are paid out + check_rewards_on_balance_and_storage( + &staker1, + free_balance_staker1, + expected_staker1_reward, + ); + check_rewards_on_balance_and_storage( + &staker2, + free_balance_staker2, + expected_staker2_reward, + ); + check_rewards_on_balance_and_storage( + &developer, + free_developer_balance, + expected_developer_reward, + ); + let expected_contract_reward = + expected_staker1_reward + expected_staker2_reward + expected_developer_reward; + check_paidout_rewards_for_contract(&contract, claim_era, expected_contract_reward); + }) +} + +#[test] +fn claim_two_contracts_three_stakers_new() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let developer1 = 1; + let developer2 = 10; + + let staker1 = 2; + let staker2 = 3; // will stake on 2 contracts + let staker3 = 4; + + let staker_1_amount = 400; + let staker_2_amount_1 = 600; + let staker_2_amount_2 = 100; + let staker_3_amount = 400; + + let contract1 = MockSmartContract::Evm(H160::repeat_byte(0x01)); + let contract2 = MockSmartContract::Evm(H160::repeat_byte(0x02)); + + // Store initial free balaces of developers and stakers + let free_balance_staker1 = ::Currency::free_balance(&staker1); + let free_balance_staker2 = ::Currency::free_balance(&staker2); + let free_balance_staker3 = ::Currency::free_balance(&staker3); + + // Register 2 contracts, bond&stake with two stakers on first contract. + let start_era = DappsStaking::current_era(); + register_contract(developer1, &contract1); + register_contract(developer2, &contract2); + let free_balance_developer1 = ::Currency::free_balance(&developer1); + let free_balance_developer2 = ::Currency::free_balance(&developer2); + bond_and_stake_with_verification(staker1, &contract1, staker_1_amount); + bond_and_stake_with_verification(staker2, &contract1, staker_2_amount_1); + let contract_1_stake = staker_1_amount + staker_2_amount_1; + let first_claim_era = start_era; + + // Advance eras and then bond&stake with two stakers on second contract. + advance_to_era(start_era + 3); + + bond_and_stake_with_verification(staker2, &contract2, staker_2_amount_2); + bond_and_stake_with_verification(staker3, &contract2, staker_3_amount); + let contract_2_stake = staker_2_amount_2 + staker_3_amount; + + // Advance era again by one, so rewards can be claimed for previous era as well. + let current_era = DappsStaking::current_era(); + let second_claim_era = current_era; + advance_to_era(current_era + 1); + + // Claim first contract rewards for the two prepared eras and verify storage content is as expected. + claim_with_verification(staker1, contract1.clone(), first_claim_era); + claim_with_verification(staker1, contract1.clone(), second_claim_era); + + // Calculate staker1 rewards for the two claimed eras + let expected_c1_staker1_e1_reward = + calc_expected_staker_reward(first_claim_era, contract_1_stake, staker_1_amount); + let expected_c1_staker1_e2_reward = + calc_expected_staker_reward(second_claim_era, contract_1_stake, staker_1_amount); + let expected_c1_staker1_reward_total = + expected_c1_staker1_e1_reward + expected_c1_staker1_e2_reward; + check_rewards_on_balance_and_storage( + &staker1, + free_balance_staker1, + expected_c1_staker1_reward_total, + ); + + // Calculate staker2 rewards for the two claimed eras + let expected_c1_staker2_e1_reward = + calc_expected_staker_reward(first_claim_era, contract_1_stake, staker_2_amount_1); + let expected_c1_staker2_e2_reward = + calc_expected_staker_reward(second_claim_era, contract_1_stake, staker_2_amount_1); + let expected_c1_staker2_reward_total = + expected_c1_staker2_e1_reward + expected_c1_staker2_e2_reward; + check_rewards_on_balance_and_storage( + &staker2, + free_balance_staker2, + expected_c1_staker2_reward_total, + ); + + // Calculate developer1 rewards for the two claimed eras + let expected_c1_dev1_e1_reward = + calc_expected_developer_reward(first_claim_era, contract_1_stake); + let expected_c1_dev1_e2_reward = + calc_expected_developer_reward(second_claim_era, contract_1_stake); + let expected_c1_developer1_reward_total = + expected_c1_dev1_e1_reward + expected_c1_dev1_e2_reward; + check_rewards_on_balance_and_storage( + &developer1, + free_balance_developer1, + expected_c1_developer1_reward_total, + ); + + // Verify total paid out rewards for the claimed eras + let expected_contract1_e1_reward = expected_c1_staker1_e1_reward + + expected_c1_staker2_e1_reward + + expected_c1_dev1_e1_reward; + check_paidout_rewards_for_contract( + &contract1, + first_claim_era, + expected_contract1_e1_reward, + ); + let expected_contract1_e2_reward = expected_c1_staker1_e2_reward + + expected_c1_staker2_e2_reward + + expected_c1_dev1_e2_reward; + check_paidout_rewards_for_contract( + &contract1, + second_claim_era, + expected_contract1_e2_reward, + ); + + claim_with_verification(staker2, contract2.clone(), second_claim_era); + + // Calculate staker 2 rewards for the second contract and a single era + let expected_c2_staker2_e2_reward = + calc_expected_staker_reward(second_claim_era, contract_2_stake, staker_2_amount_2); + check_rewards_on_balance_and_storage( + &staker2, + free_balance_staker2, + expected_c2_staker2_e2_reward + expected_c1_staker2_reward_total, + ); + + // Calculate staker 3 rewards for the second contract and a single era + let expected_c2_staker3_e2_reward = + calc_expected_staker_reward(second_claim_era, contract_2_stake, staker_3_amount); + check_rewards_on_balance_and_storage( + &staker3, + free_balance_staker3, + expected_c2_staker3_e2_reward, + ); + + // Calculate developer2 rewards for the single claimed era + let expected_c2_dev2_e2_reward = + calc_expected_developer_reward(second_claim_era, contract_2_stake); + check_rewards_on_balance_and_storage( + &developer2, + free_balance_developer2, + expected_c2_dev2_e2_reward, + ); + + let expected_contract2_reward = expected_c2_staker2_e2_reward + + expected_c2_staker3_e2_reward + + expected_c2_dev2_e2_reward; + check_paidout_rewards_for_contract(&contract2, second_claim_era, expected_contract2_reward); + }) +} diff --git a/frame/dapps-staking/src/traits.rs b/frame/dapps-staking/src/traits.rs new file mode 100644 index 00000000..009dcff5 --- /dev/null +++ b/frame/dapps-staking/src/traits.rs @@ -0,0 +1,5 @@ +// TODO: document this and sort it out +pub trait IsContract: Default { + /// Used to check whether the struct represents a valid contract or not. + fn is_valid(&self) -> bool; +} diff --git a/frame/dapps-staking/src/weights.rs b/frame/dapps-staking/src/weights.rs new file mode 100644 index 00000000..df7eacc7 --- /dev/null +++ b/frame/dapps-staking/src/weights.rs @@ -0,0 +1,186 @@ + +//! Autogenerated weights for `pallet_dapps_staking` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2021-10-07, STEPS: `20`, REPEAT: 10, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128 + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_staking. +pub trait WeightInfo { + fn register() -> Weight; + /// n - number of existing stakers on the contract + fn unregister(n: u32) -> Weight; + fn enable_developer_pre_approval() -> Weight; + fn developer_pre_approval() -> Weight; + fn bond_and_stake() -> Weight; + fn unbond_unstake_and_withdraw() -> Weight; + /// n - total number of payees + fn claim(n: u32) -> Weight; + fn force_new_era() -> Weight; +} + +/// Weights for pallet_staking using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + // Storage: DappsStaking RegisteredDevelopers (r:1 w:1) + // Storage: DappsStaking RegisteredDapps (r:1 w:1) + // Storage: DappsStaking PreApprovalIsEnabled (r:1 w:0) + fn register() -> Weight { + (63_974_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: DappsStaking RegisteredDevelopers (r:1 w:1) + // Storage: DappsStaking CurrentEra (r:1 w:0) + // Storage: DappsStaking ContractEraStake (r:3 w:0) + // Storage: DappsStaking EraRewardsAndStakes (r:1 w:1) + // Storage: System Account (r:1 w:1) + // Storage: DappsStaking Ledger (r:25 w:25) + // Storage: Balances Locks (r:25 w:25) + fn unregister(n: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 68_000 + .saturating_add((43_004_000 as Weight).saturating_mul(n as Weight)) + .saturating_add(T::DbWeight::get().reads(5 as Weight)) + .saturating_add(T::DbWeight::get().reads((3 as Weight).saturating_mul(n as Weight))) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + .saturating_add(T::DbWeight::get().writes((3 as Weight).saturating_mul(n as Weight))) + } + // Storage: DappsStaking PreApprovalIsEnabled (r:0 w:1) + fn enable_developer_pre_approval() -> Weight { + (3_132_000 as Weight) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: DappsStaking PreApprovedDevelopers (r:1 w:1) + fn developer_pre_approval() -> Weight { + (9_337_000 as Weight) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: DappsStaking RegisteredDapps (r:1 w:0) + // Storage: DappsStaking RegisteredDevelopers (r:1 w:0) + // Storage: DappsStaking Ledger (r:1 w:1) + // Storage: DappsStaking CurrentEra (r:1 w:0) + // Storage: DappsStaking ContractEraStake (r:1 w:1) + // Storage: DappsStaking EraRewardsAndStakes (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + fn bond_and_stake() -> Weight { + (373_299_000 as Weight) + .saturating_add(T::DbWeight::get().reads(7 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + // Storage: DappsStaking RegisteredDapps (r:1 w:0) + // Storage: DappsStaking CurrentEra (r:1 w:0) + // Storage: DappsStaking ContractEraStake (r:3 w:1) + // Storage: DappsStaking Ledger (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + // Storage: DappsStaking EraRewardsAndStakes (r:1 w:1) + fn unbond_unstake_and_withdraw() -> Weight { + (413_106_000 as Weight) + .saturating_add(T::DbWeight::get().reads(8 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + // Storage: DappsStaking RegisteredDapps (r:1 w:0) + // Storage: DappsStaking CurrentEra (r:1 w:0) + // Storage: DappsStaking ContractEraStake (r:1 w:1) + // Storage: DappsStaking EraRewardsAndStakes (r:1 w:0) + // Storage: Balances TotalIssuance (r:1 w:1) + fn claim(n: u32, ) -> Weight { + (76_835_000 as Weight) + // Standard Error: 30_000 + .saturating_add((8_684_000 as Weight).saturating_mul(n as Weight)) + .saturating_add(T::DbWeight::get().reads(5 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: DappsStaking ForceEra (r:0 w:1) + fn force_new_era() -> Weight { + (3_306_000 as Weight) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + // Storage: DappsStaking RegisteredDevelopers (r:1 w:1) + // Storage: DappsStaking RegisteredDapps (r:1 w:1) + // Storage: DappsStaking PreApprovalIsEnabled (r:1 w:0) + fn register() -> Weight { + (63_974_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: DappsStaking RegisteredDevelopers (r:1 w:1) + // Storage: DappsStaking CurrentEra (r:1 w:0) + // Storage: DappsStaking ContractEraStake (r:3 w:0) + // Storage: DappsStaking EraRewardsAndStakes (r:1 w:1) + // Storage: System Account (r:1 w:1) + // Storage: DappsStaking Ledger (r:25 w:25) + // Storage: Balances Locks (r:25 w:25) + fn unregister(n: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 68_000 + .saturating_add((43_004_000 as Weight).saturating_mul(n as Weight)) + .saturating_add(RocksDbWeight::get().reads(5 as Weight)) + .saturating_add(RocksDbWeight::get().reads((3 as Weight).saturating_mul(n as Weight))) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes((3 as Weight).saturating_mul(n as Weight))) + } + // Storage: DappsStaking PreApprovalIsEnabled (r:0 w:1) + fn enable_developer_pre_approval() -> Weight { + (3_132_000 as Weight) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: DappsStaking PreApprovedDevelopers (r:1 w:1) + fn developer_pre_approval() -> Weight { + (9_337_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: DappsStaking RegisteredDapps (r:1 w:0) + // Storage: DappsStaking RegisteredDevelopers (r:1 w:0) + // Storage: DappsStaking Ledger (r:1 w:1) + // Storage: DappsStaking CurrentEra (r:1 w:0) + // Storage: DappsStaking ContractEraStake (r:1 w:1) + // Storage: DappsStaking EraRewardsAndStakes (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + fn bond_and_stake() -> Weight { + (373_299_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(7 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + } + // Storage: DappsStaking RegisteredDapps (r:1 w:0) + // Storage: DappsStaking CurrentEra (r:1 w:0) + // Storage: DappsStaking ContractEraStake (r:3 w:1) + // Storage: DappsStaking Ledger (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + // Storage: DappsStaking EraRewardsAndStakes (r:1 w:1) + fn unbond_unstake_and_withdraw() -> Weight { + (413_106_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(8 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + } + // Storage: DappsStaking RegisteredDapps (r:1 w:0) + // Storage: DappsStaking CurrentEra (r:1 w:0) + // Storage: DappsStaking ContractEraStake (r:1 w:1) + // Storage: DappsStaking EraRewardsAndStakes (r:1 w:0) + // Storage: Balances TotalIssuance (r:1 w:1) + fn claim(n: u32, ) -> Weight { + (76_835_000 as Weight) + // Standard Error: 30_000 + .saturating_add((8_684_000 as Weight).saturating_mul(n as Weight)) + .saturating_add(RocksDbWeight::get().reads(5 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: DappsStaking ForceEra (r:0 w:1) + fn force_new_era() -> Weight { + (3_306_000 as Weight) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } +} diff --git a/frame/vesting/Cargo.toml b/frame/vesting/Cargo.toml new file mode 100644 index 00000000..9426af16 --- /dev/null +++ b/frame/vesting/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "pallet-vesting" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME pallet for manage vesting" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = [ + "derive", +] } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false, optional = true } +log = { version = "0.4.0", default-features = false } + +[dev-dependencies] +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } +pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } + +[features] +default = ["std"] +std = [ + "codec/std", + "scale-info/std", + "sp-std/std", + "sp-runtime/std", + "frame-support/std", + "frame-system/std", +] +runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/vesting/README.md b/frame/vesting/README.md new file mode 100644 index 00000000..c3800eb9 --- /dev/null +++ b/frame/vesting/README.md @@ -0,0 +1,31 @@ +# Vesting Module + +- [`vesting::Config`](https://docs.rs/pallet-vesting/latest/pallet_vesting/trait.Config.html) +- [`Call`](https://docs.rs/pallet-vesting/latest/pallet_vesting/enum.Call.html) + +## Overview + +A simple module providing a means of placing a linear curve on an account's locked balance. This +module ensures that there is a lock in place preventing the balance to drop below the *unvested* +amount for any reason other than transaction fee payment. + +As the amount vested increases over time, the amount unvested reduces. However, locks remain in +place and explicit action is needed on behalf of the user to ensure that the amount locked is +equivalent to the amount remaining to be vested. This is done through a dispatchable function, +either `vest` (in typical case where the sender is calling on their own behalf) or `vest_other` +in case the sender is calling on another account's behalf. + +## Interface + +This module implements the `VestingSchedule` trait. + +### Dispatchable Functions + +- `vest` - Update the lock, reducing it in line with the amount "vested" so far. +- `vest_other` - Update the lock of another account, reducing it in line with the amount + "vested" so far. + +[`Call`]: ./enum.Call.html +[`Config`]: ./trait.Config.html + +License: Apache-2.0 diff --git a/frame/vesting/src/benchmarking.rs b/frame/vesting/src/benchmarking.rs new file mode 100644 index 00000000..d1b84408 --- /dev/null +++ b/frame/vesting/src/benchmarking.rs @@ -0,0 +1,383 @@ +// This file is part of Substrate. + +// Copyright (C) 2020-2021 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. + +//! Vesting pallet benchmarking. + +#![cfg(feature = "runtime-benchmarks")] + +use frame_benchmarking::{account, benchmarks, whitelisted_caller}; +use frame_support::assert_ok; +use frame_system::{Pallet as System, RawOrigin}; +use sp_runtime::traits::{Bounded, CheckedDiv, CheckedMul}; + +use super::*; +use crate::Pallet as Vesting; + +const SEED: u32 = 0; + +type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + +fn add_locks(who: &T::AccountId, n: u8) { + for id in 0..n { + let lock_id = [id; 8]; + let locked = 256u32; + let reasons = WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE; + T::Currency::set_lock(lock_id, who, locked.into(), reasons); + } +} + +fn add_vesting_schedules( + target: ::Source, + n: u32, +) -> Result, &'static str> { + let min_transfer = T::MinVestedTransfer::get(); + let locked = min_transfer.checked_mul(&20u32.into()).unwrap(); + // Schedule has a duration of 20. + let per_block = min_transfer; + let starting_block = 1u32; + + let source: T::AccountId = account("source", 0, SEED); + let source_lookup: ::Source = T::Lookup::unlookup(source.clone()); + T::Currency::make_free_balance_be(&source, BalanceOf::::max_value()); + + System::::set_block_number(T::BlockNumber::zero()); + + let mut total_locked: BalanceOf = Zero::zero(); + for _ in 0..n { + total_locked += locked; + + let schedule = VestingInfo::new(locked, per_block, starting_block.into()); + assert_ok!(Vesting::::do_vested_transfer( + source_lookup.clone(), + target.clone(), + schedule + )); + + // Top up to guarantee we can always transfer another schedule. + T::Currency::make_free_balance_be(&source, BalanceOf::::max_value()); + } + + Ok(total_locked.into()) +} + +benchmarks! { + vest_locked { + let l in 0 .. MaxLocksOf::::get() - 1; + let s in 1 .. T::MAX_VESTING_SCHEDULES; + + let caller: T::AccountId = whitelisted_caller(); + let caller_lookup: ::Source = T::Lookup::unlookup(caller.clone()); + T::Currency::make_free_balance_be(&caller, T::Currency::minimum_balance()); + + add_locks::(&caller, l as u8); + let expected_balance = add_vesting_schedules::(caller_lookup, s)?; + + // At block zero, everything is vested. + assert_eq!(System::::block_number(), T::BlockNumber::zero()); + assert_eq!( + Vesting::::vesting_balance(&caller), + Some(expected_balance.into()), + "Vesting schedule not added", + ); + }: vest(RawOrigin::Signed(caller.clone())) + verify { + // Nothing happened since everything is still vested. + assert_eq!( + Vesting::::vesting_balance(&caller), + Some(expected_balance.into()), + "Vesting schedule was removed", + ); + } + + vest_unlocked { + let l in 0 .. MaxLocksOf::::get() - 1; + let s in 1 .. T::MAX_VESTING_SCHEDULES; + + let caller: T::AccountId = whitelisted_caller(); + let caller_lookup: ::Source = T::Lookup::unlookup(caller.clone()); + T::Currency::make_free_balance_be(&caller, T::Currency::minimum_balance()); + + add_locks::(&caller, l as u8); + add_vesting_schedules::(caller_lookup, s)?; + + // At block 21, everything is unlocked. + System::::set_block_number(21u32.into()); + assert_eq!( + Vesting::::vesting_balance(&caller), + Some(BalanceOf::::zero()), + "Vesting schedule still active", + ); + }: vest(RawOrigin::Signed(caller.clone())) + verify { + // Vesting schedule is removed! + assert_eq!( + Vesting::::vesting_balance(&caller), + None, + "Vesting schedule was not removed", + ); + } + + vest_other_locked { + let l in 0 .. MaxLocksOf::::get() - 1; + let s in 1 .. T::MAX_VESTING_SCHEDULES; + + let other: T::AccountId = account("other", 0, SEED); + let other_lookup: ::Source = T::Lookup::unlookup(other.clone()); + + add_locks::(&other, l as u8); + let expected_balance = add_vesting_schedules::(other_lookup.clone(), s)?; + + // At block zero, everything is vested. + assert_eq!(System::::block_number(), T::BlockNumber::zero()); + assert_eq!( + Vesting::::vesting_balance(&other), + Some(expected_balance), + "Vesting schedule not added", + ); + + let caller: T::AccountId = whitelisted_caller(); + }: vest_other(RawOrigin::Signed(caller.clone()), other_lookup) + verify { + // Nothing happened since everything is still vested. + assert_eq!( + Vesting::::vesting_balance(&other), + Some(expected_balance.into()), + "Vesting schedule was removed", + ); + } + + vest_other_unlocked { + let l in 0 .. MaxLocksOf::::get() - 1; + let s in 1 .. T::MAX_VESTING_SCHEDULES; + + let other: T::AccountId = account("other", 0, SEED); + let other_lookup: ::Source = T::Lookup::unlookup(other.clone()); + + add_locks::(&other, l as u8); + add_vesting_schedules::(other_lookup.clone(), s)?; + // At block 21 everything is unlocked. + System::::set_block_number(21u32.into()); + + assert_eq!( + Vesting::::vesting_balance(&other), + Some(BalanceOf::::zero()), + "Vesting schedule still active", + ); + + let caller: T::AccountId = whitelisted_caller(); + }: vest_other(RawOrigin::Signed(caller.clone()), other_lookup) + verify { + // Vesting schedule is removed. + assert_eq!( + Vesting::::vesting_balance(&other), + None, + "Vesting schedule was not removed", + ); + } + + vested_transfer { + let l in 0 .. MaxLocksOf::::get() - 1; + let s in 0 .. T::MAX_VESTING_SCHEDULES - 1; + + let caller: T::AccountId = whitelisted_caller(); + T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); + + let target: T::AccountId = account("target", 0, SEED); + let target_lookup: ::Source = T::Lookup::unlookup(target.clone()); + // Give target existing locks + add_locks::(&target, l as u8); + // Add one vesting schedules. + let mut expected_balance = add_vesting_schedules::(target_lookup.clone(), s)?; + + let transfer_amount = T::MinVestedTransfer::get(); + let per_block = transfer_amount.checked_div(&20u32.into()).unwrap(); + expected_balance += transfer_amount; + + let vesting_schedule = VestingInfo::new( + transfer_amount, + per_block, + 1u32.into(), + ); + }: _(RawOrigin::Signed(caller), target_lookup, vesting_schedule) + verify { + assert_eq!( + expected_balance, + T::Currency::free_balance(&target), + "Transfer didn't happen", + ); + assert_eq!( + Vesting::::vesting_balance(&target), + Some(expected_balance), + "Lock not correctly updated", + ); + } + + force_vested_transfer { + let l in 0 .. MaxLocksOf::::get() - 1; + let s in 0 .. T::MAX_VESTING_SCHEDULES - 1; + + let source: T::AccountId = account("source", 0, SEED); + let source_lookup: ::Source = T::Lookup::unlookup(source.clone()); + T::Currency::make_free_balance_be(&source, BalanceOf::::max_value()); + + let target: T::AccountId = account("target", 0, SEED); + let target_lookup: ::Source = T::Lookup::unlookup(target.clone()); + // Give target existing locks + add_locks::(&target, l as u8); + // Add one less than max vesting schedules + let mut expected_balance = add_vesting_schedules::(target_lookup.clone(), s)?; + + let transfer_amount = T::MinVestedTransfer::get(); + let per_block = transfer_amount.checked_div(&20u32.into()).unwrap(); + expected_balance += transfer_amount; + + let vesting_schedule = VestingInfo::new( + transfer_amount, + per_block, + 1u32.into(), + ); + }: _(RawOrigin::Root, source_lookup, target_lookup, vesting_schedule) + verify { + assert_eq!( + expected_balance, + T::Currency::free_balance(&target), + "Transfer didn't happen", + ); + assert_eq!( + Vesting::::vesting_balance(&target), + Some(expected_balance.into()), + "Lock not correctly updated", + ); + } + + not_unlocking_merge_schedules { + let l in 0 .. MaxLocksOf::::get() - 1; + let s in 2 .. T::MAX_VESTING_SCHEDULES; + + let caller: T::AccountId = account("caller", 0, SEED); + let caller_lookup: ::Source = T::Lookup::unlookup(caller.clone()); + // Give target existing locks. + add_locks::(&caller, l as u8); + // Add max vesting schedules. + let expected_balance = add_vesting_schedules::(caller_lookup.clone(), s)?; + + // Schedules are not vesting at block 0. + assert_eq!(System::::block_number(), T::BlockNumber::zero()); + assert_eq!( + Vesting::::vesting_balance(&caller), + Some(expected_balance), + "Vesting balance should equal sum locked of all schedules", + ); + assert_eq!( + Vesting::::vesting(&caller).unwrap().len(), + s as usize, + "There should be exactly max vesting schedules" + ); + }: merge_schedules(RawOrigin::Signed(caller.clone()), 0, s - 1) + verify { + let expected_schedule = VestingInfo::new( + T::MinVestedTransfer::get() * 20u32.into() * 2u32.into(), + T::MinVestedTransfer::get() * 2u32.into(), + 1u32.into(), + ); + let expected_index = (s - 2) as usize; + assert_eq!( + Vesting::::vesting(&caller).unwrap()[expected_index], + expected_schedule + ); + assert_eq!( + Vesting::::vesting_balance(&caller), + Some(expected_balance), + "Vesting balance should equal total locked of all schedules", + ); + assert_eq!( + Vesting::::vesting(&caller).unwrap().len(), + (s - 1) as usize, + "Schedule count should reduce by 1" + ); + } + + unlocking_merge_schedules { + let l in 0 .. MaxLocksOf::::get() - 1; + let s in 2 .. T::MAX_VESTING_SCHEDULES; + + // Destination used just for currency transfers in asserts. + let test_dest: T::AccountId = account("test_dest", 0, SEED); + + let caller: T::AccountId = account("caller", 0, SEED); + let caller_lookup: ::Source = T::Lookup::unlookup(caller.clone()); + // Give target other locks. + add_locks::(&caller, l as u8); + // Add max vesting schedules. + let total_transferred = add_vesting_schedules::(caller_lookup.clone(), s)?; + + // Go to about half way through all the schedules duration. (They all start at 1, and have a duration of 20 or 21). + System::::set_block_number(11u32.into()); + // We expect half the original locked balance (+ any remainder that vests on the last block). + let expected_balance = total_transferred / 2u32.into(); + assert_eq!( + Vesting::::vesting_balance(&caller), + Some(expected_balance), + "Vesting balance should reflect that we are half way through all schedules duration", + ); + assert_eq!( + Vesting::::vesting(&caller).unwrap().len(), + s as usize, + "There should be exactly max vesting schedules" + ); + // The balance is not actually transferable because it has not been unlocked. + assert!(T::Currency::transfer(&caller, &test_dest, expected_balance, ExistenceRequirement::AllowDeath).is_err()); + }: merge_schedules(RawOrigin::Signed(caller.clone()), 0, s - 1) + verify { + let expected_schedule = VestingInfo::new( + T::MinVestedTransfer::get() * 2u32.into() * 10u32.into(), + T::MinVestedTransfer::get() * 2u32.into(), + 11u32.into(), + ); + let expected_index = (s - 2) as usize; + assert_eq!( + Vesting::::vesting(&caller).unwrap()[expected_index], + expected_schedule, + "New schedule is properly created and placed" + ); + assert_eq!( + Vesting::::vesting(&caller).unwrap()[expected_index], + expected_schedule + ); + assert_eq!( + Vesting::::vesting_balance(&caller), + Some(expected_balance), + "Vesting balance should equal half total locked of all schedules", + ); + assert_eq!( + Vesting::::vesting(&caller).unwrap().len(), + (s - 1) as usize, + "Schedule count should reduce by 1" + ); + // Since merge unlocks all schedules we can now transfer the balance. + assert_ok!( + T::Currency::transfer(&caller, &test_dest, expected_balance, ExistenceRequirement::AllowDeath) + ); + } + + impl_benchmark_test_suite!( + Vesting, + crate::mock::ExtBuilder::default().existential_deposit(256).build(), + crate::mock::Test, + ); +} diff --git a/frame/vesting/src/lib.rs b/frame/vesting/src/lib.rs new file mode 100644 index 00000000..d0c70ee1 --- /dev/null +++ b/frame/vesting/src/lib.rs @@ -0,0 +1,853 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2021 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. + +//! # Vesting Pallet +//! +//! - [`Config`] +//! - [`Call`] +//! +//! ## Overview +//! +//! A simple pallet providing a means of placing a linear curve on an account's locked balance. This +//! pallet ensures that there is a lock in place preventing the balance to drop below the *unvested* +//! amount for any reason other than transaction fee payment. +//! +//! As the amount vested increases over time, the amount unvested reduces. However, locks remain in +//! place and explicit action is needed on behalf of the user to ensure that the amount locked is +//! equivalent to the amount remaining to be vested. This is done through a dispatchable function, +//! either `vest` (in typical case where the sender is calling on their own behalf) or `vest_other` +//! in case the sender is calling on another account's behalf. +//! +//! ## Interface +//! +//! This pallet implements the `VestingSchedule` trait. +//! +//! ### Dispatchable Functions +//! +//! - `vest` - Update the lock, reducing it in line with the amount "vested" so far. +//! - `vest_other` - Update the lock of another account, reducing it in line with the amount +//! "vested" so far. + +#![cfg_attr(not(feature = "std"), no_std)] + +mod benchmarking; +mod migrations; +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; +mod vesting_info; + +pub mod weights; + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{ + ensure, + pallet_prelude::*, + traits::{ + Currency, ExistenceRequirement, Get, LockIdentifier, LockableCurrency, VestingSchedule, + WithdrawReasons, + }, +}; +use frame_system::{ensure_root, ensure_signed, pallet_prelude::*}; +pub use pallet::*; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{ + AtLeast32BitUnsigned, Bounded, Convert, MaybeSerializeDeserialize, One, Saturating, + StaticLookup, Zero, + }, + RuntimeDebug, +}; +use sp_std::{convert::TryInto, fmt::Debug, prelude::*}; +pub use vesting_info::*; +pub use weights::WeightInfo; + +type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; +type MaxLocksOf = + <::Currency as LockableCurrency<::AccountId>>::MaxLocks; + +const VESTING_ID: LockIdentifier = *b"vesting "; + +// A value placed in storage that represents the current version of the Vesting storage. +// This value is used by `on_runtime_upgrade` to determine whether we run storage migration logic. +#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +enum Releases { + V0, + V1, +} + +impl Default for Releases { + fn default() -> Self { + Releases::V0 + } +} + +/// Actions to take against a user's `Vesting` storage entry. +#[derive(Clone, Copy)] +enum VestingAction { + /// Do not actively remove any schedules. + Passive, + /// Remove the schedule specified by the index. + Remove { index: usize }, + /// Remove the two schedules, specified by index, so they can be merged. + Merge { index1: usize, index2: usize }, +} + +impl VestingAction { + /// Whether or not the filter says the schedule index should be removed. + fn should_remove(&self, index: usize) -> bool { + match self { + Self::Passive => false, + Self::Remove { index: index1 } => *index1 == index, + Self::Merge { index1, index2 } => *index1 == index || *index2 == index, + } + } + + /// Pick the schedules that this action dictates should continue vesting undisturbed. + fn pick_schedules<'a, T: Config>( + &'a self, + schedules: Vec, T::BlockNumber>>, + ) -> impl Iterator, T::BlockNumber>> + 'a { + schedules + .into_iter() + .enumerate() + .filter_map(move |(index, schedule)| { + if self.should_remove(index) { + None + } else { + Some(schedule) + } + }) + } +} + +// Wrapper for `T::MAX_VESTING_SCHEDULES` to satisfy `trait Get`. +pub struct MaxVestingSchedulesGet(PhantomData); +impl Get for MaxVestingSchedulesGet { + fn get() -> u32 { + T::MAX_VESTING_SCHEDULES + } +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type Event: From> + IsType<::Event>; + + /// The currency trait. + type Currency: LockableCurrency; + + /// Convert the block number into a balance. + type BlockNumberToBalance: Convert>; + + /// The minimum amount transferred to call `vested_transfer`. + #[pallet::constant] + type MinVestedTransfer: Get>; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + + /// Maximum number of vesting schedules an account may have at a given moment. + const MAX_VESTING_SCHEDULES: u32; + } + + #[pallet::extra_constants] + impl Pallet { + // TODO: rename to snake case after https://github.com/paritytech/substrate/issues/8826 fixed. + #[allow(non_snake_case)] + fn MaxVestingSchedules() -> u32 { + T::MAX_VESTING_SCHEDULES + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result<(), &'static str> { + if StorageVersion::::get() == Releases::V0 { + migrations::v1::pre_migrate::() + } else { + Ok(()) + } + } + + fn on_runtime_upgrade() -> Weight { + if StorageVersion::::get() == Releases::V0 { + StorageVersion::::put(Releases::V1); + migrations::v1::migrate::().saturating_add(T::DbWeight::get().reads_writes(1, 1)) + } else { + T::DbWeight::get().reads(1) + } + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade() -> Result<(), &'static str> { + migrations::v1::post_migrate::() + } + + fn integrity_test() { + assert!( + T::MAX_VESTING_SCHEDULES > 0, + "`MaxVestingSchedules` must ge greater than 0" + ); + } + } + + /// Information regarding the vesting of a given account. + #[pallet::storage] + #[pallet::getter(fn vesting)] + pub type Vesting = StorageMap< + _, + Blake2_128Concat, + T::AccountId, + BoundedVec, T::BlockNumber>, MaxVestingSchedulesGet>, + >; + + /// Storage version of the pallet. + /// + /// New networks start with latest version, as determined by the genesis build. + #[pallet::storage] + pub(crate) type StorageVersion = StorageValue<_, Releases, ValueQuery>; + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + #[pallet::generate_storage_info] + pub struct Pallet(_); + + #[pallet::genesis_config] + pub struct GenesisConfig { + pub vesting: Vec<(T::AccountId, T::BlockNumber, T::BlockNumber, BalanceOf)>, + } + + #[cfg(feature = "std")] + impl Default for GenesisConfig { + fn default() -> Self { + GenesisConfig { + vesting: Default::default(), + } + } + } + + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) { + use sp_runtime::traits::Saturating; + + // Genesis uses the latest storage version. + StorageVersion::::put(Releases::V1); + + // Generate initial vesting configuration + // * who - Account which we are generating vesting configuration for + // * begin - Block when the account will start to vest + // * length - Number of blocks from `begin` until fully vested + // * liquid - Number of units which can be spent before vesting begins + for &(ref who, begin, length, liquid) in self.vesting.iter() { + let balance = T::Currency::free_balance(who); + assert!( + !balance.is_zero(), + "Currencies must be init'd before vesting" + ); + // Total genesis `balance` minus `liquid` equals funds locked for vesting + let locked = balance.saturating_sub(liquid); + let length_as_balance = T::BlockNumberToBalance::convert(length); + let per_block = locked / length_as_balance.max(sp_runtime::traits::One::one()); + let vesting_info = VestingInfo::new(locked, per_block, begin); + if !vesting_info.is_valid() { + panic!("Invalid VestingInfo params at genesis") + }; + + Vesting::::try_append(who, vesting_info) + .expect("Too many vesting schedules at genesis."); + + let reasons = WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE; + T::Currency::set_lock(VESTING_ID, who, locked, reasons); + } + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// The amount vested has been updated. This could indicate a change in funds available. + /// The balance given is the amount which is left unvested (and thus locked). + VestingUpdated { + account: T::AccountId, + unvested: BalanceOf, + }, + /// An \[account\] has become fully vested. + VestingCompleted { account: T::AccountId }, + } + + /// Error for the vesting pallet. + #[pallet::error] + pub enum Error { + /// The account given is not vesting. + NotVesting, + /// The account already has `MaxVestingSchedules` count of schedules and thus + /// cannot add another one. Consider merging existing schedules in order to add another. + AtMaxVestingSchedules, + /// Amount being transferred is too low to create a vesting schedule. + AmountLow, + /// An index was out of bounds of the vesting schedules. + ScheduleIndexOutOfBounds, + /// Failed to create a new schedule because some parameter was invalid. + InvalidScheduleParams, + } + + #[pallet::call] + impl Pallet { + /// Unlock any vested funds of the sender account. + /// + /// The dispatch origin for this call must be _Signed_ and the sender must have funds still + /// locked under this pallet. + /// + /// Emits either `VestingCompleted` or `VestingUpdated`. + /// + /// # + /// - `O(1)`. + /// - DbWeight: 2 Reads, 2 Writes + /// - Reads: Vesting Storage, Balances Locks, [Sender Account] + /// - Writes: Vesting Storage, Balances Locks, [Sender Account] + /// # + #[pallet::weight(T::WeightInfo::vest_locked(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) + .max(T::WeightInfo::vest_unlocked(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES)) + )] + pub fn vest(origin: OriginFor) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_vest(who) + } + + /// Unlock any vested funds of a `target` account. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// - `target`: The account whose vested funds should be unlocked. Must have funds still + /// locked under this pallet. + /// + /// Emits either `VestingCompleted` or `VestingUpdated`. + /// + /// # + /// - `O(1)`. + /// - DbWeight: 3 Reads, 3 Writes + /// - Reads: Vesting Storage, Balances Locks, Target Account + /// - Writes: Vesting Storage, Balances Locks, Target Account + /// # + #[pallet::weight(T::WeightInfo::vest_other_locked(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) + .max(T::WeightInfo::vest_other_unlocked(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES)) + )] + pub fn vest_other( + origin: OriginFor, + target: ::Source, + ) -> DispatchResult { + ensure_signed(origin)?; + let who = T::Lookup::lookup(target)?; + Self::do_vest(who) + } + + /// Create a vested transfer. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// - `target`: The account receiving the vested funds. + /// - `schedule`: The vesting schedule attached to the transfer. + /// + /// Emits `VestingCreated`. + /// + /// NOTE: This will unlock all schedules through the current block. + /// + /// # + /// - `O(1)`. + /// - DbWeight: 3 Reads, 3 Writes + /// - Reads: Vesting Storage, Balances Locks, Target Account, [Sender Account] + /// - Writes: Vesting Storage, Balances Locks, Target Account, [Sender Account] + /// # + #[pallet::weight( + T::WeightInfo::vested_transfer(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) + )] + pub fn vested_transfer( + origin: OriginFor, + target: ::Source, + schedule: VestingInfo, T::BlockNumber>, + ) -> DispatchResult { + let transactor = ensure_signed(origin)?; + let transactor = ::unlookup(transactor); + Self::do_vested_transfer(transactor, target, schedule) + } + + /// Force a vested transfer. + /// + /// The dispatch origin for this call must be _Root_. + /// + /// - `source`: The account whose funds should be transferred. + /// - `target`: The account that should be transferred the vested funds. + /// - `schedule`: The vesting schedule attached to the transfer. + /// + /// Emits `VestingCreated`. + /// + /// NOTE: This will unlock all schedules through the current block. + /// + /// # + /// - `O(1)`. + /// - DbWeight: 4 Reads, 4 Writes + /// - Reads: Vesting Storage, Balances Locks, Target Account, Source Account + /// - Writes: Vesting Storage, Balances Locks, Target Account, Source Account + /// # + #[pallet::weight( + T::WeightInfo::force_vested_transfer(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) + )] + pub fn force_vested_transfer( + origin: OriginFor, + source: ::Source, + target: ::Source, + schedule: VestingInfo, T::BlockNumber>, + ) -> DispatchResult { + ensure_root(origin)?; + Self::do_vested_transfer(source, target, schedule) + } + + /// Force update vesting schedule. + /// + /// The dispatch origin for this call must be _Root_. + /// + /// - `schedule_index`: index of the schedule to update. + /// - `schedule`: The vesting schedule attached to the transfer. + /// + /// # + /// - `O(1)`. + /// - DbWeight: 1 Writes + /// - Writes: Vesting Storage + /// # + #[pallet::weight( + T::WeightInfo::force_vested_transfer(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) + )] + pub fn force_update_schedules( + origin: OriginFor, + target: ::Source, + schedules: BoundedVec< + VestingInfo, T::BlockNumber>, + MaxVestingSchedulesGet, + >, + ) -> DispatchResult { + ensure_root(origin)?; + let who = T::Lookup::lookup(target)?; + >::insert(&who, schedules); + Ok(()) + } + + /// Merge two vesting schedules together, creating a new vesting schedule that unlocks over + /// the highest possible start and end blocks. If both schedules have already started the + /// current block will be used as the schedule start; with the caveat that if one schedule + /// is finished by the current block, the other will be treated as the new merged schedule, + /// unmodified. + /// + /// NOTE: If `schedule1_index == schedule2_index` this is a no-op. + /// NOTE: This will unlock all schedules through the current block prior to merging. + /// NOTE: If both schedules have ended by the current block, no new schedule will be created + /// and both will be removed. + /// + /// Merged schedule attributes: + /// - `starting_block`: `MAX(schedule1.starting_block, scheduled2.starting_block, + /// current_block)`. + /// - `ending_block`: `MAX(schedule1.ending_block, schedule2.ending_block)`. + /// - `locked`: `schedule1.locked_at(current_block) + schedule2.locked_at(current_block)`. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// - `schedule1_index`: index of the first schedule to merge. + /// - `schedule2_index`: index of the second schedule to merge. + #[pallet::weight( + T::WeightInfo::not_unlocking_merge_schedules(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) + .max(T::WeightInfo::unlocking_merge_schedules(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES)) + )] + pub fn merge_schedules( + origin: OriginFor, + schedule1_index: u32, + schedule2_index: u32, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + if schedule1_index == schedule2_index { + return Ok(()); + }; + let schedule1_index = schedule1_index as usize; + let schedule2_index = schedule2_index as usize; + + let schedules = Self::vesting(&who).ok_or(Error::::NotVesting)?; + let merge_action = VestingAction::Merge { + index1: schedule1_index, + index2: schedule2_index, + }; + + let (schedules, locked_now) = Self::exec_action(schedules.to_vec(), merge_action)?; + + Self::write_vesting(&who, schedules)?; + Self::write_lock(&who, locked_now); + + Ok(()) + } + } +} + +impl Pallet { + // Create a new `VestingInfo`, based off of two other `VestingInfo`s. + // NOTE: We assume both schedules have had funds unlocked up through the current block. + fn merge_vesting_info( + now: T::BlockNumber, + schedule1: VestingInfo, T::BlockNumber>, + schedule2: VestingInfo, T::BlockNumber>, + ) -> Option, T::BlockNumber>> { + let schedule1_ending_block = schedule1.ending_block_as_balance::(); + let schedule2_ending_block = schedule2.ending_block_as_balance::(); + let now_as_balance = T::BlockNumberToBalance::convert(now); + + // Check if one or both schedules have ended. + match ( + schedule1_ending_block <= now_as_balance, + schedule2_ending_block <= now_as_balance, + ) { + // If both schedules have ended, we don't merge and exit early. + (true, true) => return None, + // If one schedule has ended, we treat the one that has not ended as the new + // merged schedule. + (true, false) => return Some(schedule2), + (false, true) => return Some(schedule1), + // If neither schedule has ended don't exit early. + _ => {} + } + + let locked = schedule1 + .locked_at::(now) + .saturating_add(schedule2.locked_at::(now)); + // This shouldn't happen because we know at least one ending block is greater than now, + // thus at least a schedule a some locked balance. + debug_assert!( + !locked.is_zero(), + "merge_vesting_info validation checks failed to catch a locked of 0" + ); + + let ending_block = schedule1_ending_block.max(schedule2_ending_block); + let starting_block = now + .max(schedule1.starting_block()) + .max(schedule2.starting_block()); + + let per_block = { + let duration = ending_block + .saturating_sub(T::BlockNumberToBalance::convert(starting_block)) + .max(One::one()); + (locked / duration).max(One::one()) + }; + + let schedule = VestingInfo::new(locked, per_block, starting_block); + debug_assert!( + schedule.is_valid(), + "merge_vesting_info schedule validation check failed" + ); + + Some(schedule) + } + + // Execute a vested transfer from `source` to `target` with the given `schedule`. + fn do_vested_transfer( + source: ::Source, + target: ::Source, + schedule: VestingInfo, T::BlockNumber>, + ) -> DispatchResult { + // Validate user inputs. + ensure!( + schedule.locked() >= T::MinVestedTransfer::get(), + Error::::AmountLow + ); + if !schedule.is_valid() { + return Err(Error::::InvalidScheduleParams.into()); + }; + let target = T::Lookup::lookup(target)?; + let source = T::Lookup::lookup(source)?; + + // Check we can add to this account prior to any storage writes. + Self::can_add_vesting_schedule( + &target, + schedule.locked(), + schedule.per_block(), + schedule.starting_block(), + )?; + + T::Currency::transfer( + &source, + &target, + schedule.locked(), + ExistenceRequirement::AllowDeath, + )?; + + // We can't let this fail because the currency transfer has already happened. + let res = Self::add_vesting_schedule( + &target, + schedule.locked(), + schedule.per_block(), + schedule.starting_block(), + ); + debug_assert!( + res.is_ok(), + "Failed to add a schedule when we had to succeed." + ); + + Ok(()) + } + + /// Iterate through the schedules to track the current locked amount and + /// filter out completed and specified schedules. + /// + /// Returns a tuple that consists of: + /// - Vec of vesting schedules, where completed schedules and those specified + /// by filter are removed. (Note the vec is not checked for respecting + /// bounded length.) + /// - The amount locked at the current block number based on the given schedules. + /// + /// NOTE: the amount locked does not include any schedules that are filtered out via `action`. + fn report_schedule_updates( + schedules: Vec, T::BlockNumber>>, + action: VestingAction, + ) -> (Vec, T::BlockNumber>>, BalanceOf) { + let now = >::block_number(); + + let mut total_locked_now: BalanceOf = Zero::zero(); + let filtered_schedules = action + .pick_schedules::(schedules) + .filter_map(|schedule| { + let locked_now = schedule.locked_at::(now); + if locked_now.is_zero() { + None + } else { + total_locked_now = total_locked_now.saturating_add(locked_now); + Some(schedule) + } + }) + .collect::>(); + + (filtered_schedules, total_locked_now) + } + + /// Write an accounts updated vesting lock to storage. + fn write_lock(who: &T::AccountId, total_locked_now: BalanceOf) { + if total_locked_now.is_zero() { + T::Currency::remove_lock(VESTING_ID, who); + Self::deposit_event(Event::::VestingCompleted { + account: who.clone(), + }); + } else { + let reasons = WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE; + T::Currency::set_lock(VESTING_ID, who, total_locked_now, reasons); + Self::deposit_event(Event::::VestingUpdated { + account: who.clone(), + unvested: total_locked_now, + }); + }; + } + + /// Write an accounts updated vesting schedules to storage. + fn write_vesting( + who: &T::AccountId, + schedules: Vec, T::BlockNumber>>, + ) -> Result<(), DispatchError> { + let schedules: BoundedVec< + VestingInfo, T::BlockNumber>, + MaxVestingSchedulesGet, + > = schedules + .try_into() + .map_err(|_| Error::::AtMaxVestingSchedules)?; + + if schedules.len() == 0 { + Vesting::::remove(&who); + } else { + Vesting::::insert(who, schedules) + } + + Ok(()) + } + + /// Unlock any vested funds of `who`. + fn do_vest(who: T::AccountId) -> DispatchResult { + let schedules = Self::vesting(&who).ok_or(Error::::NotVesting)?; + + let (schedules, locked_now) = + Self::exec_action(schedules.to_vec(), VestingAction::Passive)?; + + Self::write_vesting(&who, schedules)?; + Self::write_lock(&who, locked_now); + + Ok(()) + } + + /// Execute a `VestingAction` against the given `schedules`. Returns the updated schedules + /// and locked amount. + fn exec_action( + schedules: Vec, T::BlockNumber>>, + action: VestingAction, + ) -> Result<(Vec, T::BlockNumber>>, BalanceOf), DispatchError> { + let (schedules, locked_now) = match action { + VestingAction::Merge { + index1: idx1, + index2: idx2, + } => { + // The schedule index is based off of the schedule ordering prior to filtering out + // any schedules that may be ending at this block. + let schedule1 = *schedules + .get(idx1) + .ok_or(Error::::ScheduleIndexOutOfBounds)?; + let schedule2 = *schedules + .get(idx2) + .ok_or(Error::::ScheduleIndexOutOfBounds)?; + + // The length of `schedules` decreases by 2 here since we filter out 2 schedules. + // Thus we know below that we can push the new merged schedule without error + // (assuming initial state was valid). + let (mut schedules, mut locked_now) = + Self::report_schedule_updates(schedules.to_vec(), action); + + let now = >::block_number(); + if let Some(new_schedule) = Self::merge_vesting_info(now, schedule1, schedule2) { + // Merging created a new schedule so we: + // 1) need to add it to the accounts vesting schedule collection, + schedules.push(new_schedule); + // (we use `locked_at` in case this is a schedule that started in the past) + let new_schedule_locked = + new_schedule.locked_at::(now); + // and 2) update the locked amount to reflect the schedule we just added. + locked_now = locked_now.saturating_add(new_schedule_locked); + } // In the None case there was no new schedule to account for. + + (schedules, locked_now) + } + _ => Self::report_schedule_updates(schedules.to_vec(), action), + }; + + debug_assert!( + locked_now > Zero::zero() && schedules.len() > 0 + || locked_now == Zero::zero() && schedules.len() == 0 + ); + + Ok((schedules, locked_now)) + } +} + +impl VestingSchedule for Pallet +where + BalanceOf: MaybeSerializeDeserialize + Debug, +{ + type Currency = T::Currency; + type Moment = T::BlockNumber; + + /// Get the amount that is currently being vested and cannot be transferred out of this account. + fn vesting_balance(who: &T::AccountId) -> Option> { + if let Some(v) = Self::vesting(who) { + let now = >::block_number(); + let total_locked_now = v.iter().fold(Zero::zero(), |total, schedule| { + schedule + .locked_at::(now) + .saturating_add(total) + }); + Some(T::Currency::free_balance(who).min(total_locked_now)) + } else { + None + } + } + + /// Adds a vesting schedule to a given account. + /// + /// If the account has `MaxVestingSchedules`, an Error is returned and nothing + /// is updated. + /// + /// On success, a linearly reducing amount of funds will be locked. In order to realise any + /// reduction of the lock over time as it diminishes, the account owner must use `vest` or + /// `vest_other`. + /// + /// Is a no-op if the amount to be vested is zero. + /// + /// NOTE: This doesn't alter the free balance of the account. + fn add_vesting_schedule( + who: &T::AccountId, + locked: BalanceOf, + per_block: BalanceOf, + starting_block: T::BlockNumber, + ) -> DispatchResult { + if locked.is_zero() { + return Ok(()); + } + + let vesting_schedule = VestingInfo::new(locked, per_block, starting_block); + // Check for `per_block` or `locked` of 0. + if !vesting_schedule.is_valid() { + return Err(Error::::InvalidScheduleParams.into()); + }; + + let mut schedules = Self::vesting(who).unwrap_or_default(); + + // NOTE: we must push the new schedule so that `exec_action` + // will give the correct new locked amount. + ensure!( + schedules.try_push(vesting_schedule).is_ok(), + Error::::AtMaxVestingSchedules + ); + + let (schedules, locked_now) = + Self::exec_action(schedules.to_vec(), VestingAction::Passive)?; + + Self::write_vesting(&who, schedules)?; + Self::write_lock(who, locked_now); + + Ok(()) + } + + // Ensure we can call `add_vesting_schedule` without error. This should always + // be called prior to `add_vesting_schedule`. + fn can_add_vesting_schedule( + who: &T::AccountId, + locked: BalanceOf, + per_block: BalanceOf, + starting_block: T::BlockNumber, + ) -> DispatchResult { + // Check for `per_block` or `locked` of 0. + if !VestingInfo::new(locked, per_block, starting_block).is_valid() { + return Err(Error::::InvalidScheduleParams.into()); + } + + ensure!( + (Vesting::::decode_len(who).unwrap_or_default() as u32) < T::MAX_VESTING_SCHEDULES, + Error::::AtMaxVestingSchedules + ); + + Ok(()) + } + + /// Remove a vesting schedule for a given account. + fn remove_vesting_schedule(who: &T::AccountId, schedule_index: u32) -> DispatchResult { + let schedules = Self::vesting(who).ok_or(Error::::NotVesting)?; + let remove_action = VestingAction::Remove { + index: schedule_index as usize, + }; + + let (schedules, locked_now) = Self::exec_action(schedules.to_vec(), remove_action)?; + + Self::write_vesting(&who, schedules)?; + Self::write_lock(who, locked_now); + Ok(()) + } +} diff --git a/frame/vesting/src/migrations.rs b/frame/vesting/src/migrations.rs new file mode 100644 index 00000000..a3fcc91a --- /dev/null +++ b/frame/vesting/src/migrations.rs @@ -0,0 +1,98 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2021 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. + +//! Storage migrations for the vesting pallet. + +use super::*; + +// Migration from single schedule to multiple schedules. +pub(crate) mod v1 { + use super::*; + + #[cfg(feature = "try-runtime")] + pub(crate) fn pre_migrate() -> Result<(), &'static str> { + assert!( + StorageVersion::::get() == Releases::V0, + "Storage version too high." + ); + + log::debug!( + target: "runtime::vesting", + "migration: Vesting storage version v1 PRE migration checks succesful!" + ); + + Ok(()) + } + + /// Migrate from single schedule to multi schedule storage. + /// WARNING: This migration will delete schedules if `MaxVestingSchedules < 1`. + pub(crate) fn migrate() -> Weight { + let mut reads_writes = 0; + + Vesting::::translate::, T::BlockNumber>, _>( + |_key, vesting_info| { + reads_writes += 1; + let v: Option< + BoundedVec< + VestingInfo, T::BlockNumber>, + MaxVestingSchedulesGet, + >, + > = vec![vesting_info].try_into().ok(); + + if v.is_none() { + log::warn!( + target: "runtime::vesting", + "migration: Failed to move a vesting schedule into a BoundedVec" + ); + } + + v + }, + ); + + T::DbWeight::get().reads_writes(reads_writes, reads_writes) + } + + #[cfg(feature = "try-runtime")] + pub(crate) fn post_migrate() -> Result<(), &'static str> { + assert_eq!(StorageVersion::::get(), Releases::V1); + + for (_key, schedules) in Vesting::::iter() { + assert!( + schedules.len() >= 1, + "A bounded vec with incorrect count of items was created." + ); + + for s in schedules { + // It is ok if this does not pass, but ideally pre-existing schedules would pass + // this validation logic so we can be more confident about edge cases. + if !s.is_valid() { + log::warn!( + target: "runtime::vesting", + "migration: A schedule does not pass new validation logic.", + ) + } + } + } + + log::debug!( + target: "runtime::vesting", + "migration: Vesting storage version v1 POST migration checks successful!" + ); + Ok(()) + } +} diff --git a/frame/vesting/src/mock.rs b/frame/vesting/src/mock.rs new file mode 100644 index 00000000..09bfd2fb --- /dev/null +++ b/frame/vesting/src/mock.rs @@ -0,0 +1,160 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2021 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 frame_support::parameter_types; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, Identity, IdentityLookup}, +}; + +use super::*; +use crate as pallet_vesting; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Vesting: pallet_vesting::{Pallet, Call, Storage, Event, Config}, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(1024); +} +impl frame_system::Config for Test { + type AccountData = pallet_balances::AccountData; + type AccountId = u64; + type BaseCallFilter = frame_support::traits::Everything; + type BlockHashCount = BlockHashCount; + type BlockLength = (); + type BlockNumber = u64; + type BlockWeights = (); + type Call = Call; + type DbWeight = (); + type Event = Event; + type Hash = H256; + type Hashing = BlakeTwo256; + type Header = Header; + type Index = u64; + type Lookup = IdentityLookup; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type Origin = Origin; + type PalletInfo = PalletInfo; + type SS58Prefix = (); + type SystemWeightInfo = (); + type Version = (); +} +parameter_types! { + pub const MaxLocks: u32 = 10; +} +impl pallet_balances::Config for Test { + type AccountStore = System; + type Balance = u64; + type DustRemoval = (); + type Event = Event; + type ExistentialDeposit = ExistentialDeposit; + type MaxLocks = MaxLocks; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); +} +parameter_types! { + pub const MinVestedTransfer: u64 = 256 * 2; + pub static ExistentialDeposit: u64 = 0; +} +impl Config for Test { + type BlockNumberToBalance = Identity; + type Currency = Balances; + type Event = Event; + const MAX_VESTING_SCHEDULES: u32 = 3; + type MinVestedTransfer = MinVestedTransfer; + type WeightInfo = (); +} + +pub struct ExtBuilder { + existential_deposit: u64, + vesting_genesis_config: Option>, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { + existential_deposit: 1, + vesting_genesis_config: None, + } + } +} + +impl ExtBuilder { + pub fn existential_deposit(mut self, existential_deposit: u64) -> Self { + self.existential_deposit = existential_deposit; + self + } + + pub fn vesting_genesis_config(mut self, config: Vec<(u64, u64, u64, u64)>) -> Self { + self.vesting_genesis_config = Some(config); + self + } + + pub fn build(self) -> sp_io::TestExternalities { + EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = self.existential_deposit); + let mut t = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + pallet_balances::GenesisConfig:: { + balances: vec![ + (1, 10 * self.existential_deposit), + (2, 20 * self.existential_deposit), + (3, 30 * self.existential_deposit), + (4, 40 * self.existential_deposit), + (12, 10 * self.existential_deposit), + (13, 9999 * self.existential_deposit), + ], + } + .assimilate_storage(&mut t) + .unwrap(); + + let vesting = if let Some(vesting_config) = self.vesting_genesis_config { + vesting_config + } else { + vec![ + (1, 0, 10, 5 * self.existential_deposit), + (2, 10, 20, 0), + (12, 10, 20, 5 * self.existential_deposit), + ] + }; + + pallet_vesting::GenesisConfig:: { vesting } + .assimilate_storage(&mut t) + .unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} diff --git a/frame/vesting/src/tests.rs b/frame/vesting/src/tests.rs new file mode 100644 index 00000000..7beb2fda --- /dev/null +++ b/frame/vesting/src/tests.rs @@ -0,0 +1,1304 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2021 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 frame_support::{assert_noop, assert_ok, assert_storage_noop, dispatch::EncodeLike}; +use frame_system::RawOrigin; +use sp_runtime::traits::{BadOrigin, Identity}; + +use super::{Vesting as VestingStorage, *}; +use crate::mock::{Balances, ExtBuilder, System, Test, Vesting}; + +/// A default existential deposit. +const ED: u64 = 256; + +/// Calls vest, and asserts that there is no entry for `account` +/// in the `Vesting` storage item. +fn vest_and_assert_no_vesting(account: u64) +where + u64: EncodeLike<::AccountId>, + T: pallet::Config, +{ + // Its ok for this to fail because the user may already have no schedules. + let _result = Vesting::vest(Some(account).into()); + assert!(!>::contains_key(account)); +} + +#[test] +fn check_vesting_status() { + ExtBuilder::default() + .existential_deposit(ED) + .build() + .execute_with(|| { + let user1_free_balance = Balances::free_balance(&1); + let user2_free_balance = Balances::free_balance(&2); + let user12_free_balance = Balances::free_balance(&12); + assert_eq!(user1_free_balance, ED * 10); // Account 1 has free balance + assert_eq!(user2_free_balance, ED * 20); // Account 2 has free balance + assert_eq!(user12_free_balance, ED * 10); // Account 12 has free balance + let user1_vesting_schedule = VestingInfo::new( + ED * 5, + 128, // Vesting over 10 blocks + 0, + ); + let user2_vesting_schedule = VestingInfo::new( + ED * 20, + ED, // Vesting over 20 blocks + 10, + ); + let user12_vesting_schedule = VestingInfo::new( + ED * 5, + 64, // Vesting over 20 blocks + 10, + ); + assert_eq!(Vesting::vesting(&1).unwrap(), vec![user1_vesting_schedule]); // Account 1 has a vesting schedule + assert_eq!(Vesting::vesting(&2).unwrap(), vec![user2_vesting_schedule]); // Account 2 has a vesting schedule + assert_eq!( + Vesting::vesting(&12).unwrap(), + vec![user12_vesting_schedule] + ); // Account 12 has a vesting schedule + + // Account 1 has only 128 units vested from their illiquid ED * 5 units at block 1 + assert_eq!(Vesting::vesting_balance(&1), Some(128 * 9)); + // Account 2 has their full balance locked + assert_eq!(Vesting::vesting_balance(&2), Some(user2_free_balance)); + // Account 12 has only their illiquid funds locked + assert_eq!( + Vesting::vesting_balance(&12), + Some(user12_free_balance - ED * 5) + ); + + System::set_block_number(10); + assert_eq!(System::block_number(), 10); + + // Account 1 has fully vested by block 10 + assert_eq!(Vesting::vesting_balance(&1), Some(0)); + // Account 2 has started vesting by block 10 + assert_eq!(Vesting::vesting_balance(&2), Some(user2_free_balance)); + // Account 12 has started vesting by block 10 + assert_eq!( + Vesting::vesting_balance(&12), + Some(user12_free_balance - ED * 5) + ); + + System::set_block_number(30); + assert_eq!(System::block_number(), 30); + + assert_eq!(Vesting::vesting_balance(&1), Some(0)); // Account 1 is still fully vested, and not negative + assert_eq!(Vesting::vesting_balance(&2), Some(0)); // Account 2 has fully vested by block 30 + assert_eq!(Vesting::vesting_balance(&12), Some(0)); // Account 2 has fully vested by block 30 + + // Once we unlock the funds, they are removed from storage. + vest_and_assert_no_vesting::(1); + vest_and_assert_no_vesting::(2); + vest_and_assert_no_vesting::(12); + }); +} + +#[test] +fn check_vesting_status_for_multi_schedule_account() { + ExtBuilder::default() + .existential_deposit(ED) + .build() + .execute_with(|| { + assert_eq!(System::block_number(), 1); + let sched0 = VestingInfo::new( + ED * 20, + ED, // Vesting over 20 blocks + 10, + ); + // Account 2 already has a vesting schedule. + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched0]); + + // Account 2's free balance is from sched0. + let free_balance = Balances::free_balance(&2); + assert_eq!(free_balance, ED * (20)); + assert_eq!(Vesting::vesting_balance(&2), Some(free_balance)); + + // Add a 2nd schedule that is already unlocking by block #1. + let sched1 = VestingInfo::new( + ED * 10, + ED, // Vesting over 10 blocks + 0, + ); + assert_ok!(Vesting::vested_transfer(Some(4).into(), 2, sched1)); + // Free balance is equal to the two existing schedules total amount. + let free_balance = Balances::free_balance(&2); + assert_eq!(free_balance, ED * (10 + 20)); + // The most recently added schedule exists. + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched0, sched1]); + // sched1 has free funds at block #1, but nothing else. + assert_eq!( + Vesting::vesting_balance(&2), + Some(free_balance - sched1.per_block()) + ); + + // Add a 3rd schedule. + let sched2 = VestingInfo::new( + ED * 30, + ED, // Vesting over 30 blocks + 5, + ); + assert_ok!(Vesting::vested_transfer(Some(4).into(), 2, sched2)); + + System::set_block_number(9); + // Free balance is equal to the 3 existing schedules total amount. + let free_balance = Balances::free_balance(&2); + assert_eq!(free_balance, ED * (10 + 20 + 30)); + // sched1 and sched2 are freeing funds at block #9. + assert_eq!( + Vesting::vesting_balance(&2), + Some(free_balance - sched1.per_block() * 9 - sched2.per_block() * 4) + ); + + System::set_block_number(20); + // At block #20 sched1 is fully unlocked while sched2 and sched0 are partially unlocked. + assert_eq!( + Vesting::vesting_balance(&2), + Some( + free_balance + - sched1.locked() + - sched2.per_block() * 15 + - sched0.per_block() * 10 + ) + ); + + System::set_block_number(30); + // At block #30 sched0 and sched1 are fully unlocked while sched2 is partially unlocked. + assert_eq!( + Vesting::vesting_balance(&2), + Some(free_balance - sched1.locked() - sched2.per_block() * 25 - sched0.locked()) + ); + + // At block #35 sched2 fully unlocks and thus all schedules funds are unlocked. + System::set_block_number(35); + assert_eq!(Vesting::vesting_balance(&2), Some(0)); + // Since we have not called any extrinsics that would unlock funds the schedules + // are still in storage, + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched0, sched1, sched2]); + // but once we unlock the funds, they are removed from storage. + vest_and_assert_no_vesting::(2); + }); +} + +#[test] +fn unvested_balance_should_not_transfer() { + ExtBuilder::default() + .existential_deposit(10) + .build() + .execute_with(|| { + let user1_free_balance = Balances::free_balance(&1); + assert_eq!(user1_free_balance, 100); // Account 1 has free balance + // Account 1 has only 5 units vested at block 1 (plus 50 unvested) + assert_eq!(Vesting::vesting_balance(&1), Some(45)); + assert_noop!( + Balances::transfer(Some(1).into(), 2, 56), + pallet_balances::Error::::LiquidityRestrictions, + ); // Account 1 cannot send more than vested amount + }); +} + +#[test] +fn vested_balance_should_transfer() { + ExtBuilder::default() + .existential_deposit(10) + .build() + .execute_with(|| { + let user1_free_balance = Balances::free_balance(&1); + assert_eq!(user1_free_balance, 100); // Account 1 has free balance + // Account 1 has only 5 units vested at block 1 (plus 50 unvested) + assert_eq!(Vesting::vesting_balance(&1), Some(45)); + assert_ok!(Vesting::vest(Some(1).into())); + assert_ok!(Balances::transfer(Some(1).into(), 2, 55)); + }); +} + +#[test] +fn vested_balance_should_transfer_with_multi_sched() { + ExtBuilder::default() + .existential_deposit(ED) + .build() + .execute_with(|| { + let sched0 = VestingInfo::new(5 * ED, 128, 0); + assert_ok!(Vesting::vested_transfer(Some(13).into(), 1, sched0)); + // Total 10*ED locked for all the schedules. + assert_eq!(Vesting::vesting(&1).unwrap(), vec![sched0, sched0]); + + let user1_free_balance = Balances::free_balance(&1); + assert_eq!(user1_free_balance, 3840); // Account 1 has free balance + + // Account 1 has only 256 units unlocking at block 1 (plus 1280 already fee). + assert_eq!(Vesting::vesting_balance(&1), Some(2304)); + assert_ok!(Vesting::vest(Some(1).into())); + assert_ok!(Balances::transfer(Some(1).into(), 2, 1536)); + }); +} + +#[test] +fn non_vested_cannot_vest() { + ExtBuilder::default() + .existential_deposit(ED) + .build() + .execute_with(|| { + assert!(!>::contains_key(4)); + assert_noop!(Vesting::vest(Some(4).into()), Error::::NotVesting); + }); +} + +#[test] +fn vested_balance_should_transfer_using_vest_other() { + ExtBuilder::default() + .existential_deposit(10) + .build() + .execute_with(|| { + let user1_free_balance = Balances::free_balance(&1); + assert_eq!(user1_free_balance, 100); // Account 1 has free balance + // Account 1 has only 5 units vested at block 1 (plus 50 unvested) + assert_eq!(Vesting::vesting_balance(&1), Some(45)); + assert_ok!(Vesting::vest_other(Some(2).into(), 1)); + assert_ok!(Balances::transfer(Some(1).into(), 2, 55)); + }); +} + +#[test] +fn vested_balance_should_transfer_using_vest_other_with_multi_sched() { + ExtBuilder::default() + .existential_deposit(ED) + .build() + .execute_with(|| { + let sched0 = VestingInfo::new(5 * ED, 128, 0); + assert_ok!(Vesting::vested_transfer(Some(13).into(), 1, sched0)); + // Total of 10*ED of locked for all the schedules. + assert_eq!(Vesting::vesting(&1).unwrap(), vec![sched0, sched0]); + + let user1_free_balance = Balances::free_balance(&1); + assert_eq!(user1_free_balance, 3840); // Account 1 has free balance + + // Account 1 has only 256 units unlocking at block 1 (plus 1280 already free). + assert_eq!(Vesting::vesting_balance(&1), Some(2304)); + assert_ok!(Vesting::vest_other(Some(2).into(), 1)); + assert_ok!(Balances::transfer(Some(1).into(), 2, 1536)); + }); +} + +#[test] +fn non_vested_cannot_vest_other() { + ExtBuilder::default() + .existential_deposit(ED) + .build() + .execute_with(|| { + assert!(!>::contains_key(4)); + assert_noop!( + Vesting::vest_other(Some(3).into(), 4), + Error::::NotVesting + ); + }); +} + +#[test] +fn extra_balance_should_transfer() { + ExtBuilder::default() + .existential_deposit(10) + .build() + .execute_with(|| { + assert_ok!(Balances::transfer(Some(3).into(), 1, 100)); + assert_ok!(Balances::transfer(Some(3).into(), 2, 100)); + + let user1_free_balance = Balances::free_balance(&1); + assert_eq!(user1_free_balance, 200); // Account 1 has 100 more free balance than normal + + let user2_free_balance = Balances::free_balance(&2); + assert_eq!(user2_free_balance, 300); // Account 2 has 100 more free balance than normal + + // Account 1 has only 5 units vested at block 1 (plus 150 unvested) + assert_eq!(Vesting::vesting_balance(&1), Some(45)); + assert_ok!(Vesting::vest(Some(1).into())); + assert_ok!(Balances::transfer(Some(1).into(), 3, 155)); // Account 1 can send extra units gained + + // Account 2 has no units vested at block 1, but gained 100 + assert_eq!(Vesting::vesting_balance(&2), Some(200)); + assert_ok!(Vesting::vest(Some(2).into())); + assert_ok!(Balances::transfer(Some(2).into(), 3, 100)); // Account 2 can send extra units gained + }); +} + +#[test] +fn liquid_funds_should_transfer_with_delayed_vesting() { + ExtBuilder::default() + .existential_deposit(256) + .build() + .execute_with(|| { + let user12_free_balance = Balances::free_balance(&12); + + assert_eq!(user12_free_balance, 2560); // Account 12 has free balance + // Account 12 has liquid funds + assert_eq!( + Vesting::vesting_balance(&12), + Some(user12_free_balance - 256 * 5) + ); + + // Account 12 has delayed vesting + let user12_vesting_schedule = VestingInfo::new( + 256 * 5, + 64, // Vesting over 20 blocks + 10, + ); + assert_eq!( + Vesting::vesting(&12).unwrap(), + vec![user12_vesting_schedule] + ); + + // Account 12 can still send liquid funds + assert_ok!(Balances::transfer(Some(12).into(), 3, 256 * 5)); + }); +} + +#[test] +fn vested_transfer_works() { + ExtBuilder::default() + .existential_deposit(256) + .build() + .execute_with(|| { + let user3_free_balance = Balances::free_balance(&3); + let user4_free_balance = Balances::free_balance(&4); + assert_eq!(user3_free_balance, 256 * 30); + assert_eq!(user4_free_balance, 256 * 40); + // Account 4 should not have any vesting yet. + assert_eq!(Vesting::vesting(&4), None); + // Make the schedule for the new transfer. + let new_vesting_schedule = VestingInfo::new( + 256 * 5, + 64, // Vesting over 20 blocks + 10, + ); + assert_ok!(Vesting::vested_transfer( + Some(3).into(), + 4, + new_vesting_schedule + )); + // Now account 4 should have vesting. + assert_eq!(Vesting::vesting(&4).unwrap(), vec![new_vesting_schedule]); + // Ensure the transfer happened correctly. + let user3_free_balance_updated = Balances::free_balance(&3); + assert_eq!(user3_free_balance_updated, 256 * 25); + let user4_free_balance_updated = Balances::free_balance(&4); + assert_eq!(user4_free_balance_updated, 256 * 45); + // Account 4 has 5 * 256 locked. + assert_eq!(Vesting::vesting_balance(&4), Some(256 * 5)); + + System::set_block_number(20); + assert_eq!(System::block_number(), 20); + + // Account 4 has 5 * 64 units vested by block 20. + assert_eq!(Vesting::vesting_balance(&4), Some(10 * 64)); + + System::set_block_number(30); + assert_eq!(System::block_number(), 30); + + // Account 4 has fully vested, + assert_eq!(Vesting::vesting_balance(&4), Some(0)); + // and after unlocking its schedules are removed from storage. + vest_and_assert_no_vesting::(4); + }); +} + +#[test] +fn vested_transfer_correctly_fails() { + ExtBuilder::default() + .existential_deposit(ED) + .build() + .execute_with(|| { + let user2_free_balance = Balances::free_balance(&2); + let user4_free_balance = Balances::free_balance(&4); + assert_eq!(user2_free_balance, ED * 20); + assert_eq!(user4_free_balance, ED * 40); + + // Account 2 should already have a vesting schedule. + let user2_vesting_schedule = VestingInfo::new( + ED * 20, + ED, // Vesting over 20 blocks + 10, + ); + assert_eq!(Vesting::vesting(&2).unwrap(), vec![user2_vesting_schedule]); + + // Fails due to too low transfer amount. + let new_vesting_schedule_too_low = + VestingInfo::new(::MinVestedTransfer::get() - 1, 64, 10); + assert_noop!( + Vesting::vested_transfer(Some(3).into(), 4, new_vesting_schedule_too_low), + Error::::AmountLow, + ); + + // `per_block` is 0, which would result in a schedule with infinite duration. + let schedule_per_block_0 = + VestingInfo::new(::MinVestedTransfer::get(), 0, 10); + assert_noop!( + Vesting::vested_transfer(Some(13).into(), 4, schedule_per_block_0), + Error::::InvalidScheduleParams, + ); + + // `locked` is 0. + let schedule_locked_0 = VestingInfo::new(0, 1, 10); + assert_noop!( + Vesting::vested_transfer(Some(3).into(), 4, schedule_locked_0), + Error::::AmountLow, + ); + + // Free balance has not changed. + assert_eq!(user2_free_balance, Balances::free_balance(&2)); + assert_eq!(user4_free_balance, Balances::free_balance(&4)); + // Account 4 has no schedules. + vest_and_assert_no_vesting::(4); + }); +} + +#[test] +fn vested_transfer_allows_max_schedules() { + ExtBuilder::default() + .existential_deposit(ED) + .build() + .execute_with(|| { + let mut user_4_free_balance = Balances::free_balance(&4); + let max_schedules = ::MAX_VESTING_SCHEDULES; + let sched = VestingInfo::new( + ::MinVestedTransfer::get(), + 1, // Vest over 2 * 256 blocks. + 10, + ); + + // Add max amount schedules to user 4. + for _ in 0..max_schedules { + assert_ok!(Vesting::vested_transfer(Some(13).into(), 4, sched)); + } + + // The schedules count towards vesting balance + let transferred_amount = + ::MinVestedTransfer::get() * max_schedules as u64; + assert_eq!(Vesting::vesting_balance(&4), Some(transferred_amount)); + // and free balance. + user_4_free_balance += transferred_amount; + assert_eq!(Balances::free_balance(&4), user_4_free_balance); + + // Cannot insert a 4th vesting schedule when `MaxVestingSchedules` === 3, + assert_noop!( + Vesting::vested_transfer(Some(3).into(), 4, sched), + Error::::AtMaxVestingSchedules, + ); + // so the free balance does not change. + assert_eq!(Balances::free_balance(&4), user_4_free_balance); + + // Account 4 has fully vested when all the schedules end, + System::set_block_number( + ::MinVestedTransfer::get() + sched.starting_block(), + ); + assert_eq!(Vesting::vesting_balance(&4), Some(0)); + // and after unlocking its schedules are removed from storage. + vest_and_assert_no_vesting::(4); + }); +} + +#[test] +fn force_vested_transfer_works() { + ExtBuilder::default() + .existential_deposit(ED) + .build() + .execute_with(|| { + let user3_free_balance = Balances::free_balance(&3); + let user4_free_balance = Balances::free_balance(&4); + assert_eq!(user3_free_balance, ED * 30); + assert_eq!(user4_free_balance, ED * 40); + // Account 4 should not have any vesting yet. + assert_eq!(Vesting::vesting(&4), None); + // Make the schedule for the new transfer. + let new_vesting_schedule = VestingInfo::new( + ED * 5, + 64, // Vesting over 20 blocks + 10, + ); + + assert_noop!( + Vesting::force_vested_transfer(Some(4).into(), 3, 4, new_vesting_schedule), + BadOrigin + ); + assert_ok!(Vesting::force_vested_transfer( + RawOrigin::Root.into(), + 3, + 4, + new_vesting_schedule + )); + // Now account 4 should have vesting. + assert_eq!(Vesting::vesting(&4).unwrap()[0], new_vesting_schedule); + assert_eq!(Vesting::vesting(&4).unwrap().len(), 1); + // Ensure the transfer happened correctly. + let user3_free_balance_updated = Balances::free_balance(&3); + assert_eq!(user3_free_balance_updated, ED * 25); + let user4_free_balance_updated = Balances::free_balance(&4); + assert_eq!(user4_free_balance_updated, ED * 45); + // Account 4 has 5 * ED locked. + assert_eq!(Vesting::vesting_balance(&4), Some(ED * 5)); + + System::set_block_number(20); + assert_eq!(System::block_number(), 20); + + // Account 4 has 5 * 64 units vested by block 20. + assert_eq!(Vesting::vesting_balance(&4), Some(10 * 64)); + + System::set_block_number(30); + assert_eq!(System::block_number(), 30); + + // Account 4 has fully vested, + assert_eq!(Vesting::vesting_balance(&4), Some(0)); + // and after unlocking its schedules are removed from storage. + vest_and_assert_no_vesting::(4); + }); +} + +#[test] +fn force_vested_transfer_correctly_fails() { + ExtBuilder::default() + .existential_deposit(ED) + .build() + .execute_with(|| { + let user2_free_balance = Balances::free_balance(&2); + let user4_free_balance = Balances::free_balance(&4); + assert_eq!(user2_free_balance, ED * 20); + assert_eq!(user4_free_balance, ED * 40); + // Account 2 should already have a vesting schedule. + let user2_vesting_schedule = VestingInfo::new( + ED * 20, + ED, // Vesting over 20 blocks + 10, + ); + assert_eq!(Vesting::vesting(&2).unwrap(), vec![user2_vesting_schedule]); + + // Too low transfer amount. + let new_vesting_schedule_too_low = + VestingInfo::new(::MinVestedTransfer::get() - 1, 64, 10); + assert_noop!( + Vesting::force_vested_transfer( + RawOrigin::Root.into(), + 3, + 4, + new_vesting_schedule_too_low + ), + Error::::AmountLow, + ); + + // `per_block` is 0. + let schedule_per_block_0 = + VestingInfo::new(::MinVestedTransfer::get(), 0, 10); + assert_noop!( + Vesting::force_vested_transfer(RawOrigin::Root.into(), 13, 4, schedule_per_block_0), + Error::::InvalidScheduleParams, + ); + + // `locked` is 0. + let schedule_locked_0 = VestingInfo::new(0, 1, 10); + assert_noop!( + Vesting::force_vested_transfer(RawOrigin::Root.into(), 3, 4, schedule_locked_0), + Error::::AmountLow, + ); + + // Verify no currency transfer happened. + assert_eq!(user2_free_balance, Balances::free_balance(&2)); + assert_eq!(user4_free_balance, Balances::free_balance(&4)); + // Account 4 has no schedules. + vest_and_assert_no_vesting::(4); + }); +} + +#[test] +fn force_vested_transfer_allows_max_schedules() { + ExtBuilder::default() + .existential_deposit(ED) + .build() + .execute_with(|| { + let mut user_4_free_balance = Balances::free_balance(&4); + let max_schedules = ::MAX_VESTING_SCHEDULES; + let sched = VestingInfo::new( + ::MinVestedTransfer::get(), + 1, // Vest over 2 * 256 blocks. + 10, + ); + + // Add max amount schedules to user 4. + for _ in 0..max_schedules { + assert_ok!(Vesting::force_vested_transfer( + RawOrigin::Root.into(), + 13, + 4, + sched + )); + } + + // The schedules count towards vesting balance. + let transferred_amount = + ::MinVestedTransfer::get() * max_schedules as u64; + assert_eq!(Vesting::vesting_balance(&4), Some(transferred_amount)); + // and free balance. + user_4_free_balance += transferred_amount; + assert_eq!(Balances::free_balance(&4), user_4_free_balance); + + // Cannot insert a 4th vesting schedule when `MaxVestingSchedules` === 3 + assert_noop!( + Vesting::force_vested_transfer(RawOrigin::Root.into(), 3, 4, sched), + Error::::AtMaxVestingSchedules, + ); + // so the free balance does not change. + assert_eq!(Balances::free_balance(&4), user_4_free_balance); + + // Account 4 has fully vested when all the schedules end, + System::set_block_number(::MinVestedTransfer::get() + 10); + assert_eq!(Vesting::vesting_balance(&4), Some(0)); + // and after unlocking its schedules are removed from storage. + vest_and_assert_no_vesting::(4); + }); +} + +#[test] +fn merge_schedules_that_have_not_started() { + ExtBuilder::default() + .existential_deposit(ED) + .build() + .execute_with(|| { + // Account 2 should already have a vesting schedule. + let sched0 = VestingInfo::new( + ED * 20, + ED, // Vest over 20 blocks. + 10, + ); + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched0]); + assert_eq!(Balances::usable_balance(&2), 0); + + // Add a schedule that is identical to the one that already exists. + assert_ok!(Vesting::vested_transfer(Some(3).into(), 2, sched0)); + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched0, sched0]); + assert_eq!(Balances::usable_balance(&2), 0); + assert_ok!(Vesting::merge_schedules(Some(2).into(), 0, 1)); + + // Since we merged identical schedules, the new schedule finishes at the same + // time as the original, just with double the amount. + let sched1 = VestingInfo::new( + sched0.locked() * 2, + sched0.per_block() * 2, + 10, // Starts at the block the schedules are merged/ + ); + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched1]); + + assert_eq!(Balances::usable_balance(&2), 0); + }); +} + +#[test] +fn merge_ongoing_schedules() { + // Merging two schedules that have started will vest both before merging. + ExtBuilder::default() + .existential_deposit(ED) + .build() + .execute_with(|| { + // Account 2 should already have a vesting schedule. + let sched0 = VestingInfo::new( + ED * 20, + ED, // Vest over 20 blocks. + 10, + ); + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched0]); + + let sched1 = VestingInfo::new( + ED * 10, + ED, // Vest over 10 blocks. + sched0.starting_block() + 5, // Start at block 15. + ); + assert_ok!(Vesting::vested_transfer(Some(4).into(), 2, sched1)); + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched0, sched1]); + + // Got to half way through the second schedule where both schedules are actively vesting. + let cur_block = 20; + System::set_block_number(cur_block); + + // Account 2 has no usable balances prior to the merge because they have not unlocked + // with `vest` yet. + assert_eq!(Balances::usable_balance(&2), 0); + + assert_ok!(Vesting::merge_schedules(Some(2).into(), 0, 1)); + + // Merging schedules un-vests all pre-existing schedules prior to merging, which is + // reflected in account 2's updated usable balance. + let sched0_vested_now = sched0.per_block() * (cur_block - sched0.starting_block()); + let sched1_vested_now = sched1.per_block() * (cur_block - sched1.starting_block()); + assert_eq!( + Balances::usable_balance(&2), + sched0_vested_now + sched1_vested_now + ); + + // The locked amount is the sum of what both schedules have locked at the current block. + let sched2_locked = sched1 + .locked_at::(cur_block) + .saturating_add(sched0.locked_at::(cur_block)); + // End block of the new schedule is the greater of either merged schedule. + let sched2_end = sched1 + .ending_block_as_balance::() + .max(sched0.ending_block_as_balance::()); + let sched2_duration = sched2_end - cur_block; + // Based off the new schedules total locked and its duration, we can calculate the + // amount to unlock per block. + let sched2_per_block = sched2_locked / sched2_duration; + + let sched2 = VestingInfo::new(sched2_locked, sched2_per_block, cur_block); + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched2]); + + // And just to double check, we assert the new merged schedule we be cleaned up as expected. + System::set_block_number(30); + vest_and_assert_no_vesting::(2); + }); +} + +#[test] +fn merging_shifts_other_schedules_index() { + // Schedules being merged are filtered out, schedules to the right of any merged + // schedule shift left and the merged schedule is always last. + ExtBuilder::default() + .existential_deposit(ED) + .build() + .execute_with(|| { + let sched0 = VestingInfo::new( + ED * 10, + ED, // Vesting over 10 blocks. + 10, + ); + let sched1 = VestingInfo::new( + ED * 11, + ED, // Vesting over 11 blocks. + 11, + ); + let sched2 = VestingInfo::new( + ED * 12, + ED, // Vesting over 12 blocks. + 12, + ); + + // Account 3 starts out with no schedules, + assert_eq!(Vesting::vesting(&3), None); + // and some usable balance. + let usable_balance = Balances::usable_balance(&3); + assert_eq!(usable_balance, 30 * ED); + + let cur_block = 1; + assert_eq!(System::block_number(), cur_block); + + // Transfer the above 3 schedules to account 3. + assert_ok!(Vesting::vested_transfer(Some(4).into(), 3, sched0)); + assert_ok!(Vesting::vested_transfer(Some(4).into(), 3, sched1)); + assert_ok!(Vesting::vested_transfer(Some(4).into(), 3, sched2)); + + // With no schedules vested or merged they are in the order they are created + assert_eq!(Vesting::vesting(&3).unwrap(), vec![sched0, sched1, sched2]); + // and the usable balance has not changed. + assert_eq!(usable_balance, Balances::usable_balance(&3)); + + assert_ok!(Vesting::merge_schedules(Some(3).into(), 0, 2)); + + // Create the merged schedule of sched0 & sched2. + // The merged schedule will have the max possible starting block, + let sched3_start = sched1.starting_block().max(sched2.starting_block()); + // `locked` equal to the sum of the two schedules locked through the current block, + let sched3_locked = + sched2.locked_at::(cur_block) + sched0.locked_at::(cur_block); + // and will end at the max possible block. + let sched3_end = sched2 + .ending_block_as_balance::() + .max(sched0.ending_block_as_balance::()); + let sched3_duration = sched3_end - sched3_start; + let sched3_per_block = sched3_locked / sched3_duration; + let sched3 = VestingInfo::new(sched3_locked, sched3_per_block, sched3_start); + + // The not touched schedule moves left and the new merged schedule is appended. + assert_eq!(Vesting::vesting(&3).unwrap(), vec![sched1, sched3]); + // The usable balance hasn't changed since none of the schedules have started. + assert_eq!(Balances::usable_balance(&3), usable_balance); + }); +} + +#[test] +fn merge_ongoing_and_yet_to_be_started_schedules() { + // Merge an ongoing schedule that has had `vest` called and a schedule that has not already + // started. + ExtBuilder::default() + .existential_deposit(ED) + .build() + .execute_with(|| { + // Account 2 should already have a vesting schedule. + let sched0 = VestingInfo::new( + ED * 20, + ED, // Vesting over 20 blocks + 10, + ); + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched0]); + + // Fast forward to half way through the life of sched1. + let mut cur_block = + (sched0.starting_block() + sched0.ending_block_as_balance::()) / 2; + assert_eq!(cur_block, 20); + System::set_block_number(cur_block); + + // Prior to vesting there is no usable balance. + let mut usable_balance = 0; + assert_eq!(Balances::usable_balance(&2), usable_balance); + // Vest the current schedules (which is just sched0 now). + Vesting::vest(Some(2).into()).unwrap(); + + // After vesting the usable balance increases by the unlocked amount. + let sched0_vested_now = sched0.locked() - sched0.locked_at::(cur_block); + usable_balance += sched0_vested_now; + assert_eq!(Balances::usable_balance(&2), usable_balance); + + // Go forward a block. + cur_block += 1; + System::set_block_number(cur_block); + + // And add a schedule that starts after this block, but before sched0 finishes. + let sched1 = VestingInfo::new( + ED * 10, + 1, // Vesting over 256 * 10 (2560) blocks + cur_block + 1, + ); + assert_ok!(Vesting::vested_transfer(Some(4).into(), 2, sched1)); + + // Merge the schedules before sched1 starts. + assert_ok!(Vesting::merge_schedules(Some(2).into(), 0, 1)); + // After merging, the usable balance only changes by the amount sched0 vested since we + // last called `vest` (which is just 1 block). The usable balance is not affected by + // sched1 because it has not started yet. + usable_balance += sched0.per_block(); + assert_eq!(Balances::usable_balance(&2), usable_balance); + + // The resulting schedule will have the later starting block of the two, + let sched2_start = sched1.starting_block(); + // `locked` equal to the sum of the two schedules locked through the current block, + let sched2_locked = + sched0.locked_at::(cur_block) + sched1.locked_at::(cur_block); + // and will end at the max possible block. + let sched2_end = sched0 + .ending_block_as_balance::() + .max(sched1.ending_block_as_balance::()); + let sched2_duration = sched2_end - sched2_start; + let sched2_per_block = sched2_locked / sched2_duration; + + let sched2 = VestingInfo::new(sched2_locked, sched2_per_block, sched2_start); + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched2]); + }); +} + +#[test] +fn merge_finished_and_ongoing_schedules() { + // If a schedule finishes by the current block we treat the ongoing schedule, + // without any alterations, as the merged one. + ExtBuilder::default() + .existential_deposit(ED) + .build() + .execute_with(|| { + // Account 2 should already have a vesting schedule. + let sched0 = VestingInfo::new( + ED * 20, + ED, // Vesting over 20 blocks. + 10, + ); + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched0]); + + let sched1 = VestingInfo::new( + ED * 40, + ED, // Vesting over 40 blocks. + 10, + ); + assert_ok!(Vesting::vested_transfer(Some(4).into(), 2, sched1)); + + // Transfer a 3rd schedule, so we can demonstrate how schedule indices change. + // (We are not merging this schedule.) + let sched2 = VestingInfo::new( + ED * 30, + ED, // Vesting over 30 blocks. + 10, + ); + assert_ok!(Vesting::vested_transfer(Some(3).into(), 2, sched2)); + + // The schedules are in expected order prior to merging. + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched0, sched1, sched2]); + + // Fast forward to sched0's end block. + let cur_block = sched0.ending_block_as_balance::(); + System::set_block_number(cur_block); + assert_eq!(System::block_number(), 30); + + // Prior to `merge_schedules` and with no vest/vest_other called the user has no usable + // balance. + assert_eq!(Balances::usable_balance(&2), 0); + assert_ok!(Vesting::merge_schedules(Some(2).into(), 0, 1)); + + // sched2 is now the first, since sched0 & sched1 get filtered out while "merging". + // sched1 gets treated like the new merged schedule by getting pushed onto back + // of the vesting schedules vec. Note: sched0 finished at the current block. + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched2, sched1]); + + // sched0 has finished, so its funds are fully unlocked. + let sched0_unlocked_now = sched0.locked(); + // The remaining schedules are ongoing, so their funds are partially unlocked. + let sched1_unlocked_now = sched1.locked() - sched1.locked_at::(cur_block); + let sched2_unlocked_now = sched2.locked() - sched2.locked_at::(cur_block); + + // Since merging also vests all the schedules, the users usable balance after merging + // includes all pre-existing schedules unlocked through the current block, including + // schedules not merged. + assert_eq!( + Balances::usable_balance(&2), + sched0_unlocked_now + sched1_unlocked_now + sched2_unlocked_now + ); + }); +} + +#[test] +fn merge_finishing_schedules_does_not_create_a_new_one() { + // If both schedules finish by the current block we don't create new one + ExtBuilder::default() + .existential_deposit(ED) + .build() + .execute_with(|| { + // Account 2 should already have a vesting schedule. + let sched0 = VestingInfo::new( + ED * 20, + ED, // 20 block duration. + 10, + ); + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched0]); + + // Create sched1 and transfer it to account 2. + let sched1 = VestingInfo::new( + ED * 30, + ED, // 30 block duration. + 10, + ); + assert_ok!(Vesting::vested_transfer(Some(3).into(), 2, sched1)); + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched0, sched1]); + + let all_scheds_end = sched0 + .ending_block_as_balance::() + .max(sched1.ending_block_as_balance::()); + + assert_eq!(all_scheds_end, 40); + System::set_block_number(all_scheds_end); + + // Prior to merge_schedules and with no vest/vest_other called the user has no usable + // balance. + assert_eq!(Balances::usable_balance(&2), 0); + + // Merge schedule 0 and 1. + assert_ok!(Vesting::merge_schedules(Some(2).into(), 0, 1)); + // The user no longer has any more vesting schedules because they both ended at the + // block they where merged, + assert!(!>::contains_key(&2)); + // and their usable balance has increased by the total amount locked in the merged + // schedules. + assert_eq!( + Balances::usable_balance(&2), + sched0.locked() + sched1.locked() + ); + }); +} + +#[test] +fn merge_finished_and_yet_to_be_started_schedules() { + ExtBuilder::default() + .existential_deposit(ED) + .build() + .execute_with(|| { + // Account 2 should already have a vesting schedule. + let sched0 = VestingInfo::new( + ED * 20, + ED, // 20 block duration. + 10, // Ends at block 30 + ); + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched0]); + + let sched1 = VestingInfo::new( + ED * 30, + ED * 2, // 30 block duration. + 35, + ); + assert_ok!(Vesting::vested_transfer(Some(13).into(), 2, sched1)); + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched0, sched1]); + + let sched2 = VestingInfo::new( + ED * 40, + ED, // 40 block duration. + 30, + ); + // Add a 3rd schedule to demonstrate how sched1 shifts. + assert_ok!(Vesting::vested_transfer(Some(13).into(), 2, sched2)); + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched0, sched1, sched2]); + + System::set_block_number(30); + + // At block 30, sched0 has finished unlocking while sched1 and sched2 are still fully + // locked, + assert_eq!( + Vesting::vesting_balance(&2), + Some(sched1.locked() + sched2.locked()) + ); + // but since we have not vested usable balance is still 0. + assert_eq!(Balances::usable_balance(&2), 0); + + // Merge schedule 0 and 1. + assert_ok!(Vesting::merge_schedules(Some(2).into(), 0, 1)); + + // sched0 is removed since it finished, and sched1 is removed and then pushed on the back + // because it is treated as the merged schedule + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched2, sched1]); + + // The usable balance is updated because merging fully unlocked sched0. + assert_eq!(Balances::usable_balance(&2), sched0.locked()); + }); +} + +#[test] +fn merge_schedules_throws_proper_errors() { + ExtBuilder::default() + .existential_deposit(ED) + .build() + .execute_with(|| { + // Account 2 should already have a vesting schedule. + let sched0 = VestingInfo::new( + ED * 20, + ED, // 20 block duration. + 10, + ); + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched0]); + + // Account 2 only has 1 vesting schedule. + assert_noop!( + Vesting::merge_schedules(Some(2).into(), 0, 1), + Error::::ScheduleIndexOutOfBounds + ); + + // Account 4 has 0 vesting schedules. + assert_eq!(Vesting::vesting(&4), None); + assert_noop!( + Vesting::merge_schedules(Some(4).into(), 0, 1), + Error::::NotVesting + ); + + // There are enough schedules to merge but an index is non-existent. + Vesting::vested_transfer(Some(3).into(), 2, sched0).unwrap(); + assert_eq!(Vesting::vesting(&2).unwrap(), vec![sched0, sched0]); + assert_noop!( + Vesting::merge_schedules(Some(2).into(), 0, 2), + Error::::ScheduleIndexOutOfBounds + ); + + // It is a storage noop with no errors if the indexes are the same. + assert_storage_noop!(Vesting::merge_schedules(Some(2).into(), 0, 0).unwrap()); + }); +} + +#[test] +fn generates_multiple_schedules_from_genesis_config() { + let vesting_config = vec![ + // 5 * existential deposit locked. + (1, 0, 10, 5 * ED), + // 1 * existential deposit locked. + (2, 10, 20, 19 * ED), + // 2 * existential deposit locked. + (2, 10, 20, 18 * ED), + // 1 * existential deposit locked. + (12, 10, 20, 9 * ED), + // 2 * existential deposit locked. + (12, 10, 20, 8 * ED), + // 3 * existential deposit locked. + (12, 10, 20, 7 * ED), + ]; + ExtBuilder::default() + .existential_deposit(ED) + .vesting_genesis_config(vesting_config) + .build() + .execute_with(|| { + let user1_sched1 = VestingInfo::new(5 * ED, 128, 0u64); + assert_eq!(Vesting::vesting(&1).unwrap(), vec![user1_sched1]); + + let user2_sched1 = VestingInfo::new(1 * ED, 12, 10u64); + let user2_sched2 = VestingInfo::new(2 * ED, 25, 10u64); + assert_eq!( + Vesting::vesting(&2).unwrap(), + vec![user2_sched1, user2_sched2] + ); + + let user12_sched1 = VestingInfo::new(1 * ED, 12, 10u64); + let user12_sched2 = VestingInfo::new(2 * ED, 25, 10u64); + let user12_sched3 = VestingInfo::new(3 * ED, 38, 10u64); + assert_eq!( + Vesting::vesting(&12).unwrap(), + vec![user12_sched1, user12_sched2, user12_sched3] + ); + }); +} + +#[test] +#[should_panic] +fn multiple_schedules_from_genesis_config_errors() { + // MaxVestingSchedules is 3, but this config has 4 for account 12 so we panic when building + // from genesis. + let vesting_config = vec![ + (12, 10, 20, ED), + (12, 10, 20, ED), + (12, 10, 20, ED), + (12, 10, 20, ED), + ]; + ExtBuilder::default() + .existential_deposit(ED) + .vesting_genesis_config(vesting_config) + .build(); +} + +#[test] +fn build_genesis_has_storage_version_v1() { + ExtBuilder::default() + .existential_deposit(ED) + .build() + .execute_with(|| { + assert_eq!(StorageVersion::::get(), Releases::V1); + }); +} + +#[test] +fn merge_vesting_handles_per_block_0() { + ExtBuilder::default() + .existential_deposit(ED) + .build() + .execute_with(|| { + let sched0 = VestingInfo::new( + ED, 0, // Vesting over 256 blocks. + 1, + ); + assert_eq!(sched0.ending_block_as_balance::(), 257); + let sched1 = VestingInfo::new( + ED * 2, + 0, // Vesting over 512 blocks. + 10, + ); + assert_eq!(sched1.ending_block_as_balance::(), 512u64 + 10); + + let merged = VestingInfo::new(764, 1, 10); + assert_eq!(Vesting::merge_vesting_info(5, sched0, sched1), Some(merged)); + }); +} + +#[test] +fn vesting_info_validate_works() { + let min_transfer = ::MinVestedTransfer::get(); + // Does not check for min transfer. + assert_eq!( + VestingInfo::new(min_transfer - 1, 1u64, 10u64).is_valid(), + true + ); + + // `locked` cannot be 0. + assert_eq!(VestingInfo::new(0, 1u64, 10u64).is_valid(), false); + + // `per_block` cannot be 0. + assert_eq!( + VestingInfo::new(min_transfer + 1, 0u64, 10u64).is_valid(), + false + ); + + // With valid inputs it does not error. + assert_eq!(VestingInfo::new(min_transfer, 1u64, 10u64).is_valid(), true); +} + +#[test] +fn vesting_info_ending_block_as_balance_works() { + // Treats `per_block` 0 as 1. + let per_block_0 = VestingInfo::new(256u32, 0u32, 10u32); + assert_eq!(per_block_0.ending_block_as_balance::(), 256 + 10); + + // `per_block >= locked` always results in a schedule ending the block after it starts + let per_block_gt_locked = VestingInfo::new(256u32, 256 * 2u32, 10u32); + assert_eq!( + per_block_gt_locked.ending_block_as_balance::(), + 1 + per_block_gt_locked.starting_block() + ); + let per_block_eq_locked = VestingInfo::new(256u32, 256u32, 10u32); + assert_eq!( + per_block_gt_locked.ending_block_as_balance::(), + per_block_eq_locked.ending_block_as_balance::() + ); + + // Correctly calcs end if `locked % per_block != 0`. (We need a block to unlock the remainder). + let imperfect_per_block = VestingInfo::new(256u32, 250u32, 10u32); + assert_eq!( + imperfect_per_block.ending_block_as_balance::(), + imperfect_per_block.starting_block() + 2u32, + ); + assert_eq!( + imperfect_per_block + .locked_at::(imperfect_per_block.ending_block_as_balance::()), + 0 + ); +} + +#[test] +fn per_block_works() { + let per_block_0 = VestingInfo::new(256u32, 0u32, 10u32); + assert_eq!(per_block_0.per_block(), 1u32); + assert_eq!(per_block_0.raw_per_block(), 0u32); + + let per_block_1 = VestingInfo::new(256u32, 1u32, 10u32); + assert_eq!(per_block_1.per_block(), 1u32); + assert_eq!(per_block_1.raw_per_block(), 1u32); +} + +// When an accounts free balance + schedule.locked is less than ED, the vested transfer will fail. +#[test] +fn vested_transfer_less_than_existential_deposit_fails() { + ExtBuilder::default() + .existential_deposit(4 * ED) + .build() + .execute_with(|| { + // MinVestedTransfer is less the ED. + assert!( + ::Currency::minimum_balance() + > ::MinVestedTransfer::get() + ); + + let sched = VestingInfo::new( + ::MinVestedTransfer::get() as u64, + 1u64, + 10u64, + ); + // The new account balance with the schedule's locked amount would be less than ED. + assert!( + Balances::free_balance(&99) + sched.locked() + < ::Currency::minimum_balance() + ); + + // vested_transfer fails. + assert_noop!( + Vesting::vested_transfer(Some(3).into(), 99, sched), + pallet_balances::Error::::ExistentialDeposit, + ); + // force_vested_transfer fails. + assert_noop!( + Vesting::force_vested_transfer(RawOrigin::Root.into(), 3, 99, sched), + pallet_balances::Error::::ExistentialDeposit, + ); + }); +} diff --git a/frame/vesting/src/vesting_info.rs b/frame/vesting/src/vesting_info.rs new file mode 100644 index 00000000..95982129 --- /dev/null +++ b/frame/vesting/src/vesting_info.rs @@ -0,0 +1,118 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2021 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. + +//! Module to enforce private fields on `VestingInfo`. + +use super::*; + +/// Struct to encode the vesting schedule of an individual account. +#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct VestingInfo { + /// Locked amount at genesis. + locked: Balance, + /// Amount that gets unlocked every block after `starting_block`. + per_block: Balance, + /// Starting block for unlocking(vesting). + starting_block: BlockNumber, +} + +impl VestingInfo +where + Balance: AtLeast32BitUnsigned + Copy, + BlockNumber: AtLeast32BitUnsigned + Copy + Bounded, +{ + /// Instantiate a new `VestingInfo`. + pub fn new( + locked: Balance, + per_block: Balance, + starting_block: BlockNumber, + ) -> VestingInfo { + VestingInfo { + locked, + per_block, + starting_block, + } + } + + /// Validate parameters for `VestingInfo`. Note that this does not check + /// against `MinVestedTransfer`. + pub fn is_valid(&self) -> bool { + !self.locked.is_zero() && !self.raw_per_block().is_zero() + } + + /// Locked amount at schedule creation. + pub fn locked(&self) -> Balance { + self.locked + } + + /// Amount that gets unlocked every block after `starting_block`. Corrects for `per_block` of 0. + /// We don't let `per_block` be less than 1, or else the vesting will never end. + /// This should be used whenever accessing `per_block` unless explicitly checking for 0 values. + pub fn per_block(&self) -> Balance { + self.per_block.max(One::one()) + } + + /// Get the unmodified `per_block`. Generally should not be used, but is useful for + /// validating `per_block`. + pub(crate) fn raw_per_block(&self) -> Balance { + self.per_block + } + + /// Starting block for unlocking(vesting). + pub fn starting_block(&self) -> BlockNumber { + self.starting_block + } + + /// Amount locked at block `n`. + pub fn locked_at>( + &self, + n: BlockNumber, + ) -> Balance { + // Number of blocks that count toward vesting; + // saturating to 0 when n < starting_block. + let vested_block_count = n.saturating_sub(self.starting_block); + let vested_block_count = BlockNumberToBalance::convert(vested_block_count); + // Return amount that is still locked in vesting. + vested_block_count + .checked_mul(&self.per_block()) // `per_block` accessor guarantees at least 1. + .map(|to_unlock| self.locked.saturating_sub(to_unlock)) + .unwrap_or(Zero::zero()) + } + + /// Block number at which the schedule ends (as type `Balance`). + pub fn ending_block_as_balance>( + &self, + ) -> Balance { + let starting_block = BlockNumberToBalance::convert(self.starting_block); + let duration = if self.per_block() >= self.locked { + // If `per_block` is bigger than `locked`, the schedule will end + // the block after starting. + One::one() + } else { + self.locked / self.per_block() + + if (self.locked % self.per_block()).is_zero() { + Zero::zero() + } else { + // `per_block` does not perfectly divide `locked`, so we need an extra block to + // unlock some amount less than `per_block`. + One::one() + } + }; + + starting_block.saturating_add(duration) + } +} diff --git a/frame/vesting/src/weights.rs b/frame/vesting/src/weights.rs new file mode 100644 index 00000000..3ccc1a5b --- /dev/null +++ b/frame/vesting/src/weights.rs @@ -0,0 +1,253 @@ +// This file is part of Substrate. + +// Copyright (C) 2021 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. + +//! Autogenerated weights for pallet_vesting +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2021-08-10, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128 + +// Executed Command: +// target/release/substrate +// benchmark +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_vesting +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./frame/vesting/src/weights.rs +// --template=./.maintain/frame-weight-template.hbs + + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_vesting. +pub trait WeightInfo { + fn vest_locked(l: u32, s: u32, ) -> Weight; + fn vest_unlocked(l: u32, s: u32, ) -> Weight; + fn vest_other_locked(l: u32, s: u32, ) -> Weight; + fn vest_other_unlocked(l: u32, s: u32, ) -> Weight; + fn vested_transfer(l: u32, s: u32, ) -> Weight; + fn force_vested_transfer(l: u32, s: u32, ) -> Weight; + fn not_unlocking_merge_schedules(l: u32, s: u32, ) -> Weight; + fn unlocking_merge_schedules(l: u32, s: u32, ) -> Weight; +} + +/// Weights for pallet_vesting using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + // Storage: Vesting Vesting (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + fn vest_locked(l: u32, s: u32, ) -> Weight { + (50_642_000 as Weight) + // Standard Error: 1_000 + .saturating_add((144_000 as Weight).saturating_mul(l as Weight)) + // Standard Error: 3_000 + .saturating_add((177_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: Vesting Vesting (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + fn vest_unlocked(l: u32, s: u32, ) -> Weight { + (50_830_000 as Weight) + // Standard Error: 1_000 + .saturating_add((115_000 as Weight).saturating_mul(l as Weight)) + // Standard Error: 3_000 + .saturating_add((112_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: Vesting Vesting (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + // Storage: System Account (r:1 w:1) + fn vest_other_locked(l: u32, s: u32, ) -> Weight { + (52_151_000 as Weight) + // Standard Error: 1_000 + .saturating_add((130_000 as Weight).saturating_mul(l as Weight)) + // Standard Error: 3_000 + .saturating_add((162_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Vesting Vesting (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + // Storage: System Account (r:1 w:1) + fn vest_other_unlocked(l: u32, s: u32, ) -> Weight { + (51_009_000 as Weight) + // Standard Error: 4_000 + .saturating_add((123_000 as Weight).saturating_mul(l as Weight)) + // Standard Error: 9_000 + .saturating_add((118_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Vesting Vesting (r:1 w:1) + // Storage: System Account (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + fn vested_transfer(l: u32, s: u32, ) -> Weight { + (89_517_000 as Weight) + // Standard Error: 5_000 + .saturating_add((114_000 as Weight).saturating_mul(l as Weight)) + // Standard Error: 10_000 + .saturating_add((23_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Vesting Vesting (r:1 w:1) + // Storage: System Account (r:2 w:2) + // Storage: Balances Locks (r:1 w:1) + fn force_vested_transfer(l: u32, s: u32, ) -> Weight { + (87_903_000 as Weight) + // Standard Error: 6_000 + .saturating_add((121_000 as Weight).saturating_mul(l as Weight)) + // Standard Error: 12_000 + .saturating_add((56_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + // Storage: Vesting Vesting (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + // Storage: System Account (r:1 w:1) + fn not_unlocking_merge_schedules(l: u32, s: u32, ) -> Weight { + (54_463_000 as Weight) + // Standard Error: 2_000 + .saturating_add((123_000 as Weight).saturating_mul(l as Weight)) + // Standard Error: 5_000 + .saturating_add((149_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Vesting Vesting (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + // Storage: System Account (r:1 w:1) + fn unlocking_merge_schedules(l: u32, s: u32, ) -> Weight { + (53_674_000 as Weight) + // Standard Error: 1_000 + .saturating_add((137_000 as Weight).saturating_mul(l as Weight)) + // Standard Error: 4_000 + .saturating_add((152_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + // Storage: Vesting Vesting (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + fn vest_locked(l: u32, s: u32, ) -> Weight { + (50_642_000 as Weight) + // Standard Error: 1_000 + .saturating_add((144_000 as Weight).saturating_mul(l as Weight)) + // Standard Error: 3_000 + .saturating_add((177_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: Vesting Vesting (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + fn vest_unlocked(l: u32, s: u32, ) -> Weight { + (50_830_000 as Weight) + // Standard Error: 1_000 + .saturating_add((115_000 as Weight).saturating_mul(l as Weight)) + // Standard Error: 3_000 + .saturating_add((112_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: Vesting Vesting (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + // Storage: System Account (r:1 w:1) + fn vest_other_locked(l: u32, s: u32, ) -> Weight { + (52_151_000 as Weight) + // Standard Error: 1_000 + .saturating_add((130_000 as Weight).saturating_mul(l as Weight)) + // Standard Error: 3_000 + .saturating_add((162_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Vesting Vesting (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + // Storage: System Account (r:1 w:1) + fn vest_other_unlocked(l: u32, s: u32, ) -> Weight { + (51_009_000 as Weight) + // Standard Error: 4_000 + .saturating_add((123_000 as Weight).saturating_mul(l as Weight)) + // Standard Error: 9_000 + .saturating_add((118_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Vesting Vesting (r:1 w:1) + // Storage: System Account (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + fn vested_transfer(l: u32, s: u32, ) -> Weight { + (89_517_000 as Weight) + // Standard Error: 5_000 + .saturating_add((114_000 as Weight).saturating_mul(l as Weight)) + // Standard Error: 10_000 + .saturating_add((23_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Vesting Vesting (r:1 w:1) + // Storage: System Account (r:2 w:2) + // Storage: Balances Locks (r:1 w:1) + fn force_vested_transfer(l: u32, s: u32, ) -> Weight { + (87_903_000 as Weight) + // Standard Error: 6_000 + .saturating_add((121_000 as Weight).saturating_mul(l as Weight)) + // Standard Error: 12_000 + .saturating_add((56_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + } + // Storage: Vesting Vesting (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + // Storage: System Account (r:1 w:1) + fn not_unlocking_merge_schedules(l: u32, s: u32, ) -> Weight { + (54_463_000 as Weight) + // Standard Error: 2_000 + .saturating_add((123_000 as Weight).saturating_mul(l as Weight)) + // Standard Error: 5_000 + .saturating_add((149_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Vesting Vesting (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + // Storage: System Account (r:1 w:1) + fn unlocking_merge_schedules(l: u32, s: u32, ) -> Weight { + (53_674_000 as Weight) + // Standard Error: 1_000 + .saturating_add((137_000 as Weight).saturating_mul(l as Weight)) + // Standard Error: 4_000 + .saturating_add((152_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } +} diff --git a/precompiles/staking/Cargo.toml b/precompiles/staking/Cargo.toml new file mode 100644 index 00000000..3db4a515 --- /dev/null +++ b/precompiles/staking/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "pallet-precompile-staking" +version = "0.2.1" +authors = ["Stake Technologies "] +edition = "2018" +license = "Apache-2.0" +homepage = "https://astar.network" +repository = "https://github.com/PlasmNetwork/Astar" +description = "Collator staking EVM precompiles" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "2.2.0", default-features = false } +evm = { git = "https://github.com/PlasmNetwork/evm", branch = "polkadot-v0.9.13", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +pallet-evm = { git = "https://github.com/PlasmNetwork/frontier", branch = "polkadot-v0.9.13", default-features = false } +pallet-session = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false } +pallet-collator-selection = { git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.13", default-features = false } + +[features] +default = ["std"] +std = [ + "codec/std", + "evm/std", + "sp-std/std", + "sp-core/std", + "frame-support/std", + "frame-system/std", + "pallet-evm/std", + "pallet-session/std", + "pallet-collator-selection/std", +] diff --git a/precompiles/staking/Staking.sol b/precompiles/staking/Staking.sol new file mode 100644 index 00000000..5049de2d --- /dev/null +++ b/precompiles/staking/Staking.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BSD-3-Clause + +pragma solidity >=0.7.0; + +interface Staking { + /* + * @dev Set session keys of function caller. + */ + function set_keys(bytes calldata keys) external; + + /* + * @dev Removes any session keys of the function caller. + */ + function purge_keys() external; + + /* + * @dev Register function caller as collation candidate. + * @note Collation staking deposit will be locked. + */ + function register_as_candidate() external; +} diff --git a/precompiles/staking/src/lib.rs b/precompiles/staking/src/lib.rs new file mode 100644 index 00000000..584cafdb --- /dev/null +++ b/precompiles/staking/src/lib.rs @@ -0,0 +1,108 @@ +//! Astar collator staking interface. + +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::Decode; +use evm::{executor::PrecompileOutput, Context, ExitError, ExitSucceed}; +use frame_support::dispatch::{Dispatchable, GetDispatchInfo, PostDispatchInfo}; +use pallet_evm::{AddressMapping, GasWeightMapping, Precompile}; +use sp_std::{marker::PhantomData, vec::Vec}; + +pub struct Staking(PhantomData); + +impl Staking +where + R: pallet_session::Config + pallet_collator_selection::Config, + R::Call: From> + From>, +{ + fn set_keys(keys: Vec) -> Result { + let keys = ::Keys::decode(&mut &keys[..]) + .map_err(|_| ExitError::Other("Unable to decode session keys".into()))?; + Ok(pallet_session::Call::::set_keys { + keys, + proof: Default::default(), + } + .into()) + } + + fn purge_keys() -> R::Call { + pallet_session::Call::::purge_keys {}.into() + } + + fn register_as_candidate() -> R::Call { + pallet_collator_selection::Call::::register_as_candidate {}.into() + } +} + +impl Precompile for Staking +where + R: pallet_evm::Config + pallet_session::Config + pallet_collator_selection::Config, + R::Call: From> + + From> + + Dispatchable + + GetDispatchInfo, + ::Origin: From>, +{ + fn execute( + input: &[u8], + target_gas: Option, + context: &Context, + ) -> Result { + const SELECTOR_SIZE_BYTES: usize = 4; + + if input.len() < SELECTOR_SIZE_BYTES { + return Err(ExitError::Other("input length less than 4 bytes".into())); + } + + // ======= Staking.sol:Staking ======= + // Function signatures: + // bcb24ddc: set_keys(bytes) + // 321c9b7a: purge_keys() + // d09b6ba5: register_as_candidate() + let call = match input[0..SELECTOR_SIZE_BYTES] { + [0xbc, 0xb2, 0x4d, 0xdc] => { + if input.len() < SELECTOR_SIZE_BYTES + 32 * 2 { + return Err(ExitError::Other("input length less than 36 bytes".into())); + } + // Low level argument parsing + let len_offset = SELECTOR_SIZE_BYTES + 32; + let keys_offset = len_offset + 32; + // Session keys is 32 byte lenght + if input.len() < SELECTOR_SIZE_BYTES + 32 * 3 { + return Err(ExitError::Other("wrong input length".into())); + } + let keys = input[keys_offset..(keys_offset + 32)].to_vec(); + Self::set_keys(keys)? + } + [0x32, 0x1c, 0x9b, 0x7a] => Self::purge_keys(), + [0xd0, 0x9b, 0x6b, 0xa5] => Self::register_as_candidate(), + _ => { + return Err(ExitError::Other( + "No method at selector given selector".into(), + )) + } + }; + + let info = call.get_dispatch_info(); + if let Some(gas_limit) = target_gas { + let required_gas = R::GasWeightMapping::weight_to_gas(info.weight); + if required_gas > gas_limit { + return Err(ExitError::OutOfGas); + } + } + + let origin = R::AddressMapping::into_account_id(context.caller); + let post_info = call + .dispatch(Some(origin).into()) + .map_err(|_| ExitError::Other("Method call via EVM failed".into()))?; + + let gas_used = + R::GasWeightMapping::weight_to_gas(post_info.actual_weight.unwrap_or(info.weight)); + Ok(PrecompileOutput { + exit_status: ExitSucceed::Stopped, + cost: gas_used, + output: Default::default(), + logs: Default::default(), + }) + } +}