diff --git a/.gitmodules b/.gitmodules index 11fa5b8c..5f8bca64 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "orml"] path = orml url = https://github.com/Slixon-Technologies/open-runtime-module-library.git +[submodule "sygma"] + path = sygma + url = https://github.com/sygmaprotocol/sygma-substrate-pallets diff --git a/README.md b/README.md index 038f4820..d9ebaa68 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,7 @@ Setheum's Blockchain Network node Implementation in Rust, ready for hacking :roc - [1.0. Introduction](#10-introduction) - [1.1. Setheum Chain](#11-setheum-chain) - [1.2. Ethical DeFi](#12-ethical-defi) - - [1.2.1. Edfis - Ethical DeFi Swap](#121-edfis---ethical-defi-swap) - - [1.2.2. The Setter Stablecoin](#122-the-setter-stablecoin) - - [1.2.3. The Slick USD Stablecoin](#123-the-slick-usd-stablecoin) + - [1.2.1. Ethical DeFi Projects](#121-ethical-defi-projects) - [2.0. Getting Started](#20-getting-started) - [2.1. Build](#21-build) - [2.2. Run](#22-run) @@ -70,36 +68,23 @@ Setheum's Blockchain Network node Implementation in Rust, ready for hacking :roc ### 1.1. Setheum Chain -Founded November 2019,Setheum achieves a high level of equilibrium in the trilemma by leveraging a Directed Acyclic Graph(DAG) to build the blockchain consensus -making it a Blockchain via DAG, achieve instant finality, high throughput and very fast blocktime while preserving network security and having a fairly decentralised network, +Founded November 2019,Setheum achieves a high level of equilibrium in the trilemma by leveraging a Directed Acyclic Graph(DAG) to build the blockchain consensus - making it a Blockchain via DAG, achieve instant finality, high throughput and very fast blocktime while preserving network security and having a fairly decentralised network, -Setheum is a secure, confidential and interoperable decentralised internet cloud compute and storage blockchain network with EVM and WASM smart contracts, -web3 and web 2 Support. The intent of the Setheum Network is to improve upon Web3 and solve the blockchain trilemma with a mixture of approaches and a recipe -formed from what we have seen and considered to be some of the best solutions in the field, improving on scalability, security, mass adoption, diversity, -and ethics while preserving decentralisation and democratisation. - -etheum intends to be the most scalable blockchain network in the world while providing -confidentiality for smart contracts, Cloud Computing and Storage Infrastructure for Web3 based Internet Solutions and Interoperability with both Web2 and -other Web3 Networks. The AlephBFT Consensus Engine powers the Setheum Chain to have near instant finality, -high throughput and high scalability. - -Setheum’s consensus system works to achieve high scalability and high security with an ethical and equitably high level of decentralisation. +Setheum is a light-speed decentralised blockchain network with EVM and WASM smart contracts, built from a mixture of what we have seen and considered to be some of the best solutions in the industry, improving on scalability, security, user experience, ethics,decentralisation and democratisation. Setheum intends to be the most complete blockchain network in the world. The AlephBFT Consensus Engine powers the Setheum Chain to have near instant finality, high throughput and high scalability and high security. ### 1.2. Ethical DeFi Ethical DeFi Suite is the DeFi powerhouse of the Setheum Network, providing all kinds of top notch DeFi protocols including an AMM DEX (inspired by Uniswap v3), Decentralised Liquid Staking and Ethical Zero-interest Halal sStablecoins that gives us the properties of both Fiat and Crypto with SlickUSD (USSD) and the Setter (SETR) using an Ethical Collateralized Debt Position (ECDP) mechanism that is over-Collateralized and multi-Collateralised and stable without compromising decentralisation or economic stability, offering stable cryptocurrencies that have scalable value and reliability, setheum provides just that, backed by crypto assets on an efficient zero-interest debt-based system. -#### 1.2.1. Edfis - Ethical DeFi Swap - -Edfis is Ethical DeFi Swap, the AMM (Automated Market Maker) DEX (Decentralized Exchange) Protocol of the Ethical DeFi Suite inspired by Uniswap v3 design natively built on the Setheum Network with optimisations and a native liquidity mining mechanism for incentivizing LPs. - -#### 1.2.2. The Setter Stablecoin - -The Setter is Ethical DeFi's flagship stablecoin, built on the `ECDP` (Ethical Collateralised Debt Position), the Unpegged ECDP Stablecoin Protocol of the Ethical DeFi Suite is inspired by MakerDAO's and RAI's design, natively built on the Setheum Network with Zero-interest loans with optimisations and native liquidation protocols which include our on-chain built-in DEX Edfis, a native auction system, as well as an on-chain native liquidation protection mechanism. The Setter uses a system we call `LVSI` (Low Volatility Stable Index) which makes the stablecoin float without a peg while remaining stable-ish, it is over-collateralised by the Setheum's native currency `SEE`. - -#### 1.2.3. The Slick USD Stablecoin +#### 1.2.1. Ethical DeFi Projects: -The Slick USD is Ethical DeFi's USD-pegged stablecoin, built on the `ECDP` (Ethical Collateralised Debt Position), the Pegged ECDP Stablecoin Protocol of the Ethical DeFi Suite is inspired by MakerDAO's design, natively built on the Setheum Network with Zero-interest loans with optimisations and native liquidation protocols which include our on-chain built-in DEX Edfis, a native auction system, as well as an on-chain native liquidation protection mechanism. The SlickUSD is over-collateralised and multi-collateralised. +- `Edfis`: DEX (Decentralized Exchange) + - `Edfis Exchange`: AMM (Automated Market Maker) DEX Protocol inspired by Uniswap v3 design + - `Edfis Launchpad`: Launchpad Crowdsales protocol for bootstrapping pools on Edfis Exchange + - `Edfis Launchpool`: Launchpool protocol for bootstrapping pools on Edfis Exchange + - `Edfis Liquid Staking`: Liquid Staking Protocol +- `Setter`: Unpegged ECDP Stablecoin +- `SlickUSD`: USD Pegged ECDP Stablecoin ## 2.0. Getting Started diff --git a/blockchain/finality-aleph/Cargo.toml b/blockchain/finality/Cargo.toml similarity index 100% rename from blockchain/finality-aleph/Cargo.toml rename to blockchain/finality/Cargo.toml diff --git a/blockchain/finality-aleph/README.md b/blockchain/finality/README.md similarity index 100% rename from blockchain/finality-aleph/README.md rename to blockchain/finality/README.md diff --git a/blockchain/finality-aleph/TODO.md b/blockchain/finality/TODO.md similarity index 100% rename from blockchain/finality-aleph/TODO.md rename to blockchain/finality/TODO.md diff --git a/blockchain/finality-aleph/src/abft/common.rs b/blockchain/finality/src/abft/common.rs similarity index 100% rename from blockchain/finality-aleph/src/abft/common.rs rename to blockchain/finality/src/abft/common.rs diff --git a/blockchain/finality-aleph/src/abft/crypto.rs b/blockchain/finality/src/abft/crypto.rs similarity index 100% rename from blockchain/finality-aleph/src/abft/crypto.rs rename to blockchain/finality/src/abft/crypto.rs diff --git a/blockchain/finality-aleph/src/abft/current/mod.rs b/blockchain/finality/src/abft/current/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/abft/current/mod.rs rename to blockchain/finality/src/abft/current/mod.rs diff --git a/blockchain/finality-aleph/src/abft/current/network.rs b/blockchain/finality/src/abft/current/network.rs similarity index 100% rename from blockchain/finality-aleph/src/abft/current/network.rs rename to blockchain/finality/src/abft/current/network.rs diff --git a/blockchain/finality-aleph/src/abft/current/traits.rs b/blockchain/finality/src/abft/current/traits.rs similarity index 100% rename from blockchain/finality-aleph/src/abft/current/traits.rs rename to blockchain/finality/src/abft/current/traits.rs diff --git a/blockchain/finality-aleph/src/abft/legacy/mod.rs b/blockchain/finality/src/abft/legacy/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/abft/legacy/mod.rs rename to blockchain/finality/src/abft/legacy/mod.rs diff --git a/blockchain/finality-aleph/src/abft/legacy/network.rs b/blockchain/finality/src/abft/legacy/network.rs similarity index 100% rename from blockchain/finality-aleph/src/abft/legacy/network.rs rename to blockchain/finality/src/abft/legacy/network.rs diff --git a/blockchain/finality-aleph/src/abft/legacy/traits.rs b/blockchain/finality/src/abft/legacy/traits.rs similarity index 100% rename from blockchain/finality-aleph/src/abft/legacy/traits.rs rename to blockchain/finality/src/abft/legacy/traits.rs diff --git a/blockchain/finality-aleph/src/abft/mod.rs b/blockchain/finality/src/abft/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/abft/mod.rs rename to blockchain/finality/src/abft/mod.rs diff --git a/blockchain/finality-aleph/src/abft/network.rs b/blockchain/finality/src/abft/network.rs similarity index 100% rename from blockchain/finality-aleph/src/abft/network.rs rename to blockchain/finality/src/abft/network.rs diff --git a/blockchain/finality-aleph/src/abft/traits.rs b/blockchain/finality/src/abft/traits.rs similarity index 100% rename from blockchain/finality-aleph/src/abft/traits.rs rename to blockchain/finality/src/abft/traits.rs diff --git a/blockchain/finality-aleph/src/abft/types.rs b/blockchain/finality/src/abft/types.rs similarity index 100% rename from blockchain/finality-aleph/src/abft/types.rs rename to blockchain/finality/src/abft/types.rs diff --git a/blockchain/finality-aleph/src/aggregation/mod.rs b/blockchain/finality/src/aggregation/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/aggregation/mod.rs rename to blockchain/finality/src/aggregation/mod.rs diff --git a/blockchain/finality-aleph/src/base_protocol/handler.rs b/blockchain/finality/src/base_protocol/handler.rs similarity index 100% rename from blockchain/finality-aleph/src/base_protocol/handler.rs rename to blockchain/finality/src/base_protocol/handler.rs diff --git a/blockchain/finality-aleph/src/base_protocol/mod.rs b/blockchain/finality/src/base_protocol/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/base_protocol/mod.rs rename to blockchain/finality/src/base_protocol/mod.rs diff --git a/blockchain/finality-aleph/src/base_protocol/service.rs b/blockchain/finality/src/base_protocol/service.rs similarity index 100% rename from blockchain/finality-aleph/src/base_protocol/service.rs rename to blockchain/finality/src/base_protocol/service.rs diff --git a/blockchain/finality-aleph/src/block/mock/backend.rs b/blockchain/finality/src/block/mock/backend.rs similarity index 100% rename from blockchain/finality-aleph/src/block/mock/backend.rs rename to blockchain/finality/src/block/mock/backend.rs diff --git a/blockchain/finality-aleph/src/block/mock/mod.rs b/blockchain/finality/src/block/mock/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/block/mock/mod.rs rename to blockchain/finality/src/block/mock/mod.rs diff --git a/blockchain/finality-aleph/src/block/mock/status_notifier.rs b/blockchain/finality/src/block/mock/status_notifier.rs similarity index 100% rename from blockchain/finality-aleph/src/block/mock/status_notifier.rs rename to blockchain/finality/src/block/mock/status_notifier.rs diff --git a/blockchain/finality-aleph/src/block/mod.rs b/blockchain/finality/src/block/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/block/mod.rs rename to blockchain/finality/src/block/mod.rs diff --git a/blockchain/finality-aleph/src/block/substrate/chain_status.rs b/blockchain/finality/src/block/substrate/chain_status.rs similarity index 100% rename from blockchain/finality-aleph/src/block/substrate/chain_status.rs rename to blockchain/finality/src/block/substrate/chain_status.rs diff --git a/blockchain/finality-aleph/src/block/substrate/finalizer.rs b/blockchain/finality/src/block/substrate/finalizer.rs similarity index 100% rename from blockchain/finality-aleph/src/block/substrate/finalizer.rs rename to blockchain/finality/src/block/substrate/finalizer.rs diff --git a/blockchain/finality-aleph/src/block/substrate/justification.rs b/blockchain/finality/src/block/substrate/justification.rs similarity index 100% rename from blockchain/finality-aleph/src/block/substrate/justification.rs rename to blockchain/finality/src/block/substrate/justification.rs diff --git a/blockchain/finality-aleph/src/block/substrate/mod.rs b/blockchain/finality/src/block/substrate/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/block/substrate/mod.rs rename to blockchain/finality/src/block/substrate/mod.rs diff --git a/blockchain/finality-aleph/src/block/substrate/status_notifier.rs b/blockchain/finality/src/block/substrate/status_notifier.rs similarity index 100% rename from blockchain/finality-aleph/src/block/substrate/status_notifier.rs rename to blockchain/finality/src/block/substrate/status_notifier.rs diff --git a/blockchain/finality-aleph/src/block/substrate/verification/cache.rs b/blockchain/finality/src/block/substrate/verification/cache.rs similarity index 100% rename from blockchain/finality-aleph/src/block/substrate/verification/cache.rs rename to blockchain/finality/src/block/substrate/verification/cache.rs diff --git a/blockchain/finality-aleph/src/block/substrate/verification/mod.rs b/blockchain/finality/src/block/substrate/verification/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/block/substrate/verification/mod.rs rename to blockchain/finality/src/block/substrate/verification/mod.rs diff --git a/blockchain/finality-aleph/src/block/substrate/verification/verifier.rs b/blockchain/finality/src/block/substrate/verification/verifier.rs similarity index 100% rename from blockchain/finality-aleph/src/block/substrate/verification/verifier.rs rename to blockchain/finality/src/block/substrate/verification/verifier.rs diff --git a/blockchain/finality-aleph/src/compatibility.rs b/blockchain/finality/src/compatibility.rs similarity index 100% rename from blockchain/finality-aleph/src/compatibility.rs rename to blockchain/finality/src/compatibility.rs diff --git a/blockchain/finality-aleph/src/crypto.rs b/blockchain/finality/src/crypto.rs similarity index 100% rename from blockchain/finality-aleph/src/crypto.rs rename to blockchain/finality/src/crypto.rs diff --git a/blockchain/finality-aleph/src/data_io/chain_info.rs b/blockchain/finality/src/data_io/chain_info.rs similarity index 100% rename from blockchain/finality-aleph/src/data_io/chain_info.rs rename to blockchain/finality/src/data_io/chain_info.rs diff --git a/blockchain/finality-aleph/src/data_io/data_interpreter.rs b/blockchain/finality/src/data_io/data_interpreter.rs similarity index 100% rename from blockchain/finality-aleph/src/data_io/data_interpreter.rs rename to blockchain/finality/src/data_io/data_interpreter.rs diff --git a/blockchain/finality-aleph/src/data_io/data_provider.rs b/blockchain/finality/src/data_io/data_provider.rs similarity index 100% rename from blockchain/finality-aleph/src/data_io/data_provider.rs rename to blockchain/finality/src/data_io/data_provider.rs diff --git a/blockchain/finality-aleph/src/data_io/data_store.rs b/blockchain/finality/src/data_io/data_store.rs similarity index 100% rename from blockchain/finality-aleph/src/data_io/data_store.rs rename to blockchain/finality/src/data_io/data_store.rs diff --git a/blockchain/finality-aleph/src/data_io/legacy/data_interpreter.rs b/blockchain/finality/src/data_io/legacy/data_interpreter.rs similarity index 100% rename from blockchain/finality-aleph/src/data_io/legacy/data_interpreter.rs rename to blockchain/finality/src/data_io/legacy/data_interpreter.rs diff --git a/blockchain/finality-aleph/src/data_io/legacy/data_provider.rs b/blockchain/finality/src/data_io/legacy/data_provider.rs similarity index 100% rename from blockchain/finality-aleph/src/data_io/legacy/data_provider.rs rename to blockchain/finality/src/data_io/legacy/data_provider.rs diff --git a/blockchain/finality-aleph/src/data_io/legacy/data_store.rs b/blockchain/finality/src/data_io/legacy/data_store.rs similarity index 100% rename from blockchain/finality-aleph/src/data_io/legacy/data_store.rs rename to blockchain/finality/src/data_io/legacy/data_store.rs diff --git a/blockchain/finality-aleph/src/data_io/legacy/mod.rs b/blockchain/finality/src/data_io/legacy/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/data_io/legacy/mod.rs rename to blockchain/finality/src/data_io/legacy/mod.rs diff --git a/blockchain/finality-aleph/src/data_io/legacy/proposal.rs b/blockchain/finality/src/data_io/legacy/proposal.rs similarity index 100% rename from blockchain/finality-aleph/src/data_io/legacy/proposal.rs rename to blockchain/finality/src/data_io/legacy/proposal.rs diff --git a/blockchain/finality-aleph/src/data_io/legacy/status_provider.rs b/blockchain/finality/src/data_io/legacy/status_provider.rs similarity index 100% rename from blockchain/finality-aleph/src/data_io/legacy/status_provider.rs rename to blockchain/finality/src/data_io/legacy/status_provider.rs diff --git a/blockchain/finality-aleph/src/data_io/mod.rs b/blockchain/finality/src/data_io/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/data_io/mod.rs rename to blockchain/finality/src/data_io/mod.rs diff --git a/blockchain/finality-aleph/src/data_io/proposal.rs b/blockchain/finality/src/data_io/proposal.rs similarity index 100% rename from blockchain/finality-aleph/src/data_io/proposal.rs rename to blockchain/finality/src/data_io/proposal.rs diff --git a/blockchain/finality-aleph/src/data_io/status_provider.rs b/blockchain/finality/src/data_io/status_provider.rs similarity index 100% rename from blockchain/finality-aleph/src/data_io/status_provider.rs rename to blockchain/finality/src/data_io/status_provider.rs diff --git a/blockchain/finality-aleph/src/finalization.rs b/blockchain/finality/src/finalization.rs similarity index 100% rename from blockchain/finality-aleph/src/finalization.rs rename to blockchain/finality/src/finalization.rs diff --git a/blockchain/finality-aleph/src/idx_to_account.rs b/blockchain/finality/src/idx_to_account.rs similarity index 100% rename from blockchain/finality-aleph/src/idx_to_account.rs rename to blockchain/finality/src/idx_to_account.rs diff --git a/blockchain/finality-aleph/src/import.rs b/blockchain/finality/src/import.rs similarity index 100% rename from blockchain/finality-aleph/src/import.rs rename to blockchain/finality/src/import.rs diff --git a/blockchain/finality-aleph/src/justification/compatibility.rs b/blockchain/finality/src/justification/compatibility.rs similarity index 100% rename from blockchain/finality-aleph/src/justification/compatibility.rs rename to blockchain/finality/src/justification/compatibility.rs diff --git a/blockchain/finality-aleph/src/justification/mod.rs b/blockchain/finality/src/justification/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/justification/mod.rs rename to blockchain/finality/src/justification/mod.rs diff --git a/blockchain/finality-aleph/src/lib.rs b/blockchain/finality/src/lib.rs similarity index 100% rename from blockchain/finality-aleph/src/lib.rs rename to blockchain/finality/src/lib.rs diff --git a/blockchain/finality-aleph/src/metrics/all_block.rs b/blockchain/finality/src/metrics/all_block.rs similarity index 100% rename from blockchain/finality-aleph/src/metrics/all_block.rs rename to blockchain/finality/src/metrics/all_block.rs diff --git a/blockchain/finality-aleph/src/metrics/chain_state.rs b/blockchain/finality/src/metrics/chain_state.rs similarity index 100% rename from blockchain/finality-aleph/src/metrics/chain_state.rs rename to blockchain/finality/src/metrics/chain_state.rs diff --git a/blockchain/finality-aleph/src/metrics/finality_rate.rs b/blockchain/finality/src/metrics/finality_rate.rs similarity index 100% rename from blockchain/finality-aleph/src/metrics/finality_rate.rs rename to blockchain/finality/src/metrics/finality_rate.rs diff --git a/blockchain/finality-aleph/src/metrics/mod.rs b/blockchain/finality/src/metrics/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/metrics/mod.rs rename to blockchain/finality/src/metrics/mod.rs diff --git a/blockchain/finality-aleph/src/metrics/timing.rs b/blockchain/finality/src/metrics/timing.rs similarity index 100% rename from blockchain/finality-aleph/src/metrics/timing.rs rename to blockchain/finality/src/metrics/timing.rs diff --git a/blockchain/finality-aleph/src/metrics/transaction_pool.rs b/blockchain/finality/src/metrics/transaction_pool.rs similarity index 100% rename from blockchain/finality-aleph/src/metrics/transaction_pool.rs rename to blockchain/finality/src/metrics/transaction_pool.rs diff --git a/blockchain/finality-aleph/src/network/address_cache.rs b/blockchain/finality/src/network/address_cache.rs similarity index 100% rename from blockchain/finality-aleph/src/network/address_cache.rs rename to blockchain/finality/src/network/address_cache.rs diff --git a/blockchain/finality-aleph/src/network/data/component.rs b/blockchain/finality/src/network/data/component.rs similarity index 100% rename from blockchain/finality-aleph/src/network/data/component.rs rename to blockchain/finality/src/network/data/component.rs diff --git a/blockchain/finality-aleph/src/network/data/mod.rs b/blockchain/finality/src/network/data/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/network/data/mod.rs rename to blockchain/finality/src/network/data/mod.rs diff --git a/blockchain/finality-aleph/src/network/data/split.rs b/blockchain/finality/src/network/data/split.rs similarity index 100% rename from blockchain/finality-aleph/src/network/data/split.rs rename to blockchain/finality/src/network/data/split.rs diff --git a/blockchain/finality-aleph/src/network/gossip/metrics.rs b/blockchain/finality/src/network/gossip/metrics.rs similarity index 100% rename from blockchain/finality-aleph/src/network/gossip/metrics.rs rename to blockchain/finality/src/network/gossip/metrics.rs diff --git a/blockchain/finality-aleph/src/network/gossip/mock.rs b/blockchain/finality/src/network/gossip/mock.rs similarity index 100% rename from blockchain/finality-aleph/src/network/gossip/mock.rs rename to blockchain/finality/src/network/gossip/mock.rs diff --git a/blockchain/finality-aleph/src/network/gossip/mod.rs b/blockchain/finality/src/network/gossip/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/network/gossip/mod.rs rename to blockchain/finality/src/network/gossip/mod.rs diff --git a/blockchain/finality-aleph/src/network/gossip/service.rs b/blockchain/finality/src/network/gossip/service.rs similarity index 100% rename from blockchain/finality-aleph/src/network/gossip/service.rs rename to blockchain/finality/src/network/gossip/service.rs diff --git a/blockchain/finality-aleph/src/network/mock.rs b/blockchain/finality/src/network/mock.rs similarity index 100% rename from blockchain/finality-aleph/src/network/mock.rs rename to blockchain/finality/src/network/mock.rs diff --git a/blockchain/finality-aleph/src/network/mod.rs b/blockchain/finality/src/network/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/network/mod.rs rename to blockchain/finality/src/network/mod.rs diff --git a/blockchain/finality-aleph/src/network/session/compatibility.rs b/blockchain/finality/src/network/session/compatibility.rs similarity index 100% rename from blockchain/finality-aleph/src/network/session/compatibility.rs rename to blockchain/finality/src/network/session/compatibility.rs diff --git a/blockchain/finality-aleph/src/network/session/connections.rs b/blockchain/finality/src/network/session/connections.rs similarity index 100% rename from blockchain/finality-aleph/src/network/session/connections.rs rename to blockchain/finality/src/network/session/connections.rs diff --git a/blockchain/finality-aleph/src/network/session/data.rs b/blockchain/finality/src/network/session/data.rs similarity index 100% rename from blockchain/finality-aleph/src/network/session/data.rs rename to blockchain/finality/src/network/session/data.rs diff --git a/blockchain/finality-aleph/src/network/session/discovery.rs b/blockchain/finality/src/network/session/discovery.rs similarity index 100% rename from blockchain/finality-aleph/src/network/session/discovery.rs rename to blockchain/finality/src/network/session/discovery.rs diff --git a/blockchain/finality-aleph/src/network/session/handler.rs b/blockchain/finality/src/network/session/handler.rs similarity index 100% rename from blockchain/finality-aleph/src/network/session/handler.rs rename to blockchain/finality/src/network/session/handler.rs diff --git a/blockchain/finality-aleph/src/network/session/manager.rs b/blockchain/finality/src/network/session/manager.rs similarity index 100% rename from blockchain/finality-aleph/src/network/session/manager.rs rename to blockchain/finality/src/network/session/manager.rs diff --git a/blockchain/finality-aleph/src/network/session/mod.rs b/blockchain/finality/src/network/session/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/network/session/mod.rs rename to blockchain/finality/src/network/session/mod.rs diff --git a/blockchain/finality-aleph/src/network/session/service.rs b/blockchain/finality/src/network/session/service.rs similarity index 100% rename from blockchain/finality-aleph/src/network/session/service.rs rename to blockchain/finality/src/network/session/service.rs diff --git a/blockchain/finality-aleph/src/network/session/testing.rs b/blockchain/finality/src/network/session/testing.rs similarity index 100% rename from blockchain/finality-aleph/src/network/session/testing.rs rename to blockchain/finality/src/network/session/testing.rs diff --git a/blockchain/finality-aleph/src/network/substrate.rs b/blockchain/finality/src/network/substrate.rs similarity index 100% rename from blockchain/finality-aleph/src/network/substrate.rs rename to blockchain/finality/src/network/substrate.rs diff --git a/blockchain/finality-aleph/src/network/tcp.rs b/blockchain/finality/src/network/tcp.rs similarity index 100% rename from blockchain/finality-aleph/src/network/tcp.rs rename to blockchain/finality/src/network/tcp.rs diff --git a/blockchain/finality-aleph/src/nodes.rs b/blockchain/finality/src/nodes.rs similarity index 100% rename from blockchain/finality-aleph/src/nodes.rs rename to blockchain/finality/src/nodes.rs diff --git a/blockchain/finality-aleph/src/party/backup.rs b/blockchain/finality/src/party/backup.rs similarity index 100% rename from blockchain/finality-aleph/src/party/backup.rs rename to blockchain/finality/src/party/backup.rs diff --git a/blockchain/finality-aleph/src/party/impls.rs b/blockchain/finality/src/party/impls.rs similarity index 100% rename from blockchain/finality-aleph/src/party/impls.rs rename to blockchain/finality/src/party/impls.rs diff --git a/blockchain/finality-aleph/src/party/manager/aggregator.rs b/blockchain/finality/src/party/manager/aggregator.rs similarity index 100% rename from blockchain/finality-aleph/src/party/manager/aggregator.rs rename to blockchain/finality/src/party/manager/aggregator.rs diff --git a/blockchain/finality-aleph/src/party/manager/authority.rs b/blockchain/finality/src/party/manager/authority.rs similarity index 100% rename from blockchain/finality-aleph/src/party/manager/authority.rs rename to blockchain/finality/src/party/manager/authority.rs diff --git a/blockchain/finality-aleph/src/party/manager/mod.rs b/blockchain/finality/src/party/manager/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/party/manager/mod.rs rename to blockchain/finality/src/party/manager/mod.rs diff --git a/blockchain/finality-aleph/src/party/manager/task.rs b/blockchain/finality/src/party/manager/task.rs similarity index 100% rename from blockchain/finality-aleph/src/party/manager/task.rs rename to blockchain/finality/src/party/manager/task.rs diff --git a/blockchain/finality-aleph/src/party/mocks.rs b/blockchain/finality/src/party/mocks.rs similarity index 100% rename from blockchain/finality-aleph/src/party/mocks.rs rename to blockchain/finality/src/party/mocks.rs diff --git a/blockchain/finality-aleph/src/party/mod.rs b/blockchain/finality/src/party/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/party/mod.rs rename to blockchain/finality/src/party/mod.rs diff --git a/blockchain/finality-aleph/src/party/traits.rs b/blockchain/finality/src/party/traits.rs similarity index 100% rename from blockchain/finality-aleph/src/party/traits.rs rename to blockchain/finality/src/party/traits.rs diff --git a/blockchain/finality-aleph/src/runtime_api.rs b/blockchain/finality/src/runtime_api.rs similarity index 100% rename from blockchain/finality-aleph/src/runtime_api.rs rename to blockchain/finality/src/runtime_api.rs diff --git a/blockchain/finality-aleph/src/session.rs b/blockchain/finality/src/session.rs similarity index 100% rename from blockchain/finality-aleph/src/session.rs rename to blockchain/finality/src/session.rs diff --git a/blockchain/finality-aleph/src/session_map.rs b/blockchain/finality/src/session_map.rs similarity index 100% rename from blockchain/finality-aleph/src/session_map.rs rename to blockchain/finality/src/session_map.rs diff --git a/blockchain/finality-aleph/src/sync/data.rs b/blockchain/finality/src/sync/data.rs similarity index 100% rename from blockchain/finality-aleph/src/sync/data.rs rename to blockchain/finality/src/sync/data.rs diff --git a/blockchain/finality-aleph/src/sync/forest/mod.rs b/blockchain/finality/src/sync/forest/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/sync/forest/mod.rs rename to blockchain/finality/src/sync/forest/mod.rs diff --git a/blockchain/finality-aleph/src/sync/forest/vertex.rs b/blockchain/finality/src/sync/forest/vertex.rs similarity index 100% rename from blockchain/finality-aleph/src/sync/forest/vertex.rs rename to blockchain/finality/src/sync/forest/vertex.rs diff --git a/blockchain/finality-aleph/src/sync/handler/mod.rs b/blockchain/finality/src/sync/handler/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/sync/handler/mod.rs rename to blockchain/finality/src/sync/handler/mod.rs diff --git a/blockchain/finality-aleph/src/sync/handler/request_handler.rs b/blockchain/finality/src/sync/handler/request_handler.rs similarity index 100% rename from blockchain/finality-aleph/src/sync/handler/request_handler.rs rename to blockchain/finality/src/sync/handler/request_handler.rs diff --git a/blockchain/finality-aleph/src/sync/message_limiter.rs b/blockchain/finality/src/sync/message_limiter.rs similarity index 100% rename from blockchain/finality-aleph/src/sync/message_limiter.rs rename to blockchain/finality/src/sync/message_limiter.rs diff --git a/blockchain/finality-aleph/src/sync/metrics.rs b/blockchain/finality/src/sync/metrics.rs similarity index 100% rename from blockchain/finality-aleph/src/sync/metrics.rs rename to blockchain/finality/src/sync/metrics.rs diff --git a/blockchain/finality-aleph/src/sync/mod.rs b/blockchain/finality/src/sync/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/sync/mod.rs rename to blockchain/finality/src/sync/mod.rs diff --git a/blockchain/finality-aleph/src/sync/service.rs b/blockchain/finality/src/sync/service.rs similarity index 100% rename from blockchain/finality-aleph/src/sync/service.rs rename to blockchain/finality/src/sync/service.rs diff --git a/blockchain/finality-aleph/src/sync/task_queue.rs b/blockchain/finality/src/sync/task_queue.rs similarity index 100% rename from blockchain/finality-aleph/src/sync/task_queue.rs rename to blockchain/finality/src/sync/task_queue.rs diff --git a/blockchain/finality-aleph/src/sync/tasks.rs b/blockchain/finality/src/sync/tasks.rs similarity index 100% rename from blockchain/finality-aleph/src/sync/tasks.rs rename to blockchain/finality/src/sync/tasks.rs diff --git a/blockchain/finality-aleph/src/sync/ticker.rs b/blockchain/finality/src/sync/ticker.rs similarity index 100% rename from blockchain/finality-aleph/src/sync/ticker.rs rename to blockchain/finality/src/sync/ticker.rs diff --git a/blockchain/finality-aleph/src/sync_oracle.rs b/blockchain/finality/src/sync_oracle.rs similarity index 100% rename from blockchain/finality-aleph/src/sync_oracle.rs rename to blockchain/finality/src/sync_oracle.rs diff --git a/blockchain/finality-aleph/src/testing/client_chain_builder.rs b/blockchain/finality/src/testing/client_chain_builder.rs similarity index 100% rename from blockchain/finality-aleph/src/testing/client_chain_builder.rs rename to blockchain/finality/src/testing/client_chain_builder.rs diff --git a/blockchain/finality-aleph/src/testing/data_store.rs b/blockchain/finality/src/testing/data_store.rs similarity index 100% rename from blockchain/finality-aleph/src/testing/data_store.rs rename to blockchain/finality/src/testing/data_store.rs diff --git a/blockchain/finality-aleph/src/testing/mocks/acceptance_policy.rs b/blockchain/finality/src/testing/mocks/acceptance_policy.rs similarity index 100% rename from blockchain/finality-aleph/src/testing/mocks/acceptance_policy.rs rename to blockchain/finality/src/testing/mocks/acceptance_policy.rs diff --git a/blockchain/finality-aleph/src/testing/mocks/block_finalizer.rs b/blockchain/finality/src/testing/mocks/block_finalizer.rs similarity index 100% rename from blockchain/finality-aleph/src/testing/mocks/block_finalizer.rs rename to blockchain/finality/src/testing/mocks/block_finalizer.rs diff --git a/blockchain/finality-aleph/src/testing/mocks/client.rs b/blockchain/finality/src/testing/mocks/client.rs similarity index 100% rename from blockchain/finality-aleph/src/testing/mocks/client.rs rename to blockchain/finality/src/testing/mocks/client.rs diff --git a/blockchain/finality-aleph/src/testing/mocks/mod.rs b/blockchain/finality/src/testing/mocks/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/testing/mocks/mod.rs rename to blockchain/finality/src/testing/mocks/mod.rs diff --git a/blockchain/finality-aleph/src/testing/mocks/proposal.rs b/blockchain/finality/src/testing/mocks/proposal.rs similarity index 100% rename from blockchain/finality-aleph/src/testing/mocks/proposal.rs rename to blockchain/finality/src/testing/mocks/proposal.rs diff --git a/blockchain/finality-aleph/src/testing/mocks/single_action_mock.rs b/blockchain/finality/src/testing/mocks/single_action_mock.rs similarity index 100% rename from blockchain/finality-aleph/src/testing/mocks/single_action_mock.rs rename to blockchain/finality/src/testing/mocks/single_action_mock.rs diff --git a/blockchain/finality-aleph/src/testing/mod.rs b/blockchain/finality/src/testing/mod.rs similarity index 100% rename from blockchain/finality-aleph/src/testing/mod.rs rename to blockchain/finality/src/testing/mod.rs diff --git a/blockchain/finality-aleph/src/testing/network.rs b/blockchain/finality/src/testing/network.rs similarity index 100% rename from blockchain/finality-aleph/src/testing/network.rs rename to blockchain/finality/src/testing/network.rs diff --git a/blockchain/modules/ecdp-setr-engine/README.md b/blockchain/modules/ecdp-setr-engine/README.md index 51e4cc75..70bb721b 100644 --- a/blockchain/modules/ecdp-setr-engine/README.md +++ b/blockchain/modules/ecdp-setr-engine/README.md @@ -3,3 +3,5 @@ ## Overview Provides Unegged ECDP `Setter (SETR)` Stablecoin on Ethical DeFi. + +The Setter is Ethical DeFi's flagship stablecoin, built on the `ECDP` (Ethical Collateralised Debt Position), the Unpegged ECDP Stablecoin Protocol of the Ethical DeFi Suite is inspired by MakerDAO's and RAI's design, natively built on the Setheum Network with Zero-interest loans with optimisations and native liquidation protocols which include our on-chain built-in DEX Edfis, a native auction system, as well as an on-chain native liquidation protection mechanism. The Setter uses a system we call `LVSI` (Low Volatility Stable Index) which makes the stablecoin float without a peg while remaining stable-ish, it is over-collateralised by the Setheum's native currency `SEE`. diff --git a/blockchain/modules/ecdp-setr-treasury/README.md b/blockchain/modules/ecdp-setr-treasury/README.md index 7968ee1e..42cf145d 100644 --- a/blockchain/modules/ecdp-setr-treasury/README.md +++ b/blockchain/modules/ecdp-setr-treasury/README.md @@ -3,3 +3,5 @@ ## Overview Provides Unegged ECDP `Setter (SETR)` Stablecoin on Ethical DeFi. + +The Setter is Ethical DeFi's flagship stablecoin, built on the `ECDP` (Ethical Collateralised Debt Position), the Unpegged ECDP Stablecoin Protocol of the Ethical DeFi Suite is inspired by MakerDAO's and RAI's design, natively built on the Setheum Network with Zero-interest loans with optimisations and native liquidation protocols which include our on-chain built-in DEX Edfis, a native auction system, as well as an on-chain native liquidation protection mechanism. The Setter uses a system we call `LVSI` (Low Volatility Stable Index) which makes the stablecoin float without a peg while remaining stable-ish, it is over-collateralised by the Setheum's native currency `SEE`. diff --git a/blockchain/modules/ecdp-ussd-engine/README.md b/blockchain/modules/ecdp-ussd-engine/README.md index 2e1c8a5d..dc8e856a 100644 --- a/blockchain/modules/ecdp-ussd-engine/README.md +++ b/blockchain/modules/ecdp-ussd-engine/README.md @@ -3,3 +3,5 @@ ## Overview Provides USD-Pegged ECDP `Slick USD (USSD)` Stablecoin on Ethical DeFi. + +The Slick USD is Ethical DeFi's USD-pegged stablecoin, built on the `ECDP` (Ethical Collateralised Debt Position), the Pegged ECDP Stablecoin Protocol of the Ethical DeFi Suite is inspired by MakerDAO's design, natively built on the Setheum Network with Zero-interest loans with optimisations and native liquidation protocols which include our on-chain built-in DEX Edfis, a native auction system, as well as an on-chain native liquidation protection mechanism. The SlickUSD is over-collateralised and multi-collateralised. diff --git a/blockchain/modules/ecdp-ussd-treasury/README.md b/blockchain/modules/ecdp-ussd-treasury/README.md index b08f666c..ac94dad6 100644 --- a/blockchain/modules/ecdp-ussd-treasury/README.md +++ b/blockchain/modules/ecdp-ussd-treasury/README.md @@ -3,3 +3,5 @@ ## Overview Provides USD-Pegged ECDP `Slick USD (USSD)` Stablecoin on Ethical DeFi. + +The Slick USD is Ethical DeFi's USD-pegged stablecoin, built on the `ECDP` (Ethical Collateralised Debt Position), the Pegged ECDP Stablecoin Protocol of the Ethical DeFi Suite is inspired by MakerDAO's design, natively built on the Setheum Network with Zero-interest loans with optimisations and native liquidation protocols which include our on-chain built-in DEX Edfis, a native auction system, as well as an on-chain native liquidation protection mechanism. The SlickUSD is over-collateralised and multi-collateralised. diff --git a/blockchain/modules/edfis-x/Cargo.toml b/blockchain/modules/edfis-liquid-edf-validators/Cargo.toml similarity index 64% rename from blockchain/modules/edfis-x/Cargo.toml rename to blockchain/modules/edfis-liquid-edf-validators/Cargo.toml index 7faac12e..ce937788 100644 --- a/blockchain/modules/edfis-x/Cargo.toml +++ b/blockchain/modules/edfis-liquid-edf-validators/Cargo.toml @@ -1,6 +1,5 @@ [package] -name = "module-edfis-x" -description = "Provides cross-chain multichain Swaps on Edfis Exchange." +name = "module-edfis-liquid-edf-validators" version = "0.9.81-dev" authors.workspace = true edition.workspace = true @@ -8,33 +7,38 @@ homepage.workspace = true repository.workspace = true [dependencies] +scale-info = { workspace = true } +serde = { workspace = true, optional = true } parity-scale-codec = { version = "3.0.0", default-features = false, features = ["max-encoded-len"] } sp-runtime = { workspace = true } -sp-io = { workspace = true } sp-std = { workspace = true } frame-support = { workspace = true } frame-system = { workspace = true } -primitives = { package = "setheum-primitives", path = "../primitives", default-features = false } -support = { package = "module-support", path = "../support", default-features = false } -orml-traits = { path = "../submodules/orml/traits", default-features = false } +orml-traits = { workspace = true, default-features = false } + +primitives = { workspace = true, default-features = false } +module-support = { workspace = true, default-features = false } [dev-dependencies] sp-core = { workspace = true, features = ["std"] } +sp-io = { workspace = true, features = ["std"] } pallet-balances = { workspace = true } +orml-currencies = { workspace = true, features = ["std"] } orml-tokens = { workspace = true } [features] default = ["std"] std = [ + "scale-info/std", + "serde", "parity-scale-codec/std", "sp-runtime/std", "sp-std/std", - "sp-io/std", "frame-support/std", "frame-system/std", "primitives/std", - "support/std", + "module-support/std", "orml-traits/std", ] runtime-benchmarks = [ @@ -45,5 +49,4 @@ runtime-benchmarks = [ try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", - "sp-runtime/try-runtime", ] diff --git a/blockchain/modules/edfis-liquid-edf-validators/README.md b/blockchain/modules/edfis-liquid-edf-validators/README.md new file mode 100644 index 00000000..6f0eb820 --- /dev/null +++ b/blockchain/modules/edfis-liquid-edf-validators/README.md @@ -0,0 +1,7 @@ +بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيم + +# Edfis Liquid EDF Validators Module + +## Overview + +Provides a liquid staking platform on Ethical DeFi for `EDF` tokens. The module requires validators to lock some Liquid EDF into insurance fund and if slash happened, EthicalDeFiCouncil can burn those Liquid EDF to compensate Liquid EDF holders. diff --git a/blockchain/modules/lockdrop/TODO.md b/blockchain/modules/edfis-liquid-edf-validators/TODO.md similarity index 94% rename from blockchain/modules/lockdrop/TODO.md rename to blockchain/modules/edfis-liquid-edf-validators/TODO.md index c8c94c13..efe9a53b 100644 --- a/blockchain/modules/lockdrop/TODO.md +++ b/blockchain/modules/edfis-liquid-edf-validators/TODO.md @@ -54,3 +54,4 @@ These tasks are just for this file specifically. - [x] [[TODO.md:0] - Add TODO.md File](TODO.md): Add a TODO.md file to organise TODOs in the repo. - [x] [[TODO.md:1] - Add a `task_title`](/TODO.md/#tasks): Adda `task_title`. +- [ ] [[src/lib.rs:0]: Do benchmarking test](src/lib.rs) diff --git a/blockchain/modules/edfis-liquid-edf-validators/src/lib.rs b/blockchain/modules/edfis-liquid-edf-validators/src/lib.rs new file mode 100644 index 00000000..1b472b6f --- /dev/null +++ b/blockchain/modules/edfis-liquid-edf-validators/src/lib.rs @@ -0,0 +1,575 @@ +// بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيم + +// This file is part of Setheum. + +// Copyright (C) 2019-Present Setheum Labs. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// 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 . + +//! # Edfis Liquid EDF Validator List Module +//! +//! ## Overview +//! +//! This will require validators to lock some Liquid EDF into insurance fund +//! and if slash happened, EthicalDeFiCouncil can burn those Liquid EDF to compensate +//! Liquid EDF holders. + +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(clippy::unused_unit)] +#![allow(clippy::collapsible_if)] + +use frame_support::{pallet_prelude::*, traits::Contains}; +use frame_system::pallet_prelude::*; +use module_support::{ExchangeRateProvider, Ratio}; +use orml_traits::{BasicCurrency, BasicLockableCurrency, Happened, LockIdentifier}; +use parity_scale_codec::MaxEncodedLen; +use primitives::Balance; +use scale_info::TypeInfo; +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; +use sp_runtime::{ + traits::{BlockNumberProvider, Bounded, MaybeDisplay, MaybeSerializeDeserialize, Member, Zero}, + DispatchResult, FixedPointNumber, RuntimeDebug, +}; +use sp_std::{fmt::Debug, vec::Vec}; + +mod mock; +mod tests; + +pub use module::*; + +pub const EDFIS_LIQUID_STAKING_VALIDATOR_LIST_ID: LockIdentifier = *b"edf/lqdedfvld"; + +pub trait WeightInfo { + fn bond() -> Weight; + fn unbond() -> Weight; + fn rebond() -> Weight; + fn withdraw_unbonded() -> Weight; + fn freeze(u: u32) -> Weight; + fn thaw() -> Weight; + fn slash() -> Weight; +} + +// TODO[src/lib.rs:0]: Do benchmarking test. +impl WeightInfo for () { + fn bond() -> Weight { + Weight::from_parts(10_000, 0) + } + fn unbond() -> Weight { + Weight::from_parts(10_000, 0) + } + fn rebond() -> Weight { + Weight::from_parts(10_000, 0) + } + fn withdraw_unbonded() -> Weight { + Weight::from_parts(10_000, 0) + } + fn freeze(_u: u32) -> Weight { + Weight::from_parts(10_000, 0) + } + fn thaw() -> Weight { + Weight::from_parts(10_000, 0) + } + fn slash() -> Weight { + Weight::from_parts(10_000, 0) + } +} + +/// Insurance for a validator from a single address +#[derive(Encode, Decode, Clone, Copy, RuntimeDebug, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)] +pub struct Guarantee { + /// The total tokens the validator has in insurance + total: Balance, + /// The number of tokens that are actively bonded for insurance + bonded: Balance, + /// The number of tokens that are in the process of unbonding for insurance + unbonding: Option<(Balance, BlockNumber)>, +} + +impl Guarantee { + /// Take `unbonding` that are sufficiently old + fn consolidate_unbonding(mut self, current_block: BlockNumber) -> Self { + match self.unbonding { + Some((_, expired_block)) if expired_block <= current_block => { + self.unbonding = None; + } + _ => {} + } + self + } + + /// Re-bond funds that were scheduled for unbonding. + fn rebond(mut self, rebond_amount: Balance) -> Self { + if let Some((amount, _)) = self.unbonding.as_mut() { + let rebond_amount = rebond_amount.min(*amount); + self.bonded = self.bonded.saturating_add(rebond_amount); + *amount = amount.saturating_sub(rebond_amount); + if amount.is_zero() { + self.unbonding = None; + } + } + self + } + + fn slash(mut self, slash_amount: Balance) -> Self { + let mut remains = slash_amount; + let slash_from_bonded = self.bonded.min(remains); + self.bonded = self.bonded.saturating_sub(remains); + self.total = self.total.saturating_sub(remains); + remains = remains.saturating_sub(slash_from_bonded); + + if !remains.is_zero() { + if let Some((unbonding_amount, _)) = self.unbonding.as_mut() { + let slash_from_unbonding = remains.min(*unbonding_amount); + *unbonding_amount = unbonding_amount.saturating_sub(slash_from_unbonding); + if unbonding_amount.is_zero() { + self.unbonding = None; + } + } + } + + self + } +} + +/// Information on a validator's slash +#[derive(Encode, Decode, Clone, RuntimeDebug, Eq, PartialEq, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct SlashInfo { + /// Address of a validator + validator: AccountId, + /// The amount of tokens a validator has in backing + token_amount: Balance, +} + +/// Validator insurance and frozen status +#[derive(Encode, Decode, Clone, Copy, RuntimeDebug, Default, MaxEncodedLen, TypeInfo)] +pub struct ValidatorBacking { + /// Total insurance from all guarantors + total_insurance: Balance, + is_frozen: bool, +} + +#[frame_support::pallet] +pub mod module { + use super::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// The liquid representation of the staking token. + type LiquidEDFCurrency: BasicLockableCurrency; + #[pallet::constant] + /// The minimum amount of tokens that can be bonded to a validator. + type MinBondAmount: Get; + #[pallet::constant] + /// The number of blocks a token is bonded to a validator for. + type BondingDuration: Get>; + #[pallet::constant] + /// The minimum amount of insurance a validator needs. + type ValidatorInsuranceThreshold: Get; + /// The AccountId that can perform a freeze. + type FreezeOrigin: EnsureOrigin; + /// The AccountId that can perform a slash. + type SlashOrigin: EnsureOrigin; + /// Callback to be called when a slash occurs. + type OnSlash: Happened; + /// Exchange rate between staked token and liquid token equivalent. + type LiquidStakingExchangeRateProvider: ExchangeRateProvider; + type WeightInfo: WeightInfo; + /// Callback to be called when a validator's insurance increases. + type OnIncreaseGuarantee: Happened<(Self::AccountId, Self::AccountId, Balance)>; + /// Callback to be called when a validator's insurance decreases. + type OnDecreaseGuarantee: Happened<(Self::AccountId, Self::AccountId, Balance)>; + + // The block number provider + type BlockNumberProvider: BlockNumberProvider>; + } + + #[pallet::error] + pub enum Error { + BelowMinBondAmount, + UnbondingExists, + FrozenValidator, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + FreezeValidator { + validator: T::AccountId, + }, + ThawValidator { + validator: T::AccountId, + }, + BondGuarantee { + who: T::AccountId, + validator: T::AccountId, + bond: Balance, + }, + UnbondGuarantee { + who: T::AccountId, + validator: T::AccountId, + bond: Balance, + }, + WithdrawnGuarantee { + who: T::AccountId, + validator: T::AccountId, + bond: Balance, + }, + SlashGuarantee { + who: T::AccountId, + validator: T::AccountId, + bond: Balance, + }, + } + + /// The slash guarantee deposits for validators. + /// + /// Guarantees: double_map AccountId, AccountId => Option + #[pallet::storage] + #[pallet::getter(fn guarantees)] + pub type Guarantees = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, + Twox64Concat, + T::AccountId, + Guarantee>, + OptionQuery, + >; + + /// Total deposits for users. + /// + /// TotalLockedByGuarantor: map AccountId => Option + #[pallet::storage] + #[pallet::getter(fn total_locked_by_guarantor)] + pub type TotalLockedByGuarantor = StorageMap<_, Twox64Concat, T::AccountId, Balance, OptionQuery>; + + /// Total deposit for validators. + /// + /// ValidatorBackings: map AccountId => Option + #[pallet::storage] + #[pallet::getter(fn validator_backings)] + pub type ValidatorBackings = + StorageMap<_, Blake2_128Concat, T::AccountId, ValidatorBacking, OptionQuery>; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::hooks] + impl Hooks> for Pallet {} + + #[pallet::call] + impl Pallet { + /// Bond tokens to a validator. + /// Ensures the amount to bond is greater than the minimum bond amount. + /// + /// - `validator`: the AccountId of a validator to bond to + /// - `amount`: the number of tokens to bond to the given validator + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::bond())] + pub fn bond( + origin: OriginFor, + validator: T::AccountId, + #[pallet::compact] amount: Balance, + ) -> DispatchResult { + let guarantor = ensure_signed(origin)?; + let free_balance = T::LiquidEDFCurrency::free_balance(&guarantor); + let total_should_locked = Self::total_locked_by_guarantor(&guarantor).unwrap_or_default(); + + if let Some(extra) = free_balance.checked_sub(total_should_locked) { + let amount = amount.min(extra); + + if !amount.is_zero() { + Self::update_guarantee(&guarantor, &validator, |guarantee| -> DispatchResult { + guarantee.total = guarantee.total.saturating_add(amount); + guarantee.bonded = guarantee.bonded.saturating_add(amount); + ensure!( + guarantee.bonded >= T::MinBondAmount::get(), + Error::::BelowMinBondAmount + ); + Ok(()) + })?; + Self::deposit_event(Event::BondGuarantee { + who: guarantor, + validator: validator.clone(), + bond: amount, + }); + } + } + Ok(()) + } + + /// Unbond tokens from a validator. + /// Ensures the bonded amount is zero or greater than the minimum bond amount. + /// + /// - `validator`: the AccountId of a validator to unbond from + /// - `amount`: the number of tokens to unbond from the given validator + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::unbond())] + pub fn unbond( + origin: OriginFor, + validator: T::AccountId, + #[pallet::compact] amount: Balance, + ) -> DispatchResult { + let guarantor = ensure_signed(origin)?; + + if !amount.is_zero() { + Self::update_guarantee(&guarantor, &validator, |guarantee| -> DispatchResult { + ensure!(guarantee.unbonding.is_none(), Error::::UnbondingExists); + let amount = amount.min(guarantee.bonded); + guarantee.bonded = guarantee.bonded.saturating_sub(amount); + ensure!( + guarantee.bonded.is_zero() || guarantee.bonded >= T::MinBondAmount::get(), + Error::::BelowMinBondAmount, + ); + let expired_block = T::BlockNumberProvider::current_block_number() + T::BondingDuration::get(); + guarantee.unbonding = Some((amount, expired_block)); + + Self::deposit_event(Event::UnbondGuarantee { + who: guarantor.clone(), + validator: validator.clone(), + bond: amount, + }); + Ok(()) + })?; + } + Ok(()) + } + + /// Rebond tokens to a validator. + /// + /// - `validator`: The AccountId of a validator to rebond to + /// - `amount`: The amount of tokens to to rebond to the given validator + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::rebond())] + pub fn rebond( + origin: OriginFor, + validator: T::AccountId, + #[pallet::compact] amount: Balance, + ) -> DispatchResult { + let guarantor = ensure_signed(origin)?; + + if !amount.is_zero() { + Self::update_guarantee(&guarantor, &validator, |guarantee| -> DispatchResult { + *guarantee = guarantee.rebond(amount); + Ok(()) + })?; + } + Ok(()) + } + + /// Withdraw the unbonded tokens from a validator. + /// Ensures the validator is not frozen. + /// + /// - `validator`: The AccountId of a validator to withdraw from + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::withdraw_unbonded())] + pub fn withdraw_unbonded(origin: OriginFor, validator: T::AccountId) -> DispatchResult { + let guarantor = ensure_signed(origin)?; + ensure!( + !Self::validator_backings(&validator).unwrap_or_default().is_frozen, + Error::::FrozenValidator + ); + Self::update_guarantee(&guarantor, &validator, |guarantee| -> DispatchResult { + let old_total = guarantee.total; + *guarantee = guarantee.consolidate_unbonding(T::BlockNumberProvider::current_block_number()); + let new_total = guarantee + .bonded + .saturating_add(guarantee.unbonding.unwrap_or_default().0); + if old_total != new_total { + guarantee.total = new_total; + Self::deposit_event(Event::WithdrawnGuarantee { + who: guarantor.clone(), + validator: validator.clone(), + bond: old_total.saturating_sub(new_total), + }); + } + Ok(()) + })?; + Ok(()) + } + + /// Freezes validators if they are not already frozen. + /// Ensures the caller can freeze validators. + /// + /// - `validators`: The AccountIds of the validators to freeze + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::freeze(validators.len() as u32))] + pub fn freeze(origin: OriginFor, validators: Vec) -> DispatchResult { + T::FreezeOrigin::ensure_origin(origin)?; + validators.iter().for_each(|validator| { + ValidatorBackings::::mutate_exists(validator, |maybe_validator| { + let mut v = maybe_validator.take().unwrap_or_default(); + if !v.is_frozen { + v.is_frozen = true; + Self::deposit_event(Event::FreezeValidator { + validator: validator.clone(), + }); + } + *maybe_validator = Some(v); + }); + }); + Ok(()) + } + + /// Unfreezes validators if they are frozen. + /// Ensures the caller can perform a slash. + /// + /// - `validators`: The AccountIds of the validators to unfreeze + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::thaw())] + pub fn thaw(origin: OriginFor, validators: Vec) -> DispatchResult { + // Using SlashOrigin instead of FreezeOrigin so that un-freezing requires more council members than + // freezing + T::SlashOrigin::ensure_origin(origin)?; + validators.iter().for_each(|validator| { + ValidatorBackings::::mutate_exists(validator, |maybe_validator| { + let mut v = maybe_validator.take().unwrap_or_default(); + if v.is_frozen { + v.is_frozen = false; + Self::deposit_event(Event::ThawValidator { + validator: validator.clone(), + }); + } + *maybe_validator = Some(v); + }); + }); + Ok(()) + } + + /// Slash validators. + /// Ensures the the caller can perform a slash. + /// + /// - `slashes`: The SlashInfos of the validators to be slashed + #[pallet::call_index(6)] + #[pallet::weight(T::WeightInfo::slash())] + pub fn slash(origin: OriginFor, slashes: Vec>) -> DispatchResult { + T::SlashOrigin::ensure_origin(origin)?; + let liquid_staking_exchange_rate = T::LiquidStakingExchangeRateProvider::get_exchange_rate(); + let staking_liquid_exchange_rate = liquid_staking_exchange_rate.reciprocal().unwrap_or_default(); + let mut actual_total_slashing: Balance = Zero::zero(); + + for SlashInfo { + validator, + token_amount, + } in slashes + { + let ValidatorBacking { total_insurance, .. } = Self::validator_backings(&validator).unwrap_or_default(); + let insurance_loss = staking_liquid_exchange_rate + .saturating_mul_int(token_amount) + .min(total_insurance); + + for (guarantor, _) in Guarantees::::iter_prefix(&validator) { + // NOTE: ignoring result because the closure will not throw err. + let res = Self::update_guarantee(&guarantor, &validator, |guarantee| -> DispatchResult { + let should_slashing = Ratio::checked_from_rational(guarantee.total, total_insurance) + .unwrap_or_else(Ratio::max_value) + .saturating_mul_int(insurance_loss); + let gap = T::LiquidEDFCurrency::slash(&guarantor, should_slashing); + let actual_slashing = should_slashing.saturating_sub(gap); + *guarantee = guarantee.slash(actual_slashing); + Self::deposit_event(Event::SlashGuarantee { + who: guarantor.clone(), + validator: validator.clone(), + bond: actual_slashing, + }); + actual_total_slashing = actual_total_slashing.saturating_add(actual_slashing); + Ok(()) + }); + debug_assert!(res.is_ok()); + } + } + + T::OnSlash::happened(&actual_total_slashing); + Ok(()) + } + } +} + +impl Pallet { + fn update_guarantee( + guarantor: &T::AccountId, + validator: &T::AccountId, + f: impl FnOnce(&mut Guarantee>) -> DispatchResult, + ) -> DispatchResult { + Guarantees::::try_mutate_exists(validator, guarantor, |maybe_guarantee| -> DispatchResult { + let mut guarantee = maybe_guarantee.take().unwrap_or_default(); + let old_total = guarantee.total; + + f(&mut guarantee).and_then(|_| -> DispatchResult { + let new_total = guarantee.total; + if guarantee.total.is_zero() { + *maybe_guarantee = None; + } else { + *maybe_guarantee = Some(guarantee); + } + + // adjust total locked of nominator, validator backing and update the lock. + if new_total != old_total { + TotalLockedByGuarantor::::try_mutate_exists( + guarantor, + |maybe_total_locked| -> DispatchResult { + let mut tl = maybe_total_locked.take().unwrap_or_default(); + + ValidatorBackings::::try_mutate_exists( + validator, + |maybe_validator_backing| -> DispatchResult { + let mut vb = maybe_validator_backing.take().unwrap_or_default(); + + if new_total > old_total { + let gap = new_total - old_total; + vb.total_insurance = vb.total_insurance.saturating_add(gap); + tl = tl.saturating_add(gap); + T::OnIncreaseGuarantee::happened(&(guarantor.clone(), validator.clone(), gap)); + } else { + let gap = old_total - new_total; + vb.total_insurance = vb.total_insurance.saturating_sub(gap); + tl = tl.saturating_sub(gap); + T::OnDecreaseGuarantee::happened(&(guarantor.clone(), validator.clone(), gap)); + }; + + if tl.is_zero() { + *maybe_total_locked = None; + T::LiquidEDFCurrency::remove_lock(EDFIS_LIQUID_STAKING_VALIDATOR_LIST_ID, guarantor)?; + } else { + *maybe_total_locked = Some(tl); + T::LiquidEDFCurrency::set_lock(EDFIS_LIQUID_STAKING_VALIDATOR_LIST_ID, guarantor, tl)?; + } + + *maybe_validator_backing = Some(vb); + Ok(()) + }, + ) + }, + )?; + } + + Ok(()) + }) + }) + } +} + +impl Contains for Pallet { + fn contains(account_id: &T::AccountId) -> bool { + Self::validator_backings(account_id) + .unwrap_or_default() + .total_insurance + >= T::ValidatorInsuranceThreshold::get() + } +} diff --git a/blockchain/modules/edfis-liquid-edf-validators/src/mock.rs b/blockchain/modules/edfis-liquid-edf-validators/src/mock.rs new file mode 100644 index 00000000..77ab6593 --- /dev/null +++ b/blockchain/modules/edfis-liquid-edf-validators/src/mock.rs @@ -0,0 +1,236 @@ +// بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيم + +// This file is part of Setheum. + +// Copyright (C) 2019-Present Setheum Labs. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// 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 . + +//! Mocks for Edfis Liquid EDF Validator List Module. + +#![cfg(test)] + +use super::*; +use frame_support::{ + construct_runtime, derive_impl, ord_parameter_types, parameter_types, + traits::{ConstU128, ConstU32, ConstU64, Nothing}, +}; +use frame_system::EnsureSignedBy; +use module_support::ExchangeRate; +use orml_traits::parameter_type_with_key; +use primitives::{Amount, Balance, CurrencyId, TokenSymbol}; +use sp_runtime::{traits::IdentityLookup, BuildStorage}; +use sp_std::cell::RefCell; +use std::collections::HashMap; + +pub type AccountId = u128; +pub type BlockNumber = u64; + +pub const ALICE: AccountId = 0; +pub const BOB: AccountId = 1; +pub const VALIDATOR_1: AccountId = 2; +pub const VALIDATOR_2: AccountId = 3; +pub const VALIDATOR_3: AccountId = 4; +pub const SEE: CurrencyId = CurrencyId::Token(TokenSymbol::SEE); +pub const LEDF: CurrencyId = CurrencyId::Token(TokenSymbol::LEDF); + +mod edfis_liquid_edf_validator_list { + pub use super::super::*; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl frame_system::Config for Runtime { + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Block = Block; + type AccountData = pallet_balances::AccountData; +} + +parameter_type_with_key! { + pub ExistentialDeposits: |_currency_id: CurrencyId| -> Balance { + Default::default() + }; +} + +impl orml_tokens::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type Amount = Amount; + type CurrencyId = CurrencyId; + type WeightInfo = (); + type ExistentialDeposits = ExistentialDeposits; + type CurrencyHooks = (); + type MaxLocks = ConstU32<100>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type DustRemovalWhitelist = Nothing; +} + +impl pallet_balances::Config for Runtime { + type Balance = Balance; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ConstU128<1>; + type AccountStore = System; + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeFreezeReason = RuntimeFreezeReason; + type FreezeIdentifier = (); + type MaxHolds = (); + type MaxFreezes = (); +} + +parameter_types! { + pub const GetNativeCurrencyId: CurrencyId = SEE; + pub const GetLiquidCurrencyId: CurrencyId = LEDF; +} + +pub type NativeCurrency = orml_currencies::BasicCurrencyAdapter; +pub type LEDFCurrency = orml_currencies::Currency; + +impl orml_currencies::Config for Runtime { + type MultiCurrency = OrmlTokens; + type NativeCurrency = NativeCurrency; + type GetNativeCurrencyId = GetNativeCurrencyId; + type WeightInfo = (); +} + +thread_local! { + pub static SHARES: RefCell> = RefCell::new(HashMap::new()); + pub static ACCUMULATED_SLASH: RefCell = RefCell::new(0); +} + +pub struct MockOnSlash; +impl Happened for MockOnSlash { + fn happened(amount: &Balance) { + ACCUMULATED_SLASH.with(|v| *v.borrow_mut() += amount); + } +} + +pub struct MockOnIncreaseGuarantee; +impl Happened<(AccountId, AccountId, Balance)> for MockOnIncreaseGuarantee { + fn happened(info: &(AccountId, AccountId, Balance)) { + let (account_id, validator_account_id, amount) = info; + SHARES.with(|v| { + let mut old_map = v.borrow().clone(); + if let Some(share) = old_map.get_mut(&(*account_id, *validator_account_id)) { + *share = share.saturating_add(*amount); + } else { + old_map.insert((*account_id, *validator_account_id), *amount); + }; + + *v.borrow_mut() = old_map; + }); + } +} + +pub struct MockOnDecreaseGuarantee; +impl Happened<(AccountId, AccountId, Balance)> for MockOnDecreaseGuarantee { + fn happened(info: &(AccountId, AccountId, Balance)) { + let (account_id, validator_account_id, amount) = info; + SHARES.with(|v| { + let mut old_map = v.borrow().clone(); + if let Some(share) = old_map.get_mut(&(*account_id, *validator_account_id)) { + *share = share.saturating_sub(*amount); + } else { + old_map.insert((*account_id, *validator_account_id), Default::default()); + }; + + *v.borrow_mut() = old_map; + }); + } +} + +pub struct MockLiquidStakingExchangeProvider; +impl ExchangeRateProvider for MockLiquidStakingExchangeProvider { + fn get_exchange_rate() -> ExchangeRate { + ExchangeRate::saturating_from_rational(1, 2) + } +} + +parameter_types! { + pub static MockBlockNumberProvider: u64 = 0; +} + +impl BlockNumberProvider for MockBlockNumberProvider { + type BlockNumber = u64; + + fn current_block_number() -> Self::BlockNumber { + Self::get() + } +} + +ord_parameter_types! { + pub const Admin: AccountId = 10; +} + +impl Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type LiquidEDFCurrency = LEDFCurrency; + type MinBondAmount = ConstU128<100>; + type BondingDuration = ConstU64<100>; + type ValidatorInsuranceThreshold = ConstU128<200>; + type FreezeOrigin = EnsureSignedBy; + type SlashOrigin = EnsureSignedBy; + type OnSlash = MockOnSlash; + type LiquidStakingExchangeRateProvider = MockLiquidStakingExchangeProvider; + type WeightInfo = (); + type OnIncreaseGuarantee = MockOnIncreaseGuarantee; + type OnDecreaseGuarantee = MockOnDecreaseGuarantee; + type BlockNumberProvider = MockBlockNumberProvider; +} + +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub enum Runtime { + System: frame_system, + OrmlTokens: orml_tokens, + PalletBalances: pallet_balances, + OrmlCurrencies: orml_currencies, + EdfisLiquidSeeValidatorsModule: edfis_liquid_edf_validator_list, + } +); + +pub struct ExtBuilder { + balances: Vec<(AccountId, CurrencyId, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { + balances: vec![(ALICE, LEDF, 1000), (BOB, LEDF, 1000)], + } + } +} + +impl ExtBuilder { + pub fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .unwrap(); + + orml_tokens::GenesisConfig:: { + balances: self.balances, + } + .assimilate_storage(&mut t) + .unwrap(); + + t.into() + } +} diff --git a/blockchain/modules/edfis-liquid-edf-validators/src/tests.rs b/blockchain/modules/edfis-liquid-edf-validators/src/tests.rs new file mode 100644 index 00000000..2a281a56 --- /dev/null +++ b/blockchain/modules/edfis-liquid-edf-validators/src/tests.rs @@ -0,0 +1,881 @@ +// بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيم + +// This file is part of Setheum. + +// Copyright (C) 2019-Present Setheum Labs. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// 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 . + +//! Unit tests for homa validator list module. + +#![cfg(test)] + +use super::*; +use frame_support::{assert_noop, assert_ok}; +use mock::*; +use sp_runtime::traits::BadOrigin; + +#[test] +fn guarantee_work() { + ExtBuilder::default().build().execute_with(|| { + let guarantee = Guarantee { + total: 1000, + bonded: 800, + unbonding: Some((200, 10)), + }; + + assert_eq!(guarantee.consolidate_unbonding(9).unbonding, Some((200, 10))); + assert_eq!(guarantee.consolidate_unbonding(10).unbonding, None); + + assert_eq!( + guarantee.rebond(50), + Guarantee { + total: 1000, + bonded: 850, + unbonding: Some((150, 10)), + } + ); + assert_eq!( + guarantee.rebond(200), + Guarantee { + total: 1000, + bonded: 1000, + unbonding: None, + } + ); + + assert_eq!( + guarantee.slash(200), + Guarantee { + total: 800, + bonded: 600, + unbonding: Some((200, 10)), + } + ); + assert_eq!( + guarantee.slash(850), + Guarantee { + total: 150, + bonded: 0, + unbonding: Some((150, 10)), + } + ); + assert_eq!( + guarantee.slash(1000), + Guarantee { + total: 0, + bonded: 0, + unbonding: None, + } + ); + }); +} + +#[test] +fn freeze_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + assert_noop!( + EdfisLiquidSeeValidatorsModule::freeze( + RuntimeOrigin::signed(ALICE), + vec![VALIDATOR_1, VALIDATOR_2, VALIDATOR_3] + ), + BadOrigin + ); + + assert!( + !EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .is_frozen, + ); + assert!( + !EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_2) + .unwrap_or_default() + .is_frozen, + ); + assert!( + !EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_3) + .unwrap_or_default() + .is_frozen, + ); + assert_ok!(EdfisLiquidSeeValidatorsModule::freeze( + RuntimeOrigin::signed(10), + vec![VALIDATOR_1, VALIDATOR_2, VALIDATOR_3] + )); + assert!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .is_frozen + ); + assert!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_2) + .unwrap_or_default() + .is_frozen + ); + assert!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_3) + .unwrap_or_default() + .is_frozen + ); + + System::assert_has_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::FreezeValidator { validator: VALIDATOR_1 }, + )); + System::assert_has_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::FreezeValidator { validator: VALIDATOR_2 }, + )); + System::assert_has_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::FreezeValidator { validator: VALIDATOR_3 }, + )); + }); +} + +#[test] +fn thaw_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + assert_noop!( + EdfisLiquidSeeValidatorsModule::thaw( + RuntimeOrigin::signed(ALICE), + vec![VALIDATOR_1, VALIDATOR_2, VALIDATOR_3] + ), + BadOrigin + ); + + assert_ok!(EdfisLiquidSeeValidatorsModule::freeze( + RuntimeOrigin::signed(10), + vec![VALIDATOR_1, VALIDATOR_2] + )); + assert!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .is_frozen + ); + assert!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_2) + .unwrap_or_default() + .is_frozen + ); + assert!( + !EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_3) + .unwrap_or_default() + .is_frozen + ); + assert_ok!(EdfisLiquidSeeValidatorsModule::thaw( + RuntimeOrigin::signed(10), + vec![VALIDATOR_1, VALIDATOR_2, VALIDATOR_3] + )); + assert!( + !EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .is_frozen + ); + assert!( + !EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_2) + .unwrap_or_default() + .is_frozen + ); + assert!( + !EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_3) + .unwrap_or_default() + .is_frozen + ); + System::assert_has_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::ThawValidator { validator: VALIDATOR_1 }, + )); + System::assert_has_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::ThawValidator { validator: VALIDATOR_2 }, + )); + }); +} + +#[test] +fn bond_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + assert_noop!( + EdfisLiquidSeeValidatorsModule::bond(RuntimeOrigin::signed(ALICE), VALIDATOR_1, 99), + Error::::BelowMinBondAmount + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 0, + bonded: 0, + unbonding: None + } + ); + assert_eq!(OrmlTokens::accounts(ALICE, LEDF).frozen, 0); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 0 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 0 + ); + assert_eq!(SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), 0); + + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 100 + )); + System::assert_last_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::BondGuarantee { + who: ALICE, + validator: VALIDATOR_1, + bond: 100, + }, + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 100, + bonded: 100, + unbonding: None + } + ); + assert_eq!(OrmlTokens::accounts(ALICE, LEDF).frozen, 100); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 100 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 100 + ); + assert_eq!( + SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), + 100 + ); + + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, BOB).unwrap_or_default(), + Guarantee { + total: 0, + bonded: 0, + unbonding: None + } + ); + assert_eq!(OrmlTokens::accounts(BOB, LEDF).frozen, 0); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(BOB).unwrap_or_default(), + 0 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 100 + ); + assert_eq!(SHARES.with(|v| *v.borrow().get(&(BOB, VALIDATOR_1)).unwrap_or(&0)), 0); + + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(BOB), + VALIDATOR_1, + 300 + )); + System::assert_last_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::BondGuarantee { + who: BOB, + validator: VALIDATOR_1, + bond: 300, + }, + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, BOB).unwrap_or_default(), + Guarantee { + total: 300, + bonded: 300, + unbonding: None + } + ); + assert_eq!(OrmlTokens::accounts(BOB, LEDF).frozen, 300); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(BOB).unwrap_or_default(), + 300 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 400 + ); + assert_eq!(SHARES.with(|v| *v.borrow().get(&(BOB, VALIDATOR_1)).unwrap_or(&0)), 300); + + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_2, BOB).unwrap_or_default(), + Guarantee { + total: 0, + bonded: 0, + unbonding: None + } + ); + assert_eq!(OrmlTokens::accounts(BOB, LEDF).frozen, 300); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(BOB).unwrap_or_default(), + 300 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_2) + .unwrap_or_default() + .total_insurance, + 0 + ); + assert_eq!(SHARES.with(|v| *v.borrow().get(&(BOB, VALIDATOR_2)).unwrap_or(&0)), 0); + + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(BOB), + VALIDATOR_2, + 200 + )); + System::assert_last_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::BondGuarantee { + who: BOB, + validator: VALIDATOR_2, + bond: 200, + }, + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_2, BOB).unwrap_or_default(), + Guarantee { + total: 200, + bonded: 200, + unbonding: None + } + ); + assert_eq!(OrmlTokens::accounts(BOB, LEDF).frozen, 500); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(BOB).unwrap_or_default(), + 500 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_2) + .unwrap_or_default() + .total_insurance, + 200 + ); + assert_eq!(SHARES.with(|v| *v.borrow().get(&(BOB, VALIDATOR_2)).unwrap_or(&0)), 200); + }); +} + +#[test] +fn unbond_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + MockBlockNumberProvider::set(1); + + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 200 + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 200, + bonded: 200, + unbonding: None + } + ); + assert_eq!(OrmlTokens::accounts(ALICE, LEDF).frozen, 200); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 200 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 200 + ); + assert_eq!( + SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), + 200 + ); + + assert_noop!( + EdfisLiquidSeeValidatorsModule::unbond(RuntimeOrigin::signed(ALICE), VALIDATOR_1, 199), + Error::::BelowMinBondAmount + ); + + assert_ok!(EdfisLiquidSeeValidatorsModule::unbond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 100 + )); + System::assert_last_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::UnbondGuarantee { + who: ALICE, + validator: VALIDATOR_1, + bond: 100, + }, + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 200, + bonded: 100, + unbonding: Some((100, 101)) + } + ); + assert_eq!(OrmlTokens::accounts(ALICE, LEDF).frozen, 200); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 200 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 200 + ); + assert_eq!( + SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), + 200 + ); + + assert_noop!( + EdfisLiquidSeeValidatorsModule::unbond(RuntimeOrigin::signed(ALICE), VALIDATOR_1, 100), + Error::::UnbondingExists + ); + }); +} + +#[test] +fn rebond_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + MockBlockNumberProvider::set(1); + + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 200 + )); + assert_ok!(EdfisLiquidSeeValidatorsModule::unbond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 100 + )); + + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 200, + bonded: 100, + unbonding: Some((100, 101)) + } + ); + assert_eq!(OrmlTokens::accounts(ALICE, LEDF).frozen, 200); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 200 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 200 + ); + assert_eq!( + SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), + 200 + ); + + assert_ok!(EdfisLiquidSeeValidatorsModule::rebond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 50 + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 200, + bonded: 150, + unbonding: Some((50, 101)) + } + ); + assert_eq!(OrmlTokens::accounts(ALICE, LEDF).frozen, 200); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 200 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 200 + ); + assert_eq!( + SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), + 200 + ); + }); +} + +#[test] +fn withdraw_unbonded_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + MockBlockNumberProvider::set(1); + + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 200 + )); + assert_ok!(EdfisLiquidSeeValidatorsModule::unbond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 100 + )); + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(BOB), + VALIDATOR_1, + 200 + )); + assert_ok!(EdfisLiquidSeeValidatorsModule::unbond( + RuntimeOrigin::signed(BOB), + VALIDATOR_1, + 100 + )); + + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 200, + bonded: 100, + unbonding: Some((100, 101)) + } + ); + assert_eq!(OrmlTokens::accounts(ALICE, LEDF).frozen, 200); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 200 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 400 + ); + assert_eq!( + SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), + 200 + ); + + MockBlockNumberProvider::set(100); + assert_ok!(EdfisLiquidSeeValidatorsModule::withdraw_unbonded( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1 + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 200, + bonded: 100, + unbonding: Some((100, 101)) + } + ); + assert_eq!(OrmlTokens::accounts(ALICE, LEDF).frozen, 200); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 200 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 400 + ); + assert_eq!( + SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), + 200 + ); + System::reset_events(); + MockBlockNumberProvider::set(101); + assert_ok!(EdfisLiquidSeeValidatorsModule::withdraw_unbonded( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1 + )); + System::assert_has_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::WithdrawnGuarantee { + who: ALICE, + validator: VALIDATOR_1, + bond: 100, + }, + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 100, + bonded: 100, + unbonding: None + } + ); + assert_eq!(OrmlTokens::accounts(ALICE, LEDF).frozen, 100); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 100 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 300 + ); + assert_eq!( + SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), + 100 + ); + + assert_ok!(EdfisLiquidSeeValidatorsModule::freeze( + RuntimeOrigin::signed(10), + vec![VALIDATOR_1] + )); + assert_noop!( + EdfisLiquidSeeValidatorsModule::withdraw_unbonded(RuntimeOrigin::signed(BOB), VALIDATOR_1), + Error::::FrozenValidator + ); + }); +} + +#[test] +fn slash_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 100 + )); + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(BOB), + VALIDATOR_1, + 200 + )); + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(BOB), + VALIDATOR_2, + 300 + )); + + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 300 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_2) + .unwrap_or_default() + .total_insurance, + 300 + ); + + // ALICE + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 100, + bonded: 100, + unbonding: None + } + ); + assert_eq!( + SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), + 100 + ); + assert_eq!(OrmlTokens::accounts(ALICE, LEDF).frozen, 100); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 100 + ); + + // BOB + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, BOB).unwrap_or_default(), + Guarantee { + total: 200, + bonded: 200, + unbonding: None + } + ); + assert_eq!(SHARES.with(|v| *v.borrow().get(&(BOB, VALIDATOR_1)).unwrap_or(&0)), 200); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_2, BOB).unwrap_or_default(), + Guarantee { + total: 300, + bonded: 300, + unbonding: None + } + ); + assert_eq!(SHARES.with(|v| *v.borrow().get(&(BOB, VALIDATOR_2)).unwrap_or(&0)), 300); + assert_eq!(OrmlTokens::accounts(BOB, LEDF).frozen, 500); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(BOB).unwrap_or_default(), + 500 + ); + + assert_noop!( + EdfisLiquidSeeValidatorsModule::slash( + RuntimeOrigin::signed(ALICE), + vec![ + SlashInfo { + validator: VALIDATOR_1, + relaychain_token_amount: 90 + }, + SlashInfo { + validator: VALIDATOR_2, + relaychain_token_amount: 50 + }, + ] + ), + BadOrigin + ); + + assert_ok!(EdfisLiquidSeeValidatorsModule::slash( + RuntimeOrigin::signed(10), + vec![ + SlashInfo { + validator: VALIDATOR_1, + relaychain_token_amount: 90 + }, + SlashInfo { + validator: VALIDATOR_2, + relaychain_token_amount: 50 + }, + ] + )); + System::assert_has_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::SlashGuarantee { + who: ALICE, + validator: VALIDATOR_1, + bond: 59, + }, + )); + System::assert_has_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::SlashGuarantee { + who: BOB, + validator: VALIDATOR_1, + bond: 119, + }, + )); + System::assert_has_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::SlashGuarantee { + who: BOB, + validator: VALIDATOR_2, + bond: 100, + }, + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 122 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_2) + .unwrap_or_default() + .total_insurance, + 200 + ); + + // ALICE + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 41, + bonded: 41, + unbonding: None + } + ); + assert_eq!( + SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), + 41 + ); + assert_eq!(OrmlTokens::accounts(ALICE, LEDF).frozen, 41); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 41 + ); + + // BOB + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, BOB).unwrap_or_default(), + Guarantee { + total: 81, + bonded: 81, + unbonding: None + } + ); + assert_eq!(SHARES.with(|v| *v.borrow().get(&(BOB, VALIDATOR_1)).unwrap_or(&0)), 81); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_2, BOB).unwrap_or_default(), + Guarantee { + total: 200, + bonded: 200, + unbonding: None + } + ); + assert_eq!(SHARES.with(|v| *v.borrow().get(&(BOB, VALIDATOR_2)).unwrap_or(&0)), 200); + assert_eq!(OrmlTokens::accounts(BOB, LEDF).frozen, 281); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(BOB).unwrap_or_default(), + 281 + ); + }); +} + +#[test] +fn contains_work() { + ExtBuilder::default().build().execute_with(|| { + MockBlockNumberProvider::set(1); + + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 100 + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 100 + ); + assert!(!EdfisLiquidSeeValidatorsModule::contains(&VALIDATOR_1)); + + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 100 + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 200 + ); + assert!(EdfisLiquidSeeValidatorsModule::contains(&VALIDATOR_1)); + }); +} diff --git a/blockchain/modules/x-wallet/Cargo.toml b/blockchain/modules/edfis-liquid-see-validators/Cargo.toml similarity index 64% rename from blockchain/modules/x-wallet/Cargo.toml rename to blockchain/modules/edfis-liquid-see-validators/Cargo.toml index 1f12f423..0d5dd3fc 100644 --- a/blockchain/modules/x-wallet/Cargo.toml +++ b/blockchain/modules/edfis-liquid-see-validators/Cargo.toml @@ -1,6 +1,5 @@ [package] -name = "module-x-wallet" -description = "Provides a Cross-chain Multichain Wallet on the Setheum Network." +name = "module-edfis-liquid-see-validators" version = "0.9.81-dev" authors.workspace = true edition.workspace = true @@ -8,33 +7,38 @@ homepage.workspace = true repository.workspace = true [dependencies] +scale-info = { workspace = true } +serde = { workspace = true, optional = true } parity-scale-codec = { version = "3.0.0", default-features = false, features = ["max-encoded-len"] } sp-runtime = { workspace = true } -sp-io = { workspace = true } sp-std = { workspace = true } frame-support = { workspace = true } frame-system = { workspace = true } -primitives = { package = "setheum-primitives", path = "../primitives", default-features = false } -support = { package = "module-support", path = "../support", default-features = false } -orml-traits = { path = "../submodules/orml/traits", default-features = false } +orml-traits = { workspace = true, default-features = false } + +primitives = { workspace = true, default-features = false } +module-support = { workspace = true, default-features = false } [dev-dependencies] sp-core = { workspace = true, features = ["std"] } +sp-io = { workspace = true, features = ["std"] } pallet-balances = { workspace = true } +orml-currencies = { workspace = true, features = ["std"] } orml-tokens = { workspace = true } [features] default = ["std"] std = [ + "scale-info/std", + "serde", "parity-scale-codec/std", "sp-runtime/std", "sp-std/std", - "sp-io/std", "frame-support/std", "frame-system/std", "primitives/std", - "support/std", + "module-support/std", "orml-traits/std", ] runtime-benchmarks = [ @@ -45,5 +49,4 @@ runtime-benchmarks = [ try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", - "sp-runtime/try-runtime", ] diff --git a/blockchain/modules/edfis-liquid-see-validators/README.md b/blockchain/modules/edfis-liquid-see-validators/README.md new file mode 100644 index 00000000..03d7e588 --- /dev/null +++ b/blockchain/modules/edfis-liquid-see-validators/README.md @@ -0,0 +1,7 @@ +بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيم + +# Edfis Liquid SEE Validators Module + +## Overview + +Provides a liquid staking platform on Ethical DeFi for `SEE` tokens. The module requires validators to lock some Liquid SEE into insurance fund and if slash happened, EthicalDeFiCouncil can burn those Liquid SEE to compensate Liquid SEE holders. diff --git a/blockchain/modules/highway/TODO.md b/blockchain/modules/edfis-liquid-see-validators/TODO.md similarity index 94% rename from blockchain/modules/highway/TODO.md rename to blockchain/modules/edfis-liquid-see-validators/TODO.md index c8c94c13..efe9a53b 100644 --- a/blockchain/modules/highway/TODO.md +++ b/blockchain/modules/edfis-liquid-see-validators/TODO.md @@ -54,3 +54,4 @@ These tasks are just for this file specifically. - [x] [[TODO.md:0] - Add TODO.md File](TODO.md): Add a TODO.md file to organise TODOs in the repo. - [x] [[TODO.md:1] - Add a `task_title`](/TODO.md/#tasks): Adda `task_title`. +- [ ] [[src/lib.rs:0]: Do benchmarking test](src/lib.rs) diff --git a/blockchain/modules/edfis-liquid-see-validators/src/lib.rs b/blockchain/modules/edfis-liquid-see-validators/src/lib.rs new file mode 100644 index 00000000..bfcd9e1a --- /dev/null +++ b/blockchain/modules/edfis-liquid-see-validators/src/lib.rs @@ -0,0 +1,575 @@ +// بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيم + +// This file is part of Setheum. + +// Copyright (C) 2019-Present Setheum Labs. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// 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 . + +//! # Edfis Liquid SEE Validator List Module +//! +//! ## Overview +//! +//! This will require validators to lock some Liquid SEE into insurance fund +//! and if slash happened, EthicalDeFiCouncil can burn those Liquid SEE to compensate +//! Liquid SEE holders. + +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(clippy::unused_unit)] +#![allow(clippy::collapsible_if)] + +use frame_support::{pallet_prelude::*, traits::Contains}; +use frame_system::pallet_prelude::*; +use module_support::{ExchangeRateProvider, Ratio}; +use orml_traits::{BasicCurrency, BasicLockableCurrency, Happened, LockIdentifier}; +use parity_scale_codec::MaxEncodedLen; +use primitives::Balance; +use scale_info::TypeInfo; +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; +use sp_runtime::{ + traits::{BlockNumberProvider, Bounded, MaybeDisplay, MaybeSerializeDeserialize, Member, Zero}, + DispatchResult, FixedPointNumber, RuntimeDebug, +}; +use sp_std::{fmt::Debug, vec::Vec}; + +mod mock; +mod tests; + +pub use module::*; + +pub const EDFIS_LIQUID_STAKING_VALIDATOR_LIST_ID: LockIdentifier = *b"edf/lqdseevld"; + +pub trait WeightInfo { + fn bond() -> Weight; + fn unbond() -> Weight; + fn rebond() -> Weight; + fn withdraw_unbonded() -> Weight; + fn freeze(u: u32) -> Weight; + fn thaw() -> Weight; + fn slash() -> Weight; +} + +// TODO[src/lib.rs:0]: Do benchmarking test. +impl WeightInfo for () { + fn bond() -> Weight { + Weight::from_parts(10_000, 0) + } + fn unbond() -> Weight { + Weight::from_parts(10_000, 0) + } + fn rebond() -> Weight { + Weight::from_parts(10_000, 0) + } + fn withdraw_unbonded() -> Weight { + Weight::from_parts(10_000, 0) + } + fn freeze(_u: u32) -> Weight { + Weight::from_parts(10_000, 0) + } + fn thaw() -> Weight { + Weight::from_parts(10_000, 0) + } + fn slash() -> Weight { + Weight::from_parts(10_000, 0) + } +} + +/// Insurance for a validator from a single address +#[derive(Encode, Decode, Clone, Copy, RuntimeDebug, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)] +pub struct Guarantee { + /// The total tokens the validator has in insurance + total: Balance, + /// The number of tokens that are actively bonded for insurance + bonded: Balance, + /// The number of tokens that are in the process of unbonding for insurance + unbonding: Option<(Balance, BlockNumber)>, +} + +impl Guarantee { + /// Take `unbonding` that are sufficiently old + fn consolidate_unbonding(mut self, current_block: BlockNumber) -> Self { + match self.unbonding { + Some((_, expired_block)) if expired_block <= current_block => { + self.unbonding = None; + } + _ => {} + } + self + } + + /// Re-bond funds that were scheduled for unbonding. + fn rebond(mut self, rebond_amount: Balance) -> Self { + if let Some((amount, _)) = self.unbonding.as_mut() { + let rebond_amount = rebond_amount.min(*amount); + self.bonded = self.bonded.saturating_add(rebond_amount); + *amount = amount.saturating_sub(rebond_amount); + if amount.is_zero() { + self.unbonding = None; + } + } + self + } + + fn slash(mut self, slash_amount: Balance) -> Self { + let mut remains = slash_amount; + let slash_from_bonded = self.bonded.min(remains); + self.bonded = self.bonded.saturating_sub(remains); + self.total = self.total.saturating_sub(remains); + remains = remains.saturating_sub(slash_from_bonded); + + if !remains.is_zero() { + if let Some((unbonding_amount, _)) = self.unbonding.as_mut() { + let slash_from_unbonding = remains.min(*unbonding_amount); + *unbonding_amount = unbonding_amount.saturating_sub(slash_from_unbonding); + if unbonding_amount.is_zero() { + self.unbonding = None; + } + } + } + + self + } +} + +/// Information on a validator's slash +#[derive(Encode, Decode, Clone, RuntimeDebug, Eq, PartialEq, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct SlashInfo { + /// Address of a validator + validator: AccountId, + /// The amount of tokens a validator has in backing + token_amount: Balance, +} + +/// Validator insurance and frozen status +#[derive(Encode, Decode, Clone, Copy, RuntimeDebug, Default, MaxEncodedLen, TypeInfo)] +pub struct ValidatorBacking { + /// Total insurance from all guarantors + total_insurance: Balance, + is_frozen: bool, +} + +#[frame_support::pallet] +pub mod module { + use super::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// The liquid representation of the staking token. + type LiquidSEECurrency: BasicLockableCurrency; + #[pallet::constant] + /// The minimum amount of tokens that can be bonded to a validator. + type MinBondAmount: Get; + #[pallet::constant] + /// The number of blocks a token is bonded to a validator for. + type BondingDuration: Get>; + #[pallet::constant] + /// The minimum amount of insurance a validator needs. + type ValidatorInsuranceThreshold: Get; + /// The AccountId that can perform a freeze. + type FreezeOrigin: EnsureOrigin; + /// The AccountId that can perform a slash. + type SlashOrigin: EnsureOrigin; + /// Callback to be called when a slash occurs. + type OnSlash: Happened; + /// Exchange rate between staked token and liquid token equivalent. + type LiquidStakingExchangeRateProvider: ExchangeRateProvider; + type WeightInfo: WeightInfo; + /// Callback to be called when a validator's insurance increases. + type OnIncreaseGuarantee: Happened<(Self::AccountId, Self::AccountId, Balance)>; + /// Callback to be called when a validator's insurance decreases. + type OnDecreaseGuarantee: Happened<(Self::AccountId, Self::AccountId, Balance)>; + + // The block number provider + type BlockNumberProvider: BlockNumberProvider>; + } + + #[pallet::error] + pub enum Error { + BelowMinBondAmount, + UnbondingExists, + FrozenValidator, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + FreezeValidator { + validator: T::AccountId, + }, + ThawValidator { + validator: T::AccountId, + }, + BondGuarantee { + who: T::AccountId, + validator: T::AccountId, + bond: Balance, + }, + UnbondGuarantee { + who: T::AccountId, + validator: T::AccountId, + bond: Balance, + }, + WithdrawnGuarantee { + who: T::AccountId, + validator: T::AccountId, + bond: Balance, + }, + SlashGuarantee { + who: T::AccountId, + validator: T::AccountId, + bond: Balance, + }, + } + + /// The slash guarantee deposits for validators. + /// + /// Guarantees: double_map AccountId, AccountId => Option + #[pallet::storage] + #[pallet::getter(fn guarantees)] + pub type Guarantees = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, + Twox64Concat, + T::AccountId, + Guarantee>, + OptionQuery, + >; + + /// Total deposits for users. + /// + /// TotalLockedByGuarantor: map AccountId => Option + #[pallet::storage] + #[pallet::getter(fn total_locked_by_guarantor)] + pub type TotalLockedByGuarantor = StorageMap<_, Twox64Concat, T::AccountId, Balance, OptionQuery>; + + /// Total deposit for validators. + /// + /// ValidatorBackings: map AccountId => Option + #[pallet::storage] + #[pallet::getter(fn validator_backings)] + pub type ValidatorBackings = + StorageMap<_, Blake2_128Concat, T::AccountId, ValidatorBacking, OptionQuery>; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::hooks] + impl Hooks> for Pallet {} + + #[pallet::call] + impl Pallet { + /// Bond tokens to a validator. + /// Ensures the amount to bond is greater than the minimum bond amount. + /// + /// - `validator`: the AccountId of a validator to bond to + /// - `amount`: the number of tokens to bond to the given validator + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::bond())] + pub fn bond( + origin: OriginFor, + validator: T::AccountId, + #[pallet::compact] amount: Balance, + ) -> DispatchResult { + let guarantor = ensure_signed(origin)?; + let free_balance = T::LiquidSEECurrency::free_balance(&guarantor); + let total_should_locked = Self::total_locked_by_guarantor(&guarantor).unwrap_or_default(); + + if let Some(extra) = free_balance.checked_sub(total_should_locked) { + let amount = amount.min(extra); + + if !amount.is_zero() { + Self::update_guarantee(&guarantor, &validator, |guarantee| -> DispatchResult { + guarantee.total = guarantee.total.saturating_add(amount); + guarantee.bonded = guarantee.bonded.saturating_add(amount); + ensure!( + guarantee.bonded >= T::MinBondAmount::get(), + Error::::BelowMinBondAmount + ); + Ok(()) + })?; + Self::deposit_event(Event::BondGuarantee { + who: guarantor, + validator: validator.clone(), + bond: amount, + }); + } + } + Ok(()) + } + + /// Unbond tokens from a validator. + /// Ensures the bonded amount is zero or greater than the minimum bond amount. + /// + /// - `validator`: the AccountId of a validator to unbond from + /// - `amount`: the number of tokens to unbond from the given validator + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::unbond())] + pub fn unbond( + origin: OriginFor, + validator: T::AccountId, + #[pallet::compact] amount: Balance, + ) -> DispatchResult { + let guarantor = ensure_signed(origin)?; + + if !amount.is_zero() { + Self::update_guarantee(&guarantor, &validator, |guarantee| -> DispatchResult { + ensure!(guarantee.unbonding.is_none(), Error::::UnbondingExists); + let amount = amount.min(guarantee.bonded); + guarantee.bonded = guarantee.bonded.saturating_sub(amount); + ensure!( + guarantee.bonded.is_zero() || guarantee.bonded >= T::MinBondAmount::get(), + Error::::BelowMinBondAmount, + ); + let expired_block = T::BlockNumberProvider::current_block_number() + T::BondingDuration::get(); + guarantee.unbonding = Some((amount, expired_block)); + + Self::deposit_event(Event::UnbondGuarantee { + who: guarantor.clone(), + validator: validator.clone(), + bond: amount, + }); + Ok(()) + })?; + } + Ok(()) + } + + /// Rebond tokens to a validator. + /// + /// - `validator`: The AccountId of a validator to rebond to + /// - `amount`: The amount of tokens to to rebond to the given validator + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::rebond())] + pub fn rebond( + origin: OriginFor, + validator: T::AccountId, + #[pallet::compact] amount: Balance, + ) -> DispatchResult { + let guarantor = ensure_signed(origin)?; + + if !amount.is_zero() { + Self::update_guarantee(&guarantor, &validator, |guarantee| -> DispatchResult { + *guarantee = guarantee.rebond(amount); + Ok(()) + })?; + } + Ok(()) + } + + /// Withdraw the unbonded tokens from a validator. + /// Ensures the validator is not frozen. + /// + /// - `validator`: The AccountId of a validator to withdraw from + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::withdraw_unbonded())] + pub fn withdraw_unbonded(origin: OriginFor, validator: T::AccountId) -> DispatchResult { + let guarantor = ensure_signed(origin)?; + ensure!( + !Self::validator_backings(&validator).unwrap_or_default().is_frozen, + Error::::FrozenValidator + ); + Self::update_guarantee(&guarantor, &validator, |guarantee| -> DispatchResult { + let old_total = guarantee.total; + *guarantee = guarantee.consolidate_unbonding(T::BlockNumberProvider::current_block_number()); + let new_total = guarantee + .bonded + .saturating_add(guarantee.unbonding.unwrap_or_default().0); + if old_total != new_total { + guarantee.total = new_total; + Self::deposit_event(Event::WithdrawnGuarantee { + who: guarantor.clone(), + validator: validator.clone(), + bond: old_total.saturating_sub(new_total), + }); + } + Ok(()) + })?; + Ok(()) + } + + /// Freezes validators if they are not already frozen. + /// Ensures the caller can freeze validators. + /// + /// - `validators`: The AccountIds of the validators to freeze + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::freeze(validators.len() as u32))] + pub fn freeze(origin: OriginFor, validators: Vec) -> DispatchResult { + T::FreezeOrigin::ensure_origin(origin)?; + validators.iter().for_each(|validator| { + ValidatorBackings::::mutate_exists(validator, |maybe_validator| { + let mut v = maybe_validator.take().unwrap_or_default(); + if !v.is_frozen { + v.is_frozen = true; + Self::deposit_event(Event::FreezeValidator { + validator: validator.clone(), + }); + } + *maybe_validator = Some(v); + }); + }); + Ok(()) + } + + /// Unfreezes validators if they are frozen. + /// Ensures the caller can perform a slash. + /// + /// - `validators`: The AccountIds of the validators to unfreeze + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::thaw())] + pub fn thaw(origin: OriginFor, validators: Vec) -> DispatchResult { + // Using SlashOrigin instead of FreezeOrigin so that un-freezing requires more council members than + // freezing + T::SlashOrigin::ensure_origin(origin)?; + validators.iter().for_each(|validator| { + ValidatorBackings::::mutate_exists(validator, |maybe_validator| { + let mut v = maybe_validator.take().unwrap_or_default(); + if v.is_frozen { + v.is_frozen = false; + Self::deposit_event(Event::ThawValidator { + validator: validator.clone(), + }); + } + *maybe_validator = Some(v); + }); + }); + Ok(()) + } + + /// Slash validators. + /// Ensures the the caller can perform a slash. + /// + /// - `slashes`: The SlashInfos of the validators to be slashed + #[pallet::call_index(6)] + #[pallet::weight(T::WeightInfo::slash())] + pub fn slash(origin: OriginFor, slashes: Vec>) -> DispatchResult { + T::SlashOrigin::ensure_origin(origin)?; + let liquid_staking_exchange_rate = T::LiquidStakingExchangeRateProvider::get_exchange_rate(); + let staking_liquid_exchange_rate = liquid_staking_exchange_rate.reciprocal().unwrap_or_default(); + let mut actual_total_slashing: Balance = Zero::zero(); + + for SlashInfo { + validator, + token_amount, + } in slashes + { + let ValidatorBacking { total_insurance, .. } = Self::validator_backings(&validator).unwrap_or_default(); + let insurance_loss = staking_liquid_exchange_rate + .saturating_mul_int(token_amount) + .min(total_insurance); + + for (guarantor, _) in Guarantees::::iter_prefix(&validator) { + // NOTE: ignoring result because the closure will not throw err. + let res = Self::update_guarantee(&guarantor, &validator, |guarantee| -> DispatchResult { + let should_slashing = Ratio::checked_from_rational(guarantee.total, total_insurance) + .unwrap_or_else(Ratio::max_value) + .saturating_mul_int(insurance_loss); + let gap = T::LiquidSEECurrency::slash(&guarantor, should_slashing); + let actual_slashing = should_slashing.saturating_sub(gap); + *guarantee = guarantee.slash(actual_slashing); + Self::deposit_event(Event::SlashGuarantee { + who: guarantor.clone(), + validator: validator.clone(), + bond: actual_slashing, + }); + actual_total_slashing = actual_total_slashing.saturating_add(actual_slashing); + Ok(()) + }); + debug_assert!(res.is_ok()); + } + } + + T::OnSlash::happened(&actual_total_slashing); + Ok(()) + } + } +} + +impl Pallet { + fn update_guarantee( + guarantor: &T::AccountId, + validator: &T::AccountId, + f: impl FnOnce(&mut Guarantee>) -> DispatchResult, + ) -> DispatchResult { + Guarantees::::try_mutate_exists(validator, guarantor, |maybe_guarantee| -> DispatchResult { + let mut guarantee = maybe_guarantee.take().unwrap_or_default(); + let old_total = guarantee.total; + + f(&mut guarantee).and_then(|_| -> DispatchResult { + let new_total = guarantee.total; + if guarantee.total.is_zero() { + *maybe_guarantee = None; + } else { + *maybe_guarantee = Some(guarantee); + } + + // adjust total locked of nominator, validator backing and update the lock. + if new_total != old_total { + TotalLockedByGuarantor::::try_mutate_exists( + guarantor, + |maybe_total_locked| -> DispatchResult { + let mut tl = maybe_total_locked.take().unwrap_or_default(); + + ValidatorBackings::::try_mutate_exists( + validator, + |maybe_validator_backing| -> DispatchResult { + let mut vb = maybe_validator_backing.take().unwrap_or_default(); + + if new_total > old_total { + let gap = new_total - old_total; + vb.total_insurance = vb.total_insurance.saturating_add(gap); + tl = tl.saturating_add(gap); + T::OnIncreaseGuarantee::happened(&(guarantor.clone(), validator.clone(), gap)); + } else { + let gap = old_total - new_total; + vb.total_insurance = vb.total_insurance.saturating_sub(gap); + tl = tl.saturating_sub(gap); + T::OnDecreaseGuarantee::happened(&(guarantor.clone(), validator.clone(), gap)); + }; + + if tl.is_zero() { + *maybe_total_locked = None; + T::LiquidSEECurrency::remove_lock(EDFIS_LIQUID_STAKING_VALIDATOR_LIST_ID, guarantor)?; + } else { + *maybe_total_locked = Some(tl); + T::LiquidSEECurrency::set_lock(EDFIS_LIQUID_STAKING_VALIDATOR_LIST_ID, guarantor, tl)?; + } + + *maybe_validator_backing = Some(vb); + Ok(()) + }, + ) + }, + )?; + } + + Ok(()) + }) + }) + } +} + +impl Contains for Pallet { + fn contains(account_id: &T::AccountId) -> bool { + Self::validator_backings(account_id) + .unwrap_or_default() + .total_insurance + >= T::ValidatorInsuranceThreshold::get() + } +} diff --git a/blockchain/modules/edfis-liquid-see-validators/src/mock.rs b/blockchain/modules/edfis-liquid-see-validators/src/mock.rs new file mode 100644 index 00000000..c142ed6e --- /dev/null +++ b/blockchain/modules/edfis-liquid-see-validators/src/mock.rs @@ -0,0 +1,236 @@ +// بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيم + +// This file is part of Setheum. + +// Copyright (C) 2019-Present Setheum Labs. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// 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 . + +//! Mocks for Edfis Liquid SEE Validator List Module. + +#![cfg(test)] + +use super::*; +use frame_support::{ + construct_runtime, derive_impl, ord_parameter_types, parameter_types, + traits::{ConstU128, ConstU32, ConstU64, Nothing}, +}; +use frame_system::EnsureSignedBy; +use module_support::ExchangeRate; +use orml_traits::parameter_type_with_key; +use primitives::{Amount, Balance, CurrencyId, TokenSymbol}; +use sp_runtime::{traits::IdentityLookup, BuildStorage}; +use sp_std::cell::RefCell; +use std::collections::HashMap; + +pub type AccountId = u128; +pub type BlockNumber = u64; + +pub const ALICE: AccountId = 0; +pub const BOB: AccountId = 1; +pub const VALIDATOR_1: AccountId = 2; +pub const VALIDATOR_2: AccountId = 3; +pub const VALIDATOR_3: AccountId = 4; +pub const SEE: CurrencyId = CurrencyId::Token(TokenSymbol::SEE); +pub const LSEE: CurrencyId = CurrencyId::Token(TokenSymbol::LSEE); + +mod edfis_liquid_see_validator_list { + pub use super::super::*; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl frame_system::Config for Runtime { + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Block = Block; + type AccountData = pallet_balances::AccountData; +} + +parameter_type_with_key! { + pub ExistentialDeposits: |_currency_id: CurrencyId| -> Balance { + Default::default() + }; +} + +impl orml_tokens::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type Amount = Amount; + type CurrencyId = CurrencyId; + type WeightInfo = (); + type ExistentialDeposits = ExistentialDeposits; + type CurrencyHooks = (); + type MaxLocks = ConstU32<100>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type DustRemovalWhitelist = Nothing; +} + +impl pallet_balances::Config for Runtime { + type Balance = Balance; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ConstU128<1>; + type AccountStore = System; + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeFreezeReason = RuntimeFreezeReason; + type FreezeIdentifier = (); + type MaxHolds = (); + type MaxFreezes = (); +} + +parameter_types! { + pub const GetNativeCurrencyId: CurrencyId = SEE; + pub const GetLiquidCurrencyId: CurrencyId = LSEE; +} + +pub type NativeCurrency = orml_currencies::BasicCurrencyAdapter; +pub type LSEECurrency = orml_currencies::Currency; + +impl orml_currencies::Config for Runtime { + type MultiCurrency = OrmlTokens; + type NativeCurrency = NativeCurrency; + type GetNativeCurrencyId = GetNativeCurrencyId; + type WeightInfo = (); +} + +thread_local! { + pub static SHARES: RefCell> = RefCell::new(HashMap::new()); + pub static ACCUMULATED_SLASH: RefCell = RefCell::new(0); +} + +pub struct MockOnSlash; +impl Happened for MockOnSlash { + fn happened(amount: &Balance) { + ACCUMULATED_SLASH.with(|v| *v.borrow_mut() += amount); + } +} + +pub struct MockOnIncreaseGuarantee; +impl Happened<(AccountId, AccountId, Balance)> for MockOnIncreaseGuarantee { + fn happened(info: &(AccountId, AccountId, Balance)) { + let (account_id, validator_account_id, amount) = info; + SHARES.with(|v| { + let mut old_map = v.borrow().clone(); + if let Some(share) = old_map.get_mut(&(*account_id, *validator_account_id)) { + *share = share.saturating_add(*amount); + } else { + old_map.insert((*account_id, *validator_account_id), *amount); + }; + + *v.borrow_mut() = old_map; + }); + } +} + +pub struct MockOnDecreaseGuarantee; +impl Happened<(AccountId, AccountId, Balance)> for MockOnDecreaseGuarantee { + fn happened(info: &(AccountId, AccountId, Balance)) { + let (account_id, validator_account_id, amount) = info; + SHARES.with(|v| { + let mut old_map = v.borrow().clone(); + if let Some(share) = old_map.get_mut(&(*account_id, *validator_account_id)) { + *share = share.saturating_sub(*amount); + } else { + old_map.insert((*account_id, *validator_account_id), Default::default()); + }; + + *v.borrow_mut() = old_map; + }); + } +} + +pub struct MockLiquidStakingExchangeProvider; +impl ExchangeRateProvider for MockLiquidStakingExchangeProvider { + fn get_exchange_rate() -> ExchangeRate { + ExchangeRate::saturating_from_rational(1, 2) + } +} + +parameter_types! { + pub static MockBlockNumberProvider: u64 = 0; +} + +impl BlockNumberProvider for MockBlockNumberProvider { + type BlockNumber = u64; + + fn current_block_number() -> Self::BlockNumber { + Self::get() + } +} + +ord_parameter_types! { + pub const Admin: AccountId = 10; +} + +impl Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type LiquidSEECurrency = LSEECurrency; + type MinBondAmount = ConstU128<100>; + type BondingDuration = ConstU64<100>; + type ValidatorInsuranceThreshold = ConstU128<200>; + type FreezeOrigin = EnsureSignedBy; + type SlashOrigin = EnsureSignedBy; + type OnSlash = MockOnSlash; + type LiquidStakingExchangeRateProvider = MockLiquidStakingExchangeProvider; + type WeightInfo = (); + type OnIncreaseGuarantee = MockOnIncreaseGuarantee; + type OnDecreaseGuarantee = MockOnDecreaseGuarantee; + type BlockNumberProvider = MockBlockNumberProvider; +} + +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub enum Runtime { + System: frame_system, + OrmlTokens: orml_tokens, + PalletBalances: pallet_balances, + OrmlCurrencies: orml_currencies, + EdfisLiquidSeeValidatorsModule: edfis_liquid_see_validator_list, + } +); + +pub struct ExtBuilder { + balances: Vec<(AccountId, CurrencyId, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { + balances: vec![(ALICE, LSEE, 1000), (BOB, LSEE, 1000)], + } + } +} + +impl ExtBuilder { + pub fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .unwrap(); + + orml_tokens::GenesisConfig:: { + balances: self.balances, + } + .assimilate_storage(&mut t) + .unwrap(); + + t.into() + } +} diff --git a/blockchain/modules/edfis-liquid-see-validators/src/tests.rs b/blockchain/modules/edfis-liquid-see-validators/src/tests.rs new file mode 100644 index 00000000..67dbc557 --- /dev/null +++ b/blockchain/modules/edfis-liquid-see-validators/src/tests.rs @@ -0,0 +1,881 @@ +// بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيم + +// This file is part of Setheum. + +// Copyright (C) 2019-Present Setheum Labs. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// 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 . + +//! Unit tests for homa validator list module. + +#![cfg(test)] + +use super::*; +use frame_support::{assert_noop, assert_ok}; +use mock::*; +use sp_runtime::traits::BadOrigin; + +#[test] +fn guarantee_work() { + ExtBuilder::default().build().execute_with(|| { + let guarantee = Guarantee { + total: 1000, + bonded: 800, + unbonding: Some((200, 10)), + }; + + assert_eq!(guarantee.consolidate_unbonding(9).unbonding, Some((200, 10))); + assert_eq!(guarantee.consolidate_unbonding(10).unbonding, None); + + assert_eq!( + guarantee.rebond(50), + Guarantee { + total: 1000, + bonded: 850, + unbonding: Some((150, 10)), + } + ); + assert_eq!( + guarantee.rebond(200), + Guarantee { + total: 1000, + bonded: 1000, + unbonding: None, + } + ); + + assert_eq!( + guarantee.slash(200), + Guarantee { + total: 800, + bonded: 600, + unbonding: Some((200, 10)), + } + ); + assert_eq!( + guarantee.slash(850), + Guarantee { + total: 150, + bonded: 0, + unbonding: Some((150, 10)), + } + ); + assert_eq!( + guarantee.slash(1000), + Guarantee { + total: 0, + bonded: 0, + unbonding: None, + } + ); + }); +} + +#[test] +fn freeze_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + assert_noop!( + EdfisLiquidSeeValidatorsModule::freeze( + RuntimeOrigin::signed(ALICE), + vec![VALIDATOR_1, VALIDATOR_2, VALIDATOR_3] + ), + BadOrigin + ); + + assert!( + !EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .is_frozen, + ); + assert!( + !EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_2) + .unwrap_or_default() + .is_frozen, + ); + assert!( + !EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_3) + .unwrap_or_default() + .is_frozen, + ); + assert_ok!(EdfisLiquidSeeValidatorsModule::freeze( + RuntimeOrigin::signed(10), + vec![VALIDATOR_1, VALIDATOR_2, VALIDATOR_3] + )); + assert!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .is_frozen + ); + assert!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_2) + .unwrap_or_default() + .is_frozen + ); + assert!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_3) + .unwrap_or_default() + .is_frozen + ); + + System::assert_has_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::FreezeValidator { validator: VALIDATOR_1 }, + )); + System::assert_has_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::FreezeValidator { validator: VALIDATOR_2 }, + )); + System::assert_has_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::FreezeValidator { validator: VALIDATOR_3 }, + )); + }); +} + +#[test] +fn thaw_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + assert_noop!( + EdfisLiquidSeeValidatorsModule::thaw( + RuntimeOrigin::signed(ALICE), + vec![VALIDATOR_1, VALIDATOR_2, VALIDATOR_3] + ), + BadOrigin + ); + + assert_ok!(EdfisLiquidSeeValidatorsModule::freeze( + RuntimeOrigin::signed(10), + vec![VALIDATOR_1, VALIDATOR_2] + )); + assert!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .is_frozen + ); + assert!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_2) + .unwrap_or_default() + .is_frozen + ); + assert!( + !EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_3) + .unwrap_or_default() + .is_frozen + ); + assert_ok!(EdfisLiquidSeeValidatorsModule::thaw( + RuntimeOrigin::signed(10), + vec![VALIDATOR_1, VALIDATOR_2, VALIDATOR_3] + )); + assert!( + !EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .is_frozen + ); + assert!( + !EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_2) + .unwrap_or_default() + .is_frozen + ); + assert!( + !EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_3) + .unwrap_or_default() + .is_frozen + ); + System::assert_has_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::ThawValidator { validator: VALIDATOR_1 }, + )); + System::assert_has_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::ThawValidator { validator: VALIDATOR_2 }, + )); + }); +} + +#[test] +fn bond_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + assert_noop!( + EdfisLiquidSeeValidatorsModule::bond(RuntimeOrigin::signed(ALICE), VALIDATOR_1, 99), + Error::::BelowMinBondAmount + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 0, + bonded: 0, + unbonding: None + } + ); + assert_eq!(OrmlTokens::accounts(ALICE, LSEE).frozen, 0); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 0 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 0 + ); + assert_eq!(SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), 0); + + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 100 + )); + System::assert_last_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::BondGuarantee { + who: ALICE, + validator: VALIDATOR_1, + bond: 100, + }, + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 100, + bonded: 100, + unbonding: None + } + ); + assert_eq!(OrmlTokens::accounts(ALICE, LSEE).frozen, 100); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 100 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 100 + ); + assert_eq!( + SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), + 100 + ); + + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, BOB).unwrap_or_default(), + Guarantee { + total: 0, + bonded: 0, + unbonding: None + } + ); + assert_eq!(OrmlTokens::accounts(BOB, LSEE).frozen, 0); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(BOB).unwrap_or_default(), + 0 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 100 + ); + assert_eq!(SHARES.with(|v| *v.borrow().get(&(BOB, VALIDATOR_1)).unwrap_or(&0)), 0); + + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(BOB), + VALIDATOR_1, + 300 + )); + System::assert_last_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::BondGuarantee { + who: BOB, + validator: VALIDATOR_1, + bond: 300, + }, + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, BOB).unwrap_or_default(), + Guarantee { + total: 300, + bonded: 300, + unbonding: None + } + ); + assert_eq!(OrmlTokens::accounts(BOB, LSEE).frozen, 300); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(BOB).unwrap_or_default(), + 300 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 400 + ); + assert_eq!(SHARES.with(|v| *v.borrow().get(&(BOB, VALIDATOR_1)).unwrap_or(&0)), 300); + + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_2, BOB).unwrap_or_default(), + Guarantee { + total: 0, + bonded: 0, + unbonding: None + } + ); + assert_eq!(OrmlTokens::accounts(BOB, LSEE).frozen, 300); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(BOB).unwrap_or_default(), + 300 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_2) + .unwrap_or_default() + .total_insurance, + 0 + ); + assert_eq!(SHARES.with(|v| *v.borrow().get(&(BOB, VALIDATOR_2)).unwrap_or(&0)), 0); + + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(BOB), + VALIDATOR_2, + 200 + )); + System::assert_last_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::BondGuarantee { + who: BOB, + validator: VALIDATOR_2, + bond: 200, + }, + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_2, BOB).unwrap_or_default(), + Guarantee { + total: 200, + bonded: 200, + unbonding: None + } + ); + assert_eq!(OrmlTokens::accounts(BOB, LSEE).frozen, 500); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(BOB).unwrap_or_default(), + 500 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_2) + .unwrap_or_default() + .total_insurance, + 200 + ); + assert_eq!(SHARES.with(|v| *v.borrow().get(&(BOB, VALIDATOR_2)).unwrap_or(&0)), 200); + }); +} + +#[test] +fn unbond_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + MockBlockNumberProvider::set(1); + + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 200 + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 200, + bonded: 200, + unbonding: None + } + ); + assert_eq!(OrmlTokens::accounts(ALICE, LSEE).frozen, 200); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 200 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 200 + ); + assert_eq!( + SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), + 200 + ); + + assert_noop!( + EdfisLiquidSeeValidatorsModule::unbond(RuntimeOrigin::signed(ALICE), VALIDATOR_1, 199), + Error::::BelowMinBondAmount + ); + + assert_ok!(EdfisLiquidSeeValidatorsModule::unbond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 100 + )); + System::assert_last_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::UnbondGuarantee { + who: ALICE, + validator: VALIDATOR_1, + bond: 100, + }, + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 200, + bonded: 100, + unbonding: Some((100, 101)) + } + ); + assert_eq!(OrmlTokens::accounts(ALICE, LSEE).frozen, 200); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 200 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 200 + ); + assert_eq!( + SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), + 200 + ); + + assert_noop!( + EdfisLiquidSeeValidatorsModule::unbond(RuntimeOrigin::signed(ALICE), VALIDATOR_1, 100), + Error::::UnbondingExists + ); + }); +} + +#[test] +fn rebond_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + MockBlockNumberProvider::set(1); + + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 200 + )); + assert_ok!(EdfisLiquidSeeValidatorsModule::unbond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 100 + )); + + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 200, + bonded: 100, + unbonding: Some((100, 101)) + } + ); + assert_eq!(OrmlTokens::accounts(ALICE, LSEE).frozen, 200); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 200 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 200 + ); + assert_eq!( + SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), + 200 + ); + + assert_ok!(EdfisLiquidSeeValidatorsModule::rebond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 50 + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 200, + bonded: 150, + unbonding: Some((50, 101)) + } + ); + assert_eq!(OrmlTokens::accounts(ALICE, LSEE).frozen, 200); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 200 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 200 + ); + assert_eq!( + SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), + 200 + ); + }); +} + +#[test] +fn withdraw_unbonded_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + MockBlockNumberProvider::set(1); + + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 200 + )); + assert_ok!(EdfisLiquidSeeValidatorsModule::unbond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 100 + )); + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(BOB), + VALIDATOR_1, + 200 + )); + assert_ok!(EdfisLiquidSeeValidatorsModule::unbond( + RuntimeOrigin::signed(BOB), + VALIDATOR_1, + 100 + )); + + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 200, + bonded: 100, + unbonding: Some((100, 101)) + } + ); + assert_eq!(OrmlTokens::accounts(ALICE, LSEE).frozen, 200); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 200 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 400 + ); + assert_eq!( + SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), + 200 + ); + + MockBlockNumberProvider::set(100); + assert_ok!(EdfisLiquidSeeValidatorsModule::withdraw_unbonded( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1 + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 200, + bonded: 100, + unbonding: Some((100, 101)) + } + ); + assert_eq!(OrmlTokens::accounts(ALICE, LSEE).frozen, 200); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 200 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 400 + ); + assert_eq!( + SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), + 200 + ); + System::reset_events(); + MockBlockNumberProvider::set(101); + assert_ok!(EdfisLiquidSeeValidatorsModule::withdraw_unbonded( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1 + )); + System::assert_has_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::WithdrawnGuarantee { + who: ALICE, + validator: VALIDATOR_1, + bond: 100, + }, + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 100, + bonded: 100, + unbonding: None + } + ); + assert_eq!(OrmlTokens::accounts(ALICE, LSEE).frozen, 100); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 100 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 300 + ); + assert_eq!( + SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), + 100 + ); + + assert_ok!(EdfisLiquidSeeValidatorsModule::freeze( + RuntimeOrigin::signed(10), + vec![VALIDATOR_1] + )); + assert_noop!( + EdfisLiquidSeeValidatorsModule::withdraw_unbonded(RuntimeOrigin::signed(BOB), VALIDATOR_1), + Error::::FrozenValidator + ); + }); +} + +#[test] +fn slash_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 100 + )); + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(BOB), + VALIDATOR_1, + 200 + )); + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(BOB), + VALIDATOR_2, + 300 + )); + + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 300 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_2) + .unwrap_or_default() + .total_insurance, + 300 + ); + + // ALICE + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 100, + bonded: 100, + unbonding: None + } + ); + assert_eq!( + SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), + 100 + ); + assert_eq!(OrmlTokens::accounts(ALICE, LSEE).frozen, 100); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 100 + ); + + // BOB + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, BOB).unwrap_or_default(), + Guarantee { + total: 200, + bonded: 200, + unbonding: None + } + ); + assert_eq!(SHARES.with(|v| *v.borrow().get(&(BOB, VALIDATOR_1)).unwrap_or(&0)), 200); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_2, BOB).unwrap_or_default(), + Guarantee { + total: 300, + bonded: 300, + unbonding: None + } + ); + assert_eq!(SHARES.with(|v| *v.borrow().get(&(BOB, VALIDATOR_2)).unwrap_or(&0)), 300); + assert_eq!(OrmlTokens::accounts(BOB, LSEE).frozen, 500); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(BOB).unwrap_or_default(), + 500 + ); + + assert_noop!( + EdfisLiquidSeeValidatorsModule::slash( + RuntimeOrigin::signed(ALICE), + vec![ + SlashInfo { + validator: VALIDATOR_1, + relaychain_token_amount: 90 + }, + SlashInfo { + validator: VALIDATOR_2, + relaychain_token_amount: 50 + }, + ] + ), + BadOrigin + ); + + assert_ok!(EdfisLiquidSeeValidatorsModule::slash( + RuntimeOrigin::signed(10), + vec![ + SlashInfo { + validator: VALIDATOR_1, + relaychain_token_amount: 90 + }, + SlashInfo { + validator: VALIDATOR_2, + relaychain_token_amount: 50 + }, + ] + )); + System::assert_has_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::SlashGuarantee { + who: ALICE, + validator: VALIDATOR_1, + bond: 59, + }, + )); + System::assert_has_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::SlashGuarantee { + who: BOB, + validator: VALIDATOR_1, + bond: 119, + }, + )); + System::assert_has_event(mock::RuntimeEvent::EdfisLiquidSeeValidatorsModule( + crate::Event::SlashGuarantee { + who: BOB, + validator: VALIDATOR_2, + bond: 100, + }, + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 122 + ); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_2) + .unwrap_or_default() + .total_insurance, + 200 + ); + + // ALICE + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, ALICE).unwrap_or_default(), + Guarantee { + total: 41, + bonded: 41, + unbonding: None + } + ); + assert_eq!( + SHARES.with(|v| *v.borrow().get(&(ALICE, VALIDATOR_1)).unwrap_or(&0)), + 41 + ); + assert_eq!(OrmlTokens::accounts(ALICE, LSEE).frozen, 41); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(ALICE).unwrap_or_default(), + 41 + ); + + // BOB + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_1, BOB).unwrap_or_default(), + Guarantee { + total: 81, + bonded: 81, + unbonding: None + } + ); + assert_eq!(SHARES.with(|v| *v.borrow().get(&(BOB, VALIDATOR_1)).unwrap_or(&0)), 81); + assert_eq!( + EdfisLiquidSeeValidatorsModule::guarantees(VALIDATOR_2, BOB).unwrap_or_default(), + Guarantee { + total: 200, + bonded: 200, + unbonding: None + } + ); + assert_eq!(SHARES.with(|v| *v.borrow().get(&(BOB, VALIDATOR_2)).unwrap_or(&0)), 200); + assert_eq!(OrmlTokens::accounts(BOB, LSEE).frozen, 281); + assert_eq!( + EdfisLiquidSeeValidatorsModule::total_locked_by_guarantor(BOB).unwrap_or_default(), + 281 + ); + }); +} + +#[test] +fn contains_work() { + ExtBuilder::default().build().execute_with(|| { + MockBlockNumberProvider::set(1); + + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 100 + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 100 + ); + assert!(!EdfisLiquidSeeValidatorsModule::contains(&VALIDATOR_1)); + + assert_ok!(EdfisLiquidSeeValidatorsModule::bond( + RuntimeOrigin::signed(ALICE), + VALIDATOR_1, + 100 + )); + assert_eq!( + EdfisLiquidSeeValidatorsModule::validator_backings(VALIDATOR_1) + .unwrap_or_default() + .total_insurance, + 200 + ); + assert!(EdfisLiquidSeeValidatorsModule::contains(&VALIDATOR_1)); + }); +} diff --git a/blockchain/modules/edfis-x/README.md b/blockchain/modules/edfis-x/README.md deleted file mode 100644 index 75b02613..00000000 --- a/blockchain/modules/edfis-x/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Edfis X Module -Provides cross-chain multichain Swaps on Edfis Exchange. - -## Overview - -This module is used to build a cross-chain multichain swap protocol on Edfis Exchange. Built on `Slick Bridge`. diff --git a/blockchain/modules/edfis-x/TODO.md b/blockchain/modules/edfis-x/TODO.md deleted file mode 100644 index c8c94c13..00000000 --- a/blockchain/modules/edfis-x/TODO.md +++ /dev/null @@ -1,56 +0,0 @@ -# To-Do List - -This list contains all TODOs in the Repo - - - -- [ToDo List - The Monofile for Setheum Repo ToDos](#to-do-list) - - [1. Introduction](#1-guidelines) - - [2. Contribution](#2-contribution) - - [3. Lists](#3-lists) - - [4. Tasks](#4-tasks) - - - -## 1. Guidelines - -Note: Before you write a ToDo in this repo, please read the below guidelines carefully. - -Whenever you write a ToDo, you need to follow this standard syntax - -```rust -//TODO:[file_name:task_number] - task_details -``` - -for example: - -```rust -//TODO:[TODO.md:0] - Add Todo Guidelines -``` - -Note > the `//TODO:[filename:task_number] - ` is what we call the `task_prefix`. - -Whenever adding/writing a Task/ToDo, you need to describe the task on this list. Whenever you write a TODO in any file, add a reference to it here. Please make sure the task reference here is titled correctly and as detailed as possible\. - -Whenever you `complete` a task/TODO from any file, please tick/complete its reference here and make sure you do it in the same `commit` that completes the task. - -Whenever a task is cancelled (discontinued or not needed for w/e reason), please note in the details why it is cancelled, make sure you do it in the same `commit` that removes/cancels the TODO, and add this `-C` as a suffix to its `file_name` in the list here, for example: - -```rust -//TODO:[TODO.md-C:0] - Add Todo Guidelines -``` - -## 2. Contribution - -You can contribute to this list by completing tasks or by adding tasks(TODOs) that are currently in the repo but not on the list. You can also contribute by updating old tasks to the new Standard. - -## 3. Lists - -Each package/module/directory has its own `TODO.md`. - -## 4. Tasks - -These tasks are just for this file specifically. - -- [x] [[TODO.md:0] - Add TODO.md File](TODO.md): Add a TODO.md file to organise TODOs in the repo. -- [x] [[TODO.md:1] - Add a `task_title`](/TODO.md/#tasks): Adda `task_title`. diff --git a/blockchain/modules/highway/Cargo.toml b/blockchain/modules/highway/Cargo.toml deleted file mode 100644 index b7433b12..00000000 --- a/blockchain/modules/highway/Cargo.toml +++ /dev/null @@ -1,48 +0,0 @@ -[package] -name = "module-highway" -version = "0.1.0" -authors.workspace = true -edition.workspace = true -homepage.workspace = true -repository.workspace = true - -[dependencies] -parity-scale-codec = { version = "3.0.0", default-features = false, features = ["max-encoded-len"] } -sp-runtime = { workspace = true } -sp-io = { workspace = true } -sp-std = { workspace = true } -frame-support = { workspace = true } -frame-system = { workspace = true } - -primitives = { package = "setheum-primitives", path = "../primitives", default-features = false } -support = { package = "module-support", path = "../support", default-features = false } -orml-traits = { path = "../submodules/orml/traits", default-features = false } - -[dev-dependencies] -sp-core = { workspace = true, features = ["std"] } -pallet-balances = { workspace = true } -orml-tokens = { workspace = true } - -[features] -default = ["std"] -std = [ - "parity-scale-codec/std", - "sp-runtime/std", - "sp-std/std", - "sp-io/std", - "frame-support/std", - "frame-system/std", - "primitives/std", - "support/std", - "orml-traits/std", -] -runtime-benchmarks = [ - "frame-support/runtime-benchmarks", - "frame-system/runtime-benchmarks", - "sp-runtime/runtime-benchmarks", -] -try-runtime = [ - "frame-support/try-runtime", - "frame-system/try-runtime", - "sp-runtime/try-runtime", -] diff --git a/blockchain/modules/highway/README.md b/blockchain/modules/highway/README.md deleted file mode 100644 index 6992905c..00000000 --- a/blockchain/modules/highway/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Highway Module - -## Overview - -Provides a DePIN Infrastructure for DePIN Projects to build and deploy on. It provides a Unified Platform for DePIN Providers. diff --git a/blockchain/modules/lockdrop/Cargo.toml b/blockchain/modules/lockdrop/Cargo.toml deleted file mode 100644 index 6d7e4cf3..00000000 --- a/blockchain/modules/lockdrop/Cargo.toml +++ /dev/null @@ -1,43 +0,0 @@ -[package] -name = "module-lockdrop" -version = "0.9.81-dev" -authors.workspace = true -edition.workspace = true -homepage.workspace = true -repository.workspace = true - -[dependencies] -parity-scale-codec = { workspace = true, features = ["max-encoded-len"] } -scale-info = { workspace = true } -sp-core = { workspace = true } -sp-runtime = { workspace = true } -sp-std = { workspace = true } -frame-support = { workspace = true } -frame-system = { workspace = true } - -orml-traits = { workspace = true } -primitives = { workspace = true } - -[dev-dependencies] -orml-tokens = { workspace = true, features = ["std"] } -sp-core = { workspace = true, features = ["std"] } - -[features] -default = ["std"] -std = [ - "frame-support/std", - "frame-system/std", - "orml-tokens/std", - "orml-traits/std", - "parity-scale-codec/std", - "primitives/std", - "sp-runtime/std", - "sp-core/std", - "sp-std/std", - "scale-info/std", -] -try-runtime = [ - "frame-support/try-runtime", - "frame-system/try-runtime", - "module-edfis_swap_legacy/try-runtime", -] diff --git a/blockchain/modules/lockdrop/README.md b/blockchain/modules/lockdrop/README.md deleted file mode 100644 index fafe7bd8..00000000 --- a/blockchain/modules/lockdrop/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Airdrop Module - -## Overview - -This module creates locked airdrops and distributes them to the - acccounts in the lockdrop list from a drop origin. The module for distributing Setheum Lockdrops. diff --git a/blockchain/modules/setheum-bridge/Cargo.toml b/blockchain/modules/setheum-bridge/Cargo.toml deleted file mode 100644 index 321812b6..00000000 --- a/blockchain/modules/setheum-bridge/Cargo.toml +++ /dev/null @@ -1,49 +0,0 @@ -[package] -name = "module-setheum-bridge" -description = "Provides a cross-chain bridge network on the Setheum Network." -version = "0.9.81-dev" -authors.workspace = true -edition.workspace = true -homepage.workspace = true -repository.workspace = true - -[dependencies] -parity-scale-codec = { version = "3.0.0", default-features = false, features = ["max-encoded-len"] } -sp-runtime = { workspace = true } -sp-io = { workspace = true } -sp-std = { workspace = true } -frame-support = { workspace = true } -frame-system = { workspace = true } - -primitives = { package = "setheum-primitives", path = "../primitives", default-features = false } -support = { package = "module-support", path = "../support", default-features = false } -orml-traits = { path = "../submodules/orml/traits", default-features = false } - -[dev-dependencies] -sp-core = { workspace = true, features = ["std"] } -pallet-balances = { workspace = true } -orml-tokens = { workspace = true } - -[features] -default = ["std"] -std = [ - "parity-scale-codec/std", - "sp-runtime/std", - "sp-std/std", - "sp-io/std", - "frame-support/std", - "frame-system/std", - "primitives/std", - "support/std", - "orml-traits/std", -] -runtime-benchmarks = [ - "frame-support/runtime-benchmarks", - "frame-system/runtime-benchmarks", - "sp-runtime/runtime-benchmarks", -] -try-runtime = [ - "frame-support/try-runtime", - "frame-system/try-runtime", - "sp-runtime/try-runtime", -] diff --git a/blockchain/modules/setheum-bridge/README.md b/blockchain/modules/setheum-bridge/README.md deleted file mode 100644 index 29b3c8f5..00000000 --- a/blockchain/modules/setheum-bridge/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Setheum Bridge Module -Provides a cross-chain bridge network on the Setheum Network. - -## Overview - -This module is used to build a cross-chain bridge network on the Setheum Network.. Built with `Sygma Protocol`. diff --git a/blockchain/modules/setheum-bridge/TODO.md b/blockchain/modules/setheum-bridge/TODO.md deleted file mode 100644 index c8c94c13..00000000 --- a/blockchain/modules/setheum-bridge/TODO.md +++ /dev/null @@ -1,56 +0,0 @@ -# To-Do List - -This list contains all TODOs in the Repo - - - -- [ToDo List - The Monofile for Setheum Repo ToDos](#to-do-list) - - [1. Introduction](#1-guidelines) - - [2. Contribution](#2-contribution) - - [3. Lists](#3-lists) - - [4. Tasks](#4-tasks) - - - -## 1. Guidelines - -Note: Before you write a ToDo in this repo, please read the below guidelines carefully. - -Whenever you write a ToDo, you need to follow this standard syntax - -```rust -//TODO:[file_name:task_number] - task_details -``` - -for example: - -```rust -//TODO:[TODO.md:0] - Add Todo Guidelines -``` - -Note > the `//TODO:[filename:task_number] - ` is what we call the `task_prefix`. - -Whenever adding/writing a Task/ToDo, you need to describe the task on this list. Whenever you write a TODO in any file, add a reference to it here. Please make sure the task reference here is titled correctly and as detailed as possible\. - -Whenever you `complete` a task/TODO from any file, please tick/complete its reference here and make sure you do it in the same `commit` that completes the task. - -Whenever a task is cancelled (discontinued or not needed for w/e reason), please note in the details why it is cancelled, make sure you do it in the same `commit` that removes/cancels the TODO, and add this `-C` as a suffix to its `file_name` in the list here, for example: - -```rust -//TODO:[TODO.md-C:0] - Add Todo Guidelines -``` - -## 2. Contribution - -You can contribute to this list by completing tasks or by adding tasks(TODOs) that are currently in the repo but not on the list. You can also contribute by updating old tasks to the new Standard. - -## 3. Lists - -Each package/module/directory has its own `TODO.md`. - -## 4. Tasks - -These tasks are just for this file specifically. - -- [x] [[TODO.md:0] - Add TODO.md File](TODO.md): Add a TODO.md file to organise TODOs in the repo. -- [x] [[TODO.md:1] - Add a `task_title`](/TODO.md/#tasks): Adda `task_title`. diff --git a/blockchain/modules/setheum-pay/Cargo.toml b/blockchain/modules/setheum-pay/Cargo.toml index 321812b6..1bb2dd02 100644 --- a/blockchain/modules/setheum-pay/Cargo.toml +++ b/blockchain/modules/setheum-pay/Cargo.toml @@ -1,49 +1,46 @@ -[package] -name = "module-setheum-bridge" -description = "Provides a cross-chain bridge network on the Setheum Network." -version = "0.9.81-dev" -authors.workspace = true -edition.workspace = true -homepage.workspace = true -repository.workspace = true - -[dependencies] -parity-scale-codec = { version = "3.0.0", default-features = false, features = ["max-encoded-len"] } -sp-runtime = { workspace = true } -sp-io = { workspace = true } -sp-std = { workspace = true } -frame-support = { workspace = true } -frame-system = { workspace = true } - -primitives = { package = "setheum-primitives", path = "../primitives", default-features = false } -support = { package = "module-support", path = "../support", default-features = false } -orml-traits = { path = "../submodules/orml/traits", default-features = false } - -[dev-dependencies] -sp-core = { workspace = true, features = ["std"] } -pallet-balances = { workspace = true } -orml-tokens = { workspace = true } - -[features] -default = ["std"] -std = [ - "parity-scale-codec/std", - "sp-runtime/std", - "sp-std/std", - "sp-io/std", - "frame-support/std", - "frame-system/std", - "primitives/std", - "support/std", - "orml-traits/std", -] -runtime-benchmarks = [ - "frame-support/runtime-benchmarks", - "frame-system/runtime-benchmarks", - "sp-runtime/runtime-benchmarks", -] -try-runtime = [ - "frame-support/try-runtime", - "frame-system/try-runtime", - "sp-runtime/try-runtime", -] +[package] +name = "module-setheum-pay" +version = "0.9.81-dev" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true + + +[dependencies] +parity-scale-codec = { workspace = true } +log = { workspace = true } +scale-info = { workspace = true } + +frame-support = { workspace = true } +frame-system = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +orml-traits = {path = "../traits", version = "0.7.0", default-features = false } + +[dev-dependencies] +serde = "1.0.136" + +sp-core = { workspace = true } +sp-io = { workspace = true } + +orml-tokens = { path = "../tokens" } + +[features] +default = [ 'std' ] +std = [ + 'frame-support/std', + 'frame-system/std', + 'log/std', + 'orml-traits/std', + 'parity-scale-codec/std', + 'scale-info/std', + 'sp-runtime/std', + 'sp-std/std', +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/blockchain/modules/setheum-pay/README.md b/blockchain/modules/setheum-pay/README.md index 29b3c8f5..4aef81cc 100644 --- a/blockchain/modules/setheum-pay/README.md +++ b/blockchain/modules/setheum-pay/README.md @@ -1,6 +1,85 @@ -# Setheum Bridge Module -Provides a cross-chain bridge network on the Setheum Network. - -## Overview - -This module is used to build a cross-chain bridge network on the Setheum Network.. Built with `Sygma Protocol`. +بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيم + +# Payments Module + +This module allows users to create secure reversible payments that keep funds locked in a merchant's account until the off-chain goods are confirmed to be received. Each payment gets assigned its own *judge* that can help resolve any disputes between the two parties. + +## Terminology + +- Created: A payment has been created and the amount arrived to its destination but it's locked. +- NeedsReview: The payment has bee disputed and is awaiting settlement by a judge. +- IncentivePercentage: A small share of the payment amount is held in escrow until a payment is completed/cancelled. The Incentive Percentage represents this value. +- Resolver Account: A resolver account is assigned to every payment created, this account has the privilege to cancel/release a payment that has been disputed. +- Remark: The module allows to create payments by optionally providing some extra(limited) amount of bytes, this is referred to as Remark. This can be used by a marketplace to separate/tag payments. +- CancelBufferBlockLength: This is the time window where the recipient can dispute a cancellation request from the payment creator. + +## Interface + +#### Events + +- `PaymentCreated { from: T::AccountId, asset: AssetIdOf, amount: BalanceOf },`, +- `PaymentReleased { from: T::AccountId, to: T::AccountId }`, +- `PaymentCancelled { from: T::AccountId, to: T::AccountId }`, +- `PaymentCreatorRequestedRefund { from: T::AccountId, to: T::AccountId, expiry: BlockNumberFor}` +- `PaymentRefundDisputed { from: T::AccountId, to: T::AccountId }` +- `PaymentRequestCreated { from: T::AccountId, to: T::AccountId }` +- `PaymentRequestCompleted { from: T::AccountId, to: T::AccountId }` + +#### Extrinsics + +- `pay` - Create an payment for the given currencyid/amount +- `pay_with_remark` - Create a payment with a remark, can be used to tag payments +- `release` - Release the payment amount to recipent +- `cancel` - Allows the recipient to cancel the payment and release the payment amount to creator +- `resolve_release_payment` - Allows assigned judge to release a payment +- `resolve_cancel_payment` - Allows assigned judge to cancel a payment +- `request_refund` - Allows the creator of the payment to trigger cancel with a buffer time. +- `claim_refund` - Allows the creator to claim payment refund after buffer time +- `dispute_refund` - Allows the recipient to dispute the payment request of sender +- `request_payment` - Create a payment that can be completed by the sender using the `accept_and_pay` extrinsic. +- `accept_and_pay` - Allows the sender to fulfill a payment request created by a recipient + +## Implementations + +The RatesProvider module provides implementations for the following traits. +- [`PaymentHandler`](./src/types.rs) + +## Types + +The `PaymentDetail` struct stores information about the payment/escrow. A "payment" in Setheum Pay is similar to an escrow, it is used to guarantee proof of funds and can be released once an agreed upon condition has reached between the payment creator and recipient. The payment lifecycle is tracked using the state field. + +```rust +pub struct PaymentDetail { + /// type of asset used for payment + pub asset: AssetIdOf, + /// amount of asset used for payment + pub amount: BalanceOf, + /// incentive amount that is credited to creator for resolving + pub incentive_amount: BalanceOf, + /// enum to track payment lifecycle [Created, NeedsReview] + pub state: PaymentState>, + /// account that can settle any disputes created in the payment + pub resolver_account: T::AccountId, + /// fee charged and recipient account details + pub fee_detail: Option<(T::AccountId, BalanceOf)>, + /// remarks to give context to payment + pub remark: Option>, +} +``` + +The `PaymentState` enum tracks the possible states that a payment can be in. When a payment is 'completed' or 'cancelled' it is removed from storage and hence not tracked by a state. + +```rust +pub enum PaymentState { + /// Amounts have been reserved and waiting for release/cancel + Created, + /// A judge needs to review and release manually + NeedsReview, + /// The user has requested refund and will be processed by `BlockNumber` + RefundRequested(BlockNumber), +} +``` + +## GenesisConfig + +The rates_provider pallet does not depend on the `GenesisConfig` diff --git a/blockchain/modules/setheum-pay/src/lib.rs b/blockchain/modules/setheum-pay/src/lib.rs new file mode 100644 index 00000000..28e8e545 --- /dev/null +++ b/blockchain/modules/setheum-pay/src/lib.rs @@ -0,0 +1,680 @@ +// بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيم + +// This file is part of Setheum. + +// Copyright (C) 2019-Present Setheum Labs. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// 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 . + +//!This module allows users to create secure reversible payments that keep +//! funds locked in a merchant's account until the off-chain goods are confirmed +//! to be received. Each payment gets assigned its own *judge* that can help +//! resolve any disputes between the two parties. + +//! ## Terminology +//! +//! - Created: A payment has been created and the amount arrived to its +//! destination but it's locked. +//! - NeedsReview: The payment has bee disputed and is awaiting settlement by a +//! judge. +//! - IncentivePercentage: A small share of the payment amount is held in escrow +//! until a payment is completed/cancelled. The Incentive Percentage +//! represents this value. +//! - Resolver Account: A resolver account is assigned to every payment created, +//! this account has the privilege to cancel/release a payment that has been +//! disputed. +//! - Remark: The module allows to create payments by optionally providing some +//! extra(limited) amount of bytes, this is referred to as Remark. This can be +//! used by a marketplace to separate/tag payments. +//! - CancelBufferBlockLength: This is the time window where the recipient can +//! dispute a cancellation request from the payment creator. + +//! Extrinsics +//! +//! - `pay` - Create an payment for the given currencyid/amount +//! - `pay_with_remark` - Create a payment with a remark, can be used to tag +//! payments +//! - `release` - Release the payment amount to recipent +//! - `cancel` - Allows the recipient to cancel the payment and release the +//! payment amount to creator +//! - `resolve_release_payment` - Allows assigned judge to release a payment +//! - `resolve_cancel_payment` - Allows assigned judge to cancel a payment +//! - `request_refund` - Allows the creator of the payment to trigger cancel +//! with a buffer time. +//! - `claim_refund` - Allows the creator to claim payment refund after buffer +//! time +//! - `dispute_refund` - Allows the recipient to dispute the payment request of +//! sender +//! - `request_payment` - Create a payment that can be completed by the sender +//! using the `accept_and_pay` extrinsic. +//! - `accept_and_pay` - Allows the sender to fulfill a payment request created +//! by a recipient + +//! Types +//! +//! The `PaymentDetail` struct stores information about the payment/escrow. A +//! "payment" on Setheum Pay is similar to an escrow, it is used to guarantee +//! proof of funds and can be released once an agreed upon condition has reached +//! between the payment creator and recipient. The payment lifecycle is tracked +//! using the state field. + +//! The `PaymentState` enum tracks the possible states that a payment can be in. +//! When a payment is 'completed' or 'cancelled' it is removed from storage and +//! hence not tracked by a state. +#![cfg_attr(not(feature = "std"), no_std)] +pub use pallet::*; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +pub mod types; +pub mod weights; + +#[frame_support::pallet] +pub mod pallet { + pub use crate::{ + types::{DisputeResolver, FeeHandler, PaymentDetail, PaymentHandler, PaymentState, ScheduledTask, Task}, + weights::WeightInfo, + }; + use frame_support::{ + dispatch::DispatchResultWithPostInfo, fail, pallet_prelude::*, require_transactional, + storage::bounded_btree_map::BoundedBTreeMap, traits::tokens::BalanceStatus, + }; + use frame_system::pallet_prelude::*; + use orml_traits::{MultiCurrency, MultiReservableCurrency}; + use sp_runtime::{ + traits::{CheckedAdd, Saturating}, + Percent, + }; + use sp_std::vec::Vec; + + pub type BalanceOf = <::Asset as MultiCurrency<::AccountId>>::Balance; + pub type AssetIdOf = <::Asset as MultiCurrency<::AccountId>>::CurrencyId; + pub type BoundedDataOf = BoundedVec::MaxRemarkLength>; + /// type of ScheduledTask used by the pallet + pub type ScheduledTaskOf = ScheduledTask>; + /// list of ScheduledTasks, stored as a BoundedBTreeMap + pub type ScheduledTaskList = BoundedBTreeMap< + ( + ::AccountId, + ::AccountId, + ), + ScheduledTaskOf, + ::MaxRemarkLength, + >; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// Because this pallet emits events, it depends on the runtime's + /// definition of an event. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// the type of assets this pallet can hold in payment + type Asset: MultiReservableCurrency; + /// Dispute resolution account + type DisputeResolver: DisputeResolver; + /// Fee handler trait + type FeeHandler: FeeHandler; + /// Incentive percentage - amount withheld from sender + #[pallet::constant] + type IncentivePercentage: Get; + /// Maximum permitted size of `Remark` + #[pallet::constant] + type MaxRemarkLength: Get; + /// Buffer period - number of blocks to wait before user can claim + /// canceled payment + #[pallet::constant] + type CancelBufferBlockLength: Get>; + /// Buffer period - number of blocks to wait before user can claim + /// canceled payment + #[pallet::constant] + type MaxScheduledTaskListLength: Get; + //// Type representing the weight of this pallet + type WeightInfo: WeightInfo; + } + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::storage] + #[pallet::getter(fn payment)] + /// Payments created by a user, this method of storageDoubleMap is chosen + /// since there is no usecase for listing payments by provider/currency. The + /// payment will only be referenced by the creator in any transaction of + /// interest. The storage map keys are the creator and the recipient, this + /// also ensures that for any (sender,recipient) combo, only a single + /// payment is active. The history of payment is not stored. + pub(super) type Payment = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, // payment creator + Blake2_128Concat, + T::AccountId, // payment recipient + PaymentDetail, + >; + + #[pallet::storage] + #[pallet::getter(fn tasks)] + /// Store the list of tasks to be executed in the on_idle function + pub(super) type ScheduledTasks = StorageValue<_, ScheduledTaskList, ValueQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A new payment has been created + PaymentCreated { + from: T::AccountId, + asset: AssetIdOf, + amount: BalanceOf, + remark: Option>, + }, + /// Payment amount released to the recipient + PaymentReleased { from: T::AccountId, to: T::AccountId }, + /// Payment has been cancelled by the creator + PaymentCancelled { from: T::AccountId, to: T::AccountId }, + /// A payment that NeedsReview has been resolved by Judge + PaymentResolved { + from: T::AccountId, + to: T::AccountId, + recipient_share: Percent, + }, + /// the payment creator has created a refund request + PaymentCreatorRequestedRefund { + from: T::AccountId, + to: T::AccountId, + expiry: BlockNumberFor, + }, + /// the refund request from creator was disputed by recipient + PaymentRefundDisputed { from: T::AccountId, to: T::AccountId }, + /// Payment request was created by recipient + PaymentRequestCreated { from: T::AccountId, to: T::AccountId }, + /// Payment request was completed by sender + PaymentRequestCompleted { from: T::AccountId, to: T::AccountId }, + } + + #[pallet::error] + pub enum Error { + /// The selected payment does not exist + InvalidPayment, + /// The selected payment cannot be released + PaymentAlreadyReleased, + /// The selected payment already exists and is in process + PaymentAlreadyInProcess, + /// Action permitted only for whitelisted users + InvalidAction, + /// Payment is in review state and cannot be modified + PaymentNeedsReview, + /// Unexpeted math error + MathError, + /// Payment request has not been created + RefundNotRequested, + /// Dispute period has not passed + DisputePeriodNotPassed, + /// The automatic cancelation queue cannot accept + RefundQueueFull, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + /// Hook that execute when there is leftover space in a block + /// This function will look for any pending scheduled tasks that can + /// be executed and will process them. + fn on_idle(now: BlockNumberFor, remaining_weight: Weight) -> Weight { + const MAX_TASKS_TO_PROCESS: usize = 5; + // used to read the task list + let mut used_weight = T::WeightInfo::remove_task(); + let cancel_weight = T::WeightInfo::cancel(); + + // calculate count of tasks that can be processed with remaining weight + let possible_task_count: usize = remaining_weight + .saturating_sub(used_weight) + .saturating_div(cancel_weight.ref_time()) + .ref_time() + .try_into() + .unwrap_or(MAX_TASKS_TO_PROCESS); + + ScheduledTasks::::mutate(|tasks| { + let mut task_list: Vec<_> = tasks + .clone() + .into_iter() + .take(possible_task_count) + // leave out tasks in the future + .filter(|(_, ScheduledTask { when, task })| when <= &now && matches!(task, Task::Cancel)) + .collect(); + + // order by oldest task to process + task_list.sort_by(|(_, t), (_, x)| x.when.cmp(&t.when)); + + while !task_list.is_empty() && used_weight.all_lte(remaining_weight) { + if let Some((account_pair, _)) = task_list.pop() { + used_weight = used_weight.saturating_add(cancel_weight); + // remove the task form the tasks storage + tasks.remove(&account_pair); + + // process the cancel payment + if >::settle_payment( + &account_pair.0, + &account_pair.1, + Percent::from_percent(0), + ) + .is_err() + { + // log the payment refund failure + log::warn!( + target: "runtime::payments", + "Warning: Unable to process payment refund!" + ); + } else { + // emit the cancel event if the refund was successful + Self::deposit_event(Event::PaymentCancelled { + from: account_pair.0, + to: account_pair.1, + }); + } + } + } + }); + used_weight + } + } + + #[pallet::call] + impl Pallet { + /// This allows any user to create a new payment, that releases only to + /// specified recipient The only action is to store the details of this + /// payment in storage and reserve the specified amount. User also has + /// the option to add a remark, this remark can then be used to run + /// custom logic and trigger alternate payment flows. the specified + /// amount. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::pay(T::MaxRemarkLength::get()))] + pub fn pay( + origin: OriginFor, + recipient: T::AccountId, + asset: AssetIdOf, + #[pallet::compact] amount: BalanceOf, + remark: Option>, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + + // create PaymentDetail and add to storage + let payment_detail = >::create_payment( + &who, + &recipient, + asset, + amount, + PaymentState::Created, + T::IncentivePercentage::get(), + remark.as_ref().map(|x| x.as_slice()), + )?; + // reserve funds for payment + >::reserve_payment_amount(&who, &recipient, payment_detail)?; + // emit paymentcreated event + Self::deposit_event(Event::PaymentCreated { + from: who, + asset, + amount, + remark, + }); + Ok(().into()) + } + + /// Release any created payment, this will transfer the reserved amount + /// from the creator of the payment to the assigned recipient + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::release())] + pub fn release(origin: OriginFor, to: T::AccountId) -> DispatchResultWithPostInfo { + let from = ensure_signed(origin)?; + + // ensure the payment is in Created state + let payment = Payment::::get(&from, &to).ok_or(Error::::InvalidPayment)?; + ensure!(payment.state == PaymentState::Created, Error::::InvalidAction); + + // release is a settle_payment with 100% recipient_share + >::settle_payment(&from, &to, Percent::from_percent(100))?; + + Self::deposit_event(Event::PaymentReleased { from, to }); + Ok(().into()) + } + + /// Cancel a payment in created state, this will release the reserved + /// back to creator of the payment. This extrinsic can only be called by + /// the recipient of the payment + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::cancel())] + pub fn cancel(origin: OriginFor, creator: T::AccountId) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + if let Some(payment) = Payment::::get(&creator, &who) { + match payment.state { + // call settle payment with recipient_share=0, this refunds the sender + PaymentState::Created => { + >::settle_payment(&creator, &who, Percent::from_percent(0))?; + Self::deposit_event(Event::PaymentCancelled { from: creator, to: who }); + } + // if the payment is in state PaymentRequested, remove from storage + PaymentState::PaymentRequested => Payment::::remove(&creator, &who), + _ => fail!(Error::::InvalidAction), + } + } + Ok(().into()) + } + + /// This extrinsic is used to resolve disputes between the creator and + /// recipient of the payment. + /// This extrinsic allows the assigned judge to + /// cancel/release/partial_release the payment. + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::resolve_payment())] + pub fn resolve_payment( + origin: OriginFor, + from: T::AccountId, + recipient: T::AccountId, + recipient_share: Percent, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let account_pair = (from, recipient); + // ensure the caller is the assigned resolver + if let Some(payment) = Payment::::get(&account_pair.0, &account_pair.1) { + ensure!(who == payment.resolver_account, Error::::InvalidAction); + ensure!( + payment.state != PaymentState::PaymentRequested, + Error::::InvalidAction + ); + if matches!(payment.state, PaymentState::RefundRequested { .. }) { + ScheduledTasks::::mutate(|tasks| { + tasks.remove(&account_pair); + }) + } + } + // try to update the payment to new state + >::settle_payment(&account_pair.0, &account_pair.1, recipient_share)?; + Self::deposit_event(Event::PaymentResolved { + from: account_pair.0, + to: account_pair.1, + recipient_share, + }); + Ok(().into()) + } + + /// Allow the creator of a payment to initiate a refund that will return + /// the funds after a configured amount of time that the reveiver has to + /// react and oppose the request + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::request_refund())] + pub fn request_refund(origin: OriginFor, recipient: T::AccountId) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + + Payment::::try_mutate(who.clone(), recipient.clone(), |maybe_payment| -> DispatchResult { + // ensure the payment exists + let payment = maybe_payment.as_mut().ok_or(Error::::InvalidPayment)?; + // refunds only possible for payments in created state + ensure!(payment.state == PaymentState::Created, Error::::InvalidAction); + + // set the payment to requested refund + let current_block = frame_system::Pallet::::block_number(); + let cancel_block = current_block + .checked_add(&T::CancelBufferBlockLength::get()) + .ok_or(Error::::MathError)?; + + ScheduledTasks::::try_mutate(|task_list| -> DispatchResult { + task_list + .try_insert( + (who.clone(), recipient.clone()), + ScheduledTask { + task: Task::Cancel, + when: cancel_block, + }, + ) + .map_err(|_| Error::::RefundQueueFull)?; + Ok(()) + })?; + + payment.state = PaymentState::RefundRequested { cancel_block }; + + Self::deposit_event(Event::PaymentCreatorRequestedRefund { + from: who, + to: recipient, + expiry: cancel_block, + }); + + Ok(()) + })?; + + Ok(().into()) + } + + /// Allow payment recipient to dispute the refund request from the + /// payment creator This does not cancel the request, instead sends the + /// payment to a NeedsReview state The assigned resolver account can + /// then change the state of the payment after review. + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::dispute_refund())] + pub fn dispute_refund(origin: OriginFor, creator: T::AccountId) -> DispatchResultWithPostInfo { + use PaymentState::*; + let who = ensure_signed(origin)?; + + Payment::::try_mutate( + creator.clone(), + who.clone(), // should be called by the payment recipient + |maybe_payment| -> DispatchResult { + // ensure the payment exists + let payment = maybe_payment.as_mut().ok_or(Error::::InvalidPayment)?; + // ensure the payment is in Requested Refund state + match payment.state { + RefundRequested { cancel_block } => { + ensure!( + cancel_block > frame_system::Pallet::::block_number(), + Error::::InvalidAction + ); + + payment.state = PaymentState::NeedsReview; + + // remove the payment from scheduled tasks + ScheduledTasks::::try_mutate(|task_list| -> DispatchResult { + task_list + .remove(&(creator.clone(), who.clone())) + .ok_or(Error::::InvalidAction)?; + Ok(()) + })?; + + Self::deposit_event(Event::PaymentRefundDisputed { from: creator, to: who }); + } + _ => fail!(Error::::InvalidAction), + } + + Ok(()) + }, + )?; + + Ok(().into()) + } + + // Creates a new payment with the given details. This can be called by the + // recipient of the payment to create a payment and then completed by the sender + // using the `accept_and_pay` extrinsic. The payment will be in + // PaymentRequested State and can only be modified by the `accept_and_pay` + // extrinsic. + #[pallet::call_index(6)] + #[pallet::weight(T::WeightInfo::request_payment())] + pub fn request_payment( + origin: OriginFor, + from: T::AccountId, + asset: AssetIdOf, + #[pallet::compact] amount: BalanceOf, + ) -> DispatchResultWithPostInfo { + let to = ensure_signed(origin)?; + + // create PaymentDetail and add to storage + >::create_payment( + &from, + &to, + asset, + amount, + PaymentState::PaymentRequested, + Percent::from_percent(0), + None, + )?; + + Self::deposit_event(Event::PaymentRequestCreated { from, to }); + + Ok(().into()) + } + + // This extrinsic allows the sender to fulfill a payment request created by a + // recipient. The amount will be transferred to the recipient and payment + // removed from storage + #[pallet::call_index(7)] + #[pallet::weight(T::WeightInfo::accept_and_pay())] + pub fn accept_and_pay(origin: OriginFor, to: T::AccountId) -> DispatchResultWithPostInfo { + let from = ensure_signed(origin)?; + + let payment = Payment::::get(&from, &to).ok_or(Error::::InvalidPayment)?; + + ensure!( + payment.state == PaymentState::PaymentRequested, + Error::::InvalidAction + ); + + // reserve all the fees from the sender + >::reserve_payment_amount(&from, &to, payment)?; + + // release the payment and delete the payment from storage + >::settle_payment(&from, &to, Percent::from_percent(100))?; + + Self::deposit_event(Event::PaymentRequestCompleted { from, to }); + + Ok(().into()) + } + } + + impl PaymentHandler for Pallet { + /// The function will create a new payment. The fee and incentive + /// amounts will be calculated and the `PaymentDetail` will be added to + /// storage. + #[require_transactional] + fn create_payment( + from: &T::AccountId, + recipient: &T::AccountId, + asset: AssetIdOf, + amount: BalanceOf, + payment_state: PaymentState, + incentive_percentage: Percent, + remark: Option<&[u8]>, + ) -> Result, sp_runtime::DispatchError> { + Payment::::try_mutate( + from, + recipient, + |maybe_payment| -> Result, sp_runtime::DispatchError> { + // only payment requests can be overwritten + if let Some(payment) = maybe_payment { + ensure!( + payment.state == PaymentState::PaymentRequested, + Error::::PaymentAlreadyInProcess + ); + } + + // Calculate incentive amount - this is to insentivise the user to release + // the funds once a transaction has been completed + let incentive_amount = incentive_percentage.mul_floor(amount); + + let mut new_payment = PaymentDetail { + asset, + amount, + incentive_amount, + state: payment_state, + resolver_account: T::DisputeResolver::get_resolver_account(), + fee_detail: None, + }; + + // Calculate fee amount - this will be implemented based on the custom + // implementation of the fee provider + let (fee_recipient, fee_percent) = T::FeeHandler::apply_fees(from, recipient, &new_payment, remark); + let fee_amount = fee_percent.mul_floor(amount); + new_payment.fee_detail = Some((fee_recipient, fee_amount)); + + *maybe_payment = Some(new_payment.clone()); + + Ok(new_payment) + }, + ) + } + + /// The function will reserve the fees+transfer amount from the `from` + /// account. After reserving the payment.amount will be transferred to + /// the recipient but will stay in Reserve state. + #[require_transactional] + fn reserve_payment_amount(from: &T::AccountId, to: &T::AccountId, payment: PaymentDetail) -> DispatchResult { + let fee_amount = payment.fee_detail.map(|(_, f)| f).unwrap_or_else(|| 0u32.into()); + + let total_fee_amount = payment.incentive_amount.saturating_add(fee_amount); + let total_amount = total_fee_amount.saturating_add(payment.amount); + + // reserve the total amount from payment creator + T::Asset::reserve(payment.asset, from, total_amount)?; + // transfer payment amount to recipient -- keeping reserve status + T::Asset::repatriate_reserved(payment.asset, from, to, payment.amount, BalanceStatus::Reserved)?; + Ok(()) + } + + /// This function allows the caller to settle the payment by specifying + /// a recipient_share this will unreserve the fee+incentive to sender + /// and unreserve transferred amount to recipient if the settlement is a + /// release (ie recipient_share=100), the fee is transferred to + /// fee_recipient For cancelling a payment, recipient_share = 0 + /// For releasing a payment, recipient_share = 100 + /// In other cases, the custom recipient_share can be specified + fn settle_payment(from: &T::AccountId, to: &T::AccountId, recipient_share: Percent) -> DispatchResult { + Payment::::try_mutate(from, to, |maybe_payment| -> DispatchResult { + let payment = maybe_payment.take().ok_or(Error::::InvalidPayment)?; + + // unreserve the incentive amount and fees from the owner account + match payment.fee_detail { + Some((fee_recipient, fee_amount)) => { + T::Asset::unreserve(payment.asset, from, payment.incentive_amount.saturating_add(fee_amount)); + // transfer fee to marketplace if operation is not cancel + if recipient_share != Percent::zero() { + T::Asset::transfer( + payment.asset, + from, // fee is paid by payment creator + &fee_recipient, // account of fee recipient + fee_amount, // amount of fee + )?; + } + } + None => { + T::Asset::unreserve(payment.asset, from, payment.incentive_amount); + } + }; + + // Unreserve the transfer amount + T::Asset::unreserve(payment.asset, to, payment.amount); + + let amount_to_recipient = recipient_share.mul_floor(payment.amount); + let amount_to_sender = payment.amount.saturating_sub(amount_to_recipient); + // send share to recipient + T::Asset::transfer(payment.asset, to, from, amount_to_sender)?; + + Ok(()) + })?; + Ok(()) + } + + fn get_payment_details(from: &T::AccountId, to: &T::AccountId) -> Option> { + Payment::::get(from, to) + } + } +} diff --git a/blockchain/modules/setheum-pay/src/mock.rs b/blockchain/modules/setheum-pay/src/mock.rs new file mode 100644 index 00000000..20a8ae4e --- /dev/null +++ b/blockchain/modules/setheum-pay/src/mock.rs @@ -0,0 +1,184 @@ +// بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيم + +// This file is part of Setheum. + +// Copyright (C) 2019-Present Setheum Labs. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// 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 . + +use crate as pay; +use crate::PaymentDetail; +use frame_support::{ + derive_impl, + dispatch::DispatchClass, + parameter_types, + traits::{ConstU32, Contains, Hooks, OnFinalize}, +}; +use frame_system as system; +use orml_traits::parameter_type_with_key; +use sp_runtime::{traits::IdentityLookup, BuildStorage, Percent}; + +type Block = frame_system::mocking::MockBlock; +pub type Balance = u128; + +pub type AccountId = u8; +pub const PAYMENT_CREATOR: AccountId = 10; +pub const PAYMENT_RECIPENT: AccountId = 11; +pub const PAYMENT_CREATOR_TWO: AccountId = 30; +pub const PAYMENT_RECIPENT_TWO: AccountId = 31; +pub const CURRENCY_ID: u32 = 1; +pub const RESOLVER_ACCOUNT: AccountId = 12; +pub const FEE_RECIPIENT_ACCOUNT: AccountId = 20; +pub const PAYMENT_RECIPENT_FEE_CHARGED: AccountId = 21; +pub const INCENTIVE_PERCENTAGE: u8 = 10; +pub const MARKETPLACE_FEE_PERCENTAGE: u8 = 10; +pub const CANCEL_BLOCK_BUFFER: u64 = 600; + +frame_support::construct_runtime!( + pub enum Test { + System: frame_system, + Tokens: orml_tokens, + Payment: pay, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl system::Config for Test { + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Block = Block; +} + +parameter_type_with_key! { + pub ExistentialDeposits: |_currency_id: u32| -> Balance { + 0u128 + }; +} +parameter_types! { + pub const MaxLocks: u32 = 50; +} +pub type ReserveIdentifier = [u8; 8]; + +pub struct MockDustRemovalWhitelist; +impl Contains for MockDustRemovalWhitelist { + fn contains(_a: &AccountId) -> bool { + false + } +} + +impl orml_tokens::Config for Test { + type Amount = i64; + type Balance = Balance; + type CurrencyId = u32; + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposits = ExistentialDeposits; + type CurrencyHooks = (); + type WeightInfo = (); + type MaxLocks = MaxLocks; + type DustRemovalWhitelist = MockDustRemovalWhitelist; + type MaxReserves = ConstU32<2>; + type ReserveIdentifier = ReserveIdentifier; +} + +pub struct MockDisputeResolver; +impl crate::types::DisputeResolver for MockDisputeResolver { + fn get_resolver_account() -> AccountId { + RESOLVER_ACCOUNT + } +} + +pub struct MockFeeHandler; +impl crate::types::FeeHandler for MockFeeHandler { + fn apply_fees( + _from: &AccountId, + to: &AccountId, + _detail: &PaymentDetail, + _remark: Option<&[u8]>, + ) -> (AccountId, Percent) { + match to { + &PAYMENT_RECIPENT_FEE_CHARGED => (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(MARKETPLACE_FEE_PERCENTAGE)), + _ => (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(0)), + } + } +} + +parameter_types! { + pub const IncentivePercentage: Percent = Percent::from_percent(INCENTIVE_PERCENTAGE); + pub const MaxRemarkLength: u32 = 50; + pub const CancelBufferBlockLength: u64 = CANCEL_BLOCK_BUFFER; + pub const MaxScheduledTaskListLength : u32 = 5; +} + +impl pay::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Asset = Tokens; + type DisputeResolver = MockDisputeResolver; + type IncentivePercentage = IncentivePercentage; + type FeeHandler = MockFeeHandler; + type MaxRemarkLength = MaxRemarkLength; + type CancelBufferBlockLength = CancelBufferBlockLength; + type MaxScheduledTaskListLength = MaxScheduledTaskListLength; + type WeightInfo = (); +} + +// Build genesis storage according to the mock runtime. +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = system::GenesisConfig::::default().build_storage().unwrap(); + + orml_tokens::GenesisConfig:: { + balances: vec![ + (PAYMENT_CREATOR, CURRENCY_ID, 100), + (PAYMENT_CREATOR_TWO, CURRENCY_ID, 100), + ], + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext: sp_io::TestExternalities = t.into(); + // need to set block number to 1 to test events + ext.execute_with(|| System::set_block_number(1)); + ext +} + +pub fn run_n_blocks(n: u64) -> u64 { + const IDLE_WEIGHT: u64 = 10_000_000_000; + const BUSY_WEIGHT: u64 = IDLE_WEIGHT / 1000; + + let start_block = System::block_number(); + + for block_number in (0..=n).map(|n| n + start_block) { + System::set_block_number(block_number); + + // Odd blocks gets busy + let idle_weight = if block_number % 2 == 0 { + IDLE_WEIGHT + } else { + BUSY_WEIGHT + }; + // ensure the on_idle is executed + >::register_extra_weight_unchecked( + Payment::on_idle(block_number, frame_support::weights::Weight::from_parts(idle_weight, 0)), + DispatchClass::Mandatory, + ); + + as OnFinalize>::on_finalize(block_number); + } + System::block_number() +} diff --git a/blockchain/modules/setheum-pay/src/tests.rs b/blockchain/modules/setheum-pay/src/tests.rs new file mode 100644 index 00000000..97f54116 --- /dev/null +++ b/blockchain/modules/setheum-pay/src/tests.rs @@ -0,0 +1,1470 @@ +// بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيم + +// This file is part of Setheum. + +// Copyright (C) 2019-Present Setheum Labs. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// 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 . + +use crate::{ + mock::*, + types::{PaymentDetail, PaymentState}, + weights::WeightInfo, + Payment as PaymentStore, PaymentHandler, ScheduledTask, ScheduledTasks, Task, +}; +use frame_support::{assert_noop, assert_ok, storage::with_transaction, traits::OnIdle, weights::Weight}; +use orml_traits::MultiCurrency; +use sp_runtime::{Percent, TransactionOutcome}; + +type Error = crate::Error; + +fn last_event() -> RuntimeEvent { + System::events().pop().expect("Event expected").event +} + +#[test] +fn test_pay_works() { + new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + + // the payment amount should not be reserved + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // should be able to create a payment with available balance + assert_ok!(Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + )); + assert_eq!( + last_event(), + crate::Event::::PaymentCreated { + from: PAYMENT_CREATOR, + asset: CURRENCY_ID, + amount: payment_amount, + remark: None + } + .into() + ); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: payment_amount, + incentive_amount: expected_incentive_amount, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + }) + ); + // the payment amount should be reserved correctly + // the amount + incentive should be removed from the sender account + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_incentive_amount + ); + // the incentive amount should be reserved in the sender account + assert_eq!( + Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + // the transferred amount should be reserved in the recipent account + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); + + // the payment should not be overwritten + assert_noop!( + Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + ), + crate::Error::::PaymentAlreadyInProcess + ); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: payment_amount, + incentive_amount: 2, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + }) + ); + }); +} + +#[test] +fn test_cancel_works() { + new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 40; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + + // should be able to create a payment with available balance + assert_ok!(Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + )); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: payment_amount, + incentive_amount: expected_incentive_amount, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + }) + ); + // the payment amount should be reserved + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_incentive_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // cancel should succeed when caller is the recipent + assert_ok!(Payment::cancel( + RuntimeOrigin::signed(PAYMENT_RECIPENT), + PAYMENT_CREATOR + )); + assert_eq!( + last_event(), + crate::Event::::PaymentCancelled { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT + } + .into() + ); + // the payment amount should be released back to creator + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // should be released from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + }); +} + +#[test] +fn test_release_works() { + new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 40; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + + // should be able to create a payment with available balance + assert_ok!(Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + )); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: payment_amount, + incentive_amount: expected_incentive_amount, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + }) + ); + // the payment amount should be reserved + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_incentive_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // should succeed for valid payment + assert_ok!(Payment::release( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT + )); + assert_eq!( + last_event(), + crate::Event::::PaymentReleased { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT + } + .into() + ); + // the payment amount should be transferred + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); + + // should be deleted from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + + // should be able to create another payment since previous is released + assert_ok!(Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + )); + // the payment amount should be reserved + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - (payment_amount * 2) - expected_incentive_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); + }); +} + +#[test] +fn test_resolve_payment_works() { + new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 40; + + // should be able to create a payment with available balance + assert_ok!(Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + )); + + // should fail for non whitelisted caller + assert_noop!( + Payment::resolve_payment( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + Percent::from_percent(100) + ), + Error::InvalidAction + ); + + // should be able to release a payment + assert_ok!(Payment::resolve_payment( + RuntimeOrigin::signed(RESOLVER_ACCOUNT), + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + Percent::from_percent(100) + )); + assert_eq!( + last_event(), + crate::Event::::PaymentResolved { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT, + recipient_share: Percent::from_percent(100) + } + .into() + ); + + // the payment amount should be transferred + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); + + // should be removed from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + + assert_ok!(Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + )); + + // should be able to cancel a payment + assert_ok!(Payment::resolve_payment( + RuntimeOrigin::signed(RESOLVER_ACCOUNT), + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + Percent::from_percent(0) + )); + assert_eq!( + last_event(), + crate::Event::::PaymentResolved { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT, + recipient_share: Percent::from_percent(0) + } + .into() + ); + + // the payment amount should be transferred + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); + + // should be released from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + }); +} + +#[test] +fn test_charging_fee_payment_works() { + new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 40; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + let expected_fee_amount = payment_amount / MARKETPLACE_FEE_PERCENTAGE as u128; + + // should be able to create a payment with available balance + assert_ok!(Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED, + CURRENCY_ID, + payment_amount, + None + )); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: payment_amount, + incentive_amount: expected_incentive_amount, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, expected_fee_amount)), + }) + ); + // the payment amount should be reserved + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_fee_amount - expected_incentive_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); + + // should succeed for valid payment + assert_ok!(Payment::release( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED + )); + // the payment amount should be transferred + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_fee_amount + ); + assert_eq!( + Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_fee_amount + ); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), + payment_amount + ); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), + expected_fee_amount + ); + }); +} + +#[test] +fn test_charging_fee_payment_works_when_canceled() { + new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 40; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + let expected_fee_amount = payment_amount / MARKETPLACE_FEE_PERCENTAGE as u128; + + // should be able to create a payment with available balance + assert_ok!(Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED, + CURRENCY_ID, + payment_amount, + None + )); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: payment_amount, + incentive_amount: expected_incentive_amount, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, expected_fee_amount)), + }) + ); + // the payment amount should be reserved + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_fee_amount - expected_incentive_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); + + // should succeed for valid payment + assert_ok!(Payment::cancel( + RuntimeOrigin::signed(PAYMENT_RECIPENT_FEE_CHARGED), + PAYMENT_CREATOR + )); + // the payment amount should be transferred + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 0); + }); +} + +#[test] +fn test_pay_with_remark_works() { + new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 40; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + + // should be able to create a payment with available balance + assert_ok!(Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + Some(vec![1u8; 10].try_into().unwrap()) + )); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: payment_amount, + incentive_amount: expected_incentive_amount, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + }) + ); + // the payment amount should be reserved correctly + // the amount + incentive should be removed from the sender account + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_incentive_amount + ); + // the incentive amount should be reserved in the sender account + assert_eq!( + Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + // the transferred amount should be reserved in the recipent account + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); + + // the payment should not be overwritten + assert_noop!( + Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + ), + crate::Error::::PaymentAlreadyInProcess + ); + + assert_eq!( + last_event(), + crate::Event::::PaymentCreated { + from: PAYMENT_CREATOR, + asset: CURRENCY_ID, + amount: payment_amount, + remark: Some(vec![1u8; 10].try_into().unwrap()) + } + .into() + ); + }); +} + +#[test] +fn test_do_not_overwrite_logic_works() { + new_test_ext().execute_with(|| { + let payment_amount = 40; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + + assert_ok!(Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + )); + + assert_noop!( + Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + ), + crate::Error::::PaymentAlreadyInProcess + ); + + // set payment state to NeedsReview + PaymentStore::::insert( + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + PaymentDetail { + asset: CURRENCY_ID, + amount: payment_amount, + incentive_amount: expected_incentive_amount, + state: PaymentState::NeedsReview, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + }, + ); + + // the payment should not be overwritten + assert_noop!( + Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + ), + crate::Error::::PaymentAlreadyInProcess + ); + }); +} + +#[test] +fn test_request_refund() { + new_test_ext().execute_with(|| { + let payment_amount = 20; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + let expected_cancel_block = CANCEL_BLOCK_BUFFER + 1; + + assert_ok!(Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + )); + + assert_ok!(Payment::request_refund( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT + )); + + // do not overwrite payment + assert_noop!( + Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + ), + crate::Error::::PaymentAlreadyInProcess + ); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: payment_amount, + incentive_amount: expected_incentive_amount, + state: PaymentState::RefundRequested { + cancel_block: expected_cancel_block + }, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + }) + ); + + assert_eq!( + last_event(), + crate::Event::::PaymentCreatorRequestedRefund { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT, + expiry: expected_cancel_block + } + .into() + ); + }); +} + +#[test] +fn test_dispute_refund() { + new_test_ext().execute_with(|| { + let payment_amount = 20; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + let expected_cancel_block = CANCEL_BLOCK_BUFFER + 1; + + assert_ok!(Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + )); + + // cannot dispute if refund is not requested + assert_noop!( + Payment::dispute_refund(RuntimeOrigin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR), + Error::InvalidAction + ); + // creator requests a refund + assert_ok!(Payment::request_refund( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT + )); + // ensure the request is added to the refund queue + let scheduled_tasks_list = ScheduledTasks::::get(); + assert_eq!( + scheduled_tasks_list.get(&(PAYMENT_CREATOR, PAYMENT_RECIPENT)).unwrap(), + &ScheduledTask { + task: Task::Cancel, + when: expected_cancel_block + } + ); + + // recipient disputes the refund request + assert_ok!(Payment::dispute_refund( + RuntimeOrigin::signed(PAYMENT_RECIPENT), + PAYMENT_CREATOR + )); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: payment_amount, + incentive_amount: expected_incentive_amount, + state: PaymentState::NeedsReview, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + }) + ); + + assert_eq!( + last_event(), + crate::Event::::PaymentRefundDisputed { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT, + } + .into() + ); + + // ensure the request is removed from the refund queue + let scheduled_tasks_list = ScheduledTasks::::get(); + assert_eq!(scheduled_tasks_list.get(&(PAYMENT_CREATOR, PAYMENT_RECIPENT)), None); + }); +} + +#[test] +fn test_request_payment() { + new_test_ext().execute_with(|| { + let payment_amount = 20; + let expected_incentive_amount = 0; + + assert_ok!(Payment::request_payment( + RuntimeOrigin::signed(PAYMENT_RECIPENT), + PAYMENT_CREATOR, + CURRENCY_ID, + payment_amount, + )); + + assert_noop!( + Payment::request_refund(RuntimeOrigin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), + crate::Error::::InvalidAction + ); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: payment_amount, + incentive_amount: expected_incentive_amount, + state: PaymentState::PaymentRequested, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + }) + ); + + assert_eq!( + last_event(), + crate::Event::::PaymentRequestCreated { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT, + } + .into() + ); + }); +} + +#[test] +fn test_requested_payment_cannot_be_released() { + new_test_ext().execute_with(|| { + let payment_amount = 20; + + assert_ok!(Payment::request_payment( + RuntimeOrigin::signed(PAYMENT_RECIPENT), + PAYMENT_CREATOR, + CURRENCY_ID, + payment_amount, + )); + + // requested payment cannot be released + assert_noop!( + Payment::release(RuntimeOrigin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), + Error::InvalidAction + ); + }); +} + +#[test] +fn test_requested_payment_can_be_cancelled_by_requestor() { + new_test_ext().execute_with(|| { + let payment_amount = 20; + + assert_ok!(Payment::request_payment( + RuntimeOrigin::signed(PAYMENT_RECIPENT), + PAYMENT_CREATOR, + CURRENCY_ID, + payment_amount, + )); + + assert_ok!(Payment::cancel( + RuntimeOrigin::signed(PAYMENT_RECIPENT), + PAYMENT_CREATOR + )); + + // the request should be removed from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + }); +} + +#[test] +fn test_accept_and_pay() { + new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + let expected_incentive_amount = 0; + + assert_ok!(Payment::request_payment( + RuntimeOrigin::signed(PAYMENT_RECIPENT), + PAYMENT_CREATOR, + CURRENCY_ID, + payment_amount, + )); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: payment_amount, + incentive_amount: expected_incentive_amount, + state: PaymentState::PaymentRequested, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + }) + ); + + assert_ok!(Payment::accept_and_pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + )); + + // the payment amount should be transferred + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); + + // should be deleted from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + + assert_eq!( + last_event(), + crate::Event::::PaymentRequestCompleted { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT, + } + .into() + ); + }); +} + +#[test] +fn test_accept_and_pay_should_fail_for_non_payment_requested() { + new_test_ext().execute_with(|| { + assert_ok!(Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + None + )); + + assert_noop!( + Payment::accept_and_pay(RuntimeOrigin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT,), + Error::InvalidAction + ); + }); +} + +#[test] +fn test_accept_and_pay_should_charge_fee_correctly() { + new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + let expected_incentive_amount = 0; + let expected_fee_amount = payment_amount / MARKETPLACE_FEE_PERCENTAGE as u128; + + assert_ok!(Payment::request_payment( + RuntimeOrigin::signed(PAYMENT_RECIPENT_FEE_CHARGED), + PAYMENT_CREATOR, + CURRENCY_ID, + payment_amount, + )); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: payment_amount, + incentive_amount: expected_incentive_amount, + state: PaymentState::PaymentRequested, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, expected_fee_amount)), + }) + ); + + assert_ok!(Payment::accept_and_pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED, + )); + + // the payment amount should be transferred + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_fee_amount + ); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), + payment_amount + ); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), + expected_fee_amount + ); + + // should be deleted from storage + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + None + ); + + assert_eq!( + last_event(), + crate::Event::::PaymentRequestCompleted { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT_FEE_CHARGED, + } + .into() + ); + }); +} + +#[test] +fn test_create_payment_works() { + new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + let expected_fee_amount = 0; + + // the payment amount should not be reserved + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // should be able to create a payment with available balance within a + // transaction + assert_ok!(with_transaction(|| TransactionOutcome::Commit({ + >::create_payment( + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + PaymentState::Created, + Percent::from_percent(INCENTIVE_PERCENTAGE), + Some(&[1u8; 10]), + ) + }))); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: payment_amount, + incentive_amount: expected_incentive_amount, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, expected_fee_amount)), + }) + ); + + // the payment should not be overwritten + assert_noop!( + with_transaction(|| TransactionOutcome::Commit({ + >::create_payment( + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + PaymentState::Created, + Percent::from_percent(INCENTIVE_PERCENTAGE), + Some(&[1u8; 10]), + ) + })), + Error::PaymentAlreadyInProcess + ); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: payment_amount, + incentive_amount: expected_incentive_amount, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, expected_fee_amount)), + }) + ); + }); +} + +#[test] +fn test_reserve_payment_amount_works() { + new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + let expected_fee_amount = 0; + + // the payment amount should not be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // should be able to create a payment with available balance within a + // transaction + assert_ok!(with_transaction(|| TransactionOutcome::Commit({ + >::create_payment( + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + PaymentState::Created, + Percent::from_percent(INCENTIVE_PERCENTAGE), + Some(&[1u8; 10]), + ) + }))); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: payment_amount, + incentive_amount: expected_incentive_amount, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, expected_fee_amount)), + }) + ); + + assert_ok!(with_transaction(|| TransactionOutcome::Commit({ + >::reserve_payment_amount( + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).unwrap(), + ) + }))); + // the payment amount should be reserved correctly + // the amount + incentive should be removed from the sender account + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_incentive_amount + ); + // the incentive amount should be reserved in the sender account + assert_eq!( + Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + // the transferred amount should be reserved in the recipent account + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); + + // the payment should not be overwritten + assert_noop!( + with_transaction(|| TransactionOutcome::Commit({ + >::create_payment( + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + PaymentState::Created, + Percent::from_percent(INCENTIVE_PERCENTAGE), + Some(&[1u8; 10]), + ) + })), + Error::PaymentAlreadyInProcess + ); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: payment_amount, + incentive_amount: expected_incentive_amount, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, expected_fee_amount)), + }) + ); + }); +} + +#[test] +fn test_settle_payment_works_for_cancel() { + new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + + // the payment amount should not be reserved + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // should be able to create a payment with available balance within a + // transaction + assert_ok!(Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + )); + + assert_ok!(with_transaction(|| TransactionOutcome::Commit({ + >::settle_payment( + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, + Percent::from_percent(0), + ) + }))); + + // the payment amount should be released back to creator + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // should be released from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + }); +} + +#[test] +fn test_settle_payment_works_for_release() { + new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + + // the payment amount should not be reserved + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // should be able to create a payment with available balance within a + // transaction + assert_ok!(Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + )); + + assert_ok!(with_transaction(|| TransactionOutcome::Commit({ + >::settle_payment( + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, + Percent::from_percent(100), + ) + }))); + + // the payment amount should be transferred + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); + + // should be deleted from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + }); +} + +#[test] +fn test_settle_payment_works_for_70_30() { + new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 10; + let expected_fee_amount = payment_amount / MARKETPLACE_FEE_PERCENTAGE as u128; + + // the payment amount should not be reserved + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); + + // should be able to create a payment with available balance within a + // transaction + assert_ok!(Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED, + CURRENCY_ID, + payment_amount, + None + )); + + assert_ok!(with_transaction(|| TransactionOutcome::Commit({ + >::settle_payment( + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT_FEE_CHARGED, + Percent::from_percent(70), + ) + }))); + + let expected_amount_for_creator = creator_initial_balance - payment_amount - expected_fee_amount + + (Percent::from_percent(30) * payment_amount); + let expected_amount_for_recipient = Percent::from_percent(70) * payment_amount; + + // the payment amount should be transferred + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + expected_amount_for_creator + ); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), + expected_amount_for_recipient + ); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), + expected_fee_amount + ); + + // should be deleted from storage + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + None + ); + }); +} + +#[test] +fn test_settle_payment_works_for_50_50() { + new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 10; + let expected_fee_amount = payment_amount / MARKETPLACE_FEE_PERCENTAGE as u128; + + // the payment amount should not be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); + + // should be able to create a payment with available balance within a + // transaction + assert_ok!(Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED, + CURRENCY_ID, + payment_amount, + None + )); + + assert_ok!(with_transaction(|| TransactionOutcome::Commit({ + >::settle_payment( + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT_FEE_CHARGED, + Percent::from_percent(50), + ) + }))); + + let expected_amount_for_creator = creator_initial_balance - payment_amount - expected_fee_amount + + (Percent::from_percent(50) * payment_amount); + let expected_amount_for_recipient = Percent::from_percent(50) * payment_amount; + + // the payment amount should be transferred + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + expected_amount_for_creator + ); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), + expected_amount_for_recipient + ); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), + expected_fee_amount + ); + + // should be deleted from storage + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + None + ); + }); +} + +#[test] +fn test_automatic_refund_works() { + new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + const CANCEL_PERIOD: u64 = 600; + const CANCEL_BLOCK: u64 = CANCEL_PERIOD + 1; + + assert_ok!(Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + )); + + assert_ok!(Payment::request_refund( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT + )); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: payment_amount, + incentive_amount: expected_incentive_amount, + state: PaymentState::RefundRequested { + cancel_block: CANCEL_BLOCK + }, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + }) + ); + + let scheduled_tasks_list = ScheduledTasks::::get(); + assert_eq!( + scheduled_tasks_list.get(&(PAYMENT_CREATOR, PAYMENT_RECIPENT)).unwrap(), + &ScheduledTask { + task: Task::Cancel, + when: CANCEL_BLOCK + } + ); + + // run to one block before cancel and make sure data is same + assert_eq!(run_n_blocks(CANCEL_PERIOD - 1), 600); + let scheduled_tasks_list = ScheduledTasks::::get(); + assert_eq!( + scheduled_tasks_list.get(&(PAYMENT_CREATOR, PAYMENT_RECIPENT)).unwrap(), + &ScheduledTask { + task: Task::Cancel, + when: CANCEL_BLOCK + } + ); + + // run to after cancel block but odd blocks are busy + assert_eq!(run_n_blocks(1), 601); + // the payment is still not processed since the block was busy + assert!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).is_some()); + + // next block has spare weight to process the payment + assert_eq!(run_n_blocks(1), 602); + // the payment should be removed from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + + // the scheduled storage should be cleared + let scheduled_tasks_list = ScheduledTasks::::get(); + assert_eq!(scheduled_tasks_list.get(&(PAYMENT_CREATOR, PAYMENT_RECIPENT)), None); + + // test that the refund happened correctly + assert_eq!( + last_event(), + crate::Event::::PaymentCancelled { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT + } + .into() + ); + // the payment amount should be released back to creator + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + }); +} + +#[test] +fn test_automatic_refund_works_for_multiple_payments() { + new_test_ext().execute_with(|| { + const CANCEL_PERIOD: u64 = 600; + + assert_ok!(Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + None + )); + + assert_ok!(Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR_TWO), + PAYMENT_RECIPENT_TWO, + CURRENCY_ID, + 20, + None + )); + + assert_ok!(Payment::request_refund( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT + )); + run_n_blocks(1); + assert_ok!(Payment::request_refund( + RuntimeOrigin::signed(PAYMENT_CREATOR_TWO), + PAYMENT_RECIPENT_TWO + )); + + assert_eq!(run_n_blocks(CANCEL_PERIOD - 1), 601); + + // Odd block 601 was busy so we still haven't processed the first payment + assert_ok!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).ok_or(())); + + // Even block 602 has enough room to process both pending payments + assert_eq!(run_n_blocks(1), 602); + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR_TWO, PAYMENT_RECIPENT_TWO), + None + ); + + // the scheduled storage should be cleared + let scheduled_tasks_list = ScheduledTasks::::get(); + assert_eq!(scheduled_tasks_list.get(&(PAYMENT_CREATOR, PAYMENT_RECIPENT)), None); + assert_eq!( + scheduled_tasks_list.get(&(PAYMENT_CREATOR_TWO, PAYMENT_RECIPENT_TWO)), + None + ); + + // test that the refund happened correctly + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR_TWO), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_TWO), 0); + }); +} + +#[test] +fn on_idle_works() { + new_test_ext().execute_with(|| { + assert_eq!( + Payment::on_idle(System::block_number(), Weight::MAX), + <()>::remove_task() + ); + + let payment_amount = 20; + let expected_cancel_block = CANCEL_BLOCK_BUFFER + 1; + + assert_ok!(Payment::pay( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + )); + + // creator requests a refund + assert_ok!(Payment::request_refund( + RuntimeOrigin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT + )); + // ensure the request is added to the refund queue + let scheduled_tasks_list = ScheduledTasks::::get(); + assert_eq!(scheduled_tasks_list.len(), 1); + assert_eq!( + scheduled_tasks_list.get(&(PAYMENT_CREATOR, PAYMENT_RECIPENT)).unwrap(), + &ScheduledTask { + task: Task::Cancel, + when: expected_cancel_block + } + ); + + assert_eq!(run_n_blocks(CANCEL_BLOCK_BUFFER - 1), 600); + assert_eq!( + Payment::on_idle(System::block_number(), Weight::MAX), + <()>::remove_task() + ); + + assert_eq!(run_n_blocks(1), 601); + assert_eq!( + Payment::on_idle(System::block_number(), Weight::MAX), + <()>::remove_task() + <()>::cancel() + ); + }); +} diff --git a/blockchain/modules/setheum-pay/src/types.rs b/blockchain/modules/setheum-pay/src/types.rs new file mode 100644 index 00000000..9f719338 --- /dev/null +++ b/blockchain/modules/setheum-pay/src/types.rs @@ -0,0 +1,142 @@ +// بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيم + +// This file is part of Setheum. + +// Copyright (C) 2019-Present Setheum Labs. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// 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 . + +#![allow(unused_qualifications)] +use crate::{pallet, AssetIdOf, BalanceOf}; +use frame_system::pallet_prelude::*; +use parity_scale_codec::{Decode, Encode, HasCompact, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::{DispatchResult, Percent}; + +/// The PaymentDetail struct stores information about the payment/escrow +/// A "payment" in Setheum Pay is similar to an escrow, it is used to +/// guarantee proof of funds and can be released once an agreed upon condition +/// has reached between the payment creator and recipient. The payment lifecycle +/// is tracked using the state field. +#[derive(Encode, Decode, Debug, Clone, PartialEq, Eq, MaxEncodedLen, TypeInfo)] +#[scale_info(skip_type_params(T))] +#[codec(mel_bound(T: pallet::Config))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct PaymentDetail { + /// type of asset used for payment + pub asset: AssetIdOf, + /// amount of asset used for payment + #[codec(compact)] + pub amount: BalanceOf, + /// incentive amount that is credited to creator for resolving + #[codec(compact)] + pub incentive_amount: BalanceOf, + /// enum to track payment lifecycle [Created, NeedsReview, RefundRequested, + /// Requested] + pub state: PaymentState, + /// account that can settle any disputes created in the payment + pub resolver_account: T::AccountId, + /// fee charged and recipient account details + pub fee_detail: Option<(T::AccountId, BalanceOf)>, +} + +/// The `PaymentState` enum tracks the possible states that a payment can be in. +/// When a payment is 'completed' or 'cancelled' it is removed from storage and +/// hence not tracked by a state. +#[derive(Encode, Decode, Debug, Clone, PartialEq, Eq, MaxEncodedLen, TypeInfo)] +#[scale_info(skip_type_params(T))] +#[codec(mel_bound(T: pallet::Config))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum PaymentState { + /// Amounts have been reserved and waiting for release/cancel + Created, + /// A judge needs to review and release manually + NeedsReview, + /// The user has requested refund and will be processed by `BlockNumber` + RefundRequested { cancel_block: BlockNumberFor }, + /// The recipient of this transaction has created a request + PaymentRequested, +} + +/// trait that defines how to create/release payments for users +pub trait PaymentHandler { + /// Create a PaymentDetail from the given payment details + /// Calculate the fee amount and store PaymentDetail in storage + /// Possible reasons for failure include: + /// - Payment already exists and cannot be overwritten + fn create_payment( + from: &T::AccountId, + to: &T::AccountId, + asset: AssetIdOf, + amount: BalanceOf, + payment_state: PaymentState, + incentive_percentage: Percent, + remark: Option<&[u8]>, + ) -> Result, sp_runtime::DispatchError>; + + /// Attempt to reserve an amount of the given asset from the caller + /// If not possible then return Error. Possible reasons for failure include: + /// - User does not have enough balance. + fn reserve_payment_amount(from: &T::AccountId, to: &T::AccountId, payment: PaymentDetail) -> DispatchResult; + + // Settle a payment of `from` to `to`. To release a payment, the + // recipient_share=100, to cancel a payment recipient_share=0 + // Possible reasonse for failure include + /// + /// - The payment does not exist + /// - The unreserve operation fails + /// - The transfer operation fails + fn settle_payment(from: &T::AccountId, to: &T::AccountId, recipient_share: Percent) -> DispatchResult; + + /// Attempt to fetch the details of a payment from the given payment_id + /// Possible reasons for failure include: + /// - The payment does not exist + fn get_payment_details(from: &T::AccountId, to: &T::AccountId) -> Option>; +} + +/// DisputeResolver trait defines how to create/assign judges for solving +/// payment disputes +pub trait DisputeResolver { + /// Returns an `Account` + fn get_resolver_account() -> Account; +} + +/// Fee Handler trait that defines how to handle marketplace fees to every +/// payment/swap +pub trait FeeHandler { + /// Get the distribution of fees to marketplace participants + fn apply_fees( + from: &T::AccountId, + to: &T::AccountId, + detail: &PaymentDetail, + remark: Option<&[u8]>, + ) -> (T::AccountId, Percent); +} + +/// Types of Tasks that can be scheduled in the pallet +#[derive(PartialEq, Eq, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen)] +pub enum Task { + // payment `from` to `to` has to be cancelled + Cancel, +} + +/// The details of a scheduled task +#[derive(PartialEq, Eq, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen)] +pub struct ScheduledTask { + /// the type of scheduled task + pub task: Task, + /// the 'time' at which the task should be executed + pub when: Time, +} diff --git a/blockchain/modules/setheum-pay/src/weights.rs b/blockchain/modules/setheum-pay/src/weights.rs new file mode 100644 index 00000000..2cb181bb --- /dev/null +++ b/blockchain/modules/setheum-pay/src/weights.rs @@ -0,0 +1,205 @@ +// بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيم + +// This file is part of Setheum. + +// Copyright (C) 2019-Present Setheum Labs. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// 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 . + +//! Autogenerated weights for module_payment +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2022-03-19, STEPS: `20`, REPEAT: 10, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// target/release/setheum-node +// benchmark +// --chain +// dev +// --execution=wasm +// --wasm-execution +// compiled +// --extrinsic=* +// --pallet=module_payment +// --steps=20 +// --repeat=10 +// --heap-pages=4096 +// --output +// ./pallets/payment/src/weights.rs +// --template +// ./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(clippy::unnecessary_cast)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for module_payment. +pub trait WeightInfo { + fn pay(x: u32, ) -> Weight; + fn release() -> Weight; + fn cancel() -> Weight; + fn resolve_payment() -> Weight; + fn request_refund() -> Weight; + fn dispute_refund() -> Weight; + fn request_payment() -> Weight; + fn accept_and_pay() -> Weight; + fn remove_task() -> Weight; +} + +/// Weights for module_payment using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + // Storage: Payment Payment (r:1 w:1) + // Storage: Sudo Key (r:1 w:0) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:1) + fn pay(_x: u32, ) -> Weight { + Weight::from_parts(55_900_000, 0) + .saturating_add(T::DbWeight::get().reads(5 as u64)) + .saturating_add(T::DbWeight::get().writes(4 as u64)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + fn release() -> Weight { + Weight::from_parts(36_000_000, 0) + .saturating_add(T::DbWeight::get().reads(3 as u64)) + .saturating_add(T::DbWeight::get().writes(3 as u64)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:0) + fn cancel() -> Weight { + Weight::from_parts(48_000_000, 0) + .saturating_add(T::DbWeight::get().reads(4 as u64)) + .saturating_add(T::DbWeight::get().writes(3 as u64)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + fn resolve_payment() -> Weight { + Weight::from_parts(35_000_000, 0) + .saturating_add(T::DbWeight::get().reads(3 as u64)) + .saturating_add(T::DbWeight::get().writes(3 as u64)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Payment ScheduledTasks (r:1 w:1) + fn request_refund() -> Weight { + Weight::from_parts(20_000_000, 0) + .saturating_add(T::DbWeight::get().reads(2 as u64)) + .saturating_add(T::DbWeight::get().writes(2 as u64)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Payment ScheduledTasks (r:1 w:1) + fn dispute_refund() -> Weight { + Weight::from_parts(21_000_000, 0) + .saturating_add(T::DbWeight::get().reads(2 as u64)) + .saturating_add(T::DbWeight::get().writes(2 as u64)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Sudo Key (r:1 w:0) + fn request_payment() -> Weight { + Weight::from_parts(17_000_000, 0) + .saturating_add(T::DbWeight::get().reads(2 as u64)) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:1) + fn accept_and_pay() -> Weight { + Weight::from_parts(58_000_000, 0) + .saturating_add(T::DbWeight::get().reads(4 as u64)) + .saturating_add(T::DbWeight::get().writes(4 as u64)) + } + // Storage: Payment ScheduledTasks (r:1 w:1) + fn remove_task() -> Weight { + Weight::from_parts(4_000_000, 0) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + // Storage: Payment Payment (r:1 w:1) + // Storage: Sudo Key (r:1 w:0) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:1) + fn pay(_x: u32, ) -> Weight { + Weight::from_parts(55_900_000, 0) + .saturating_add(RocksDbWeight::get().reads(5 as u64)) + .saturating_add(RocksDbWeight::get().writes(4 as u64)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + fn release() -> Weight { + Weight::from_parts(36_000_000, 0) + .saturating_add(RocksDbWeight::get().reads(3 as u64)) + .saturating_add(RocksDbWeight::get().writes(3 as u64)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:0) + fn cancel() -> Weight { + Weight::from_parts(48_000_000, 0) + .saturating_add(RocksDbWeight::get().reads(4 as u64)) + .saturating_add(RocksDbWeight::get().writes(3 as u64)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + fn resolve_payment() -> Weight { + Weight::from_parts(35_000_000, 0) + .saturating_add(RocksDbWeight::get().reads(3 as u64)) + .saturating_add(RocksDbWeight::get().writes(3 as u64)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Payment ScheduledTasks (r:1 w:1) + fn request_refund() -> Weight { + Weight::from_parts(20_000_000, 0) + .saturating_add(RocksDbWeight::get().reads(2 as u64)) + .saturating_add(RocksDbWeight::get().writes(2 as u64)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Payment ScheduledTasks (r:1 w:1) + fn dispute_refund() -> Weight { + Weight::from_parts(21_000_000, 0) + .saturating_add(RocksDbWeight::get().reads(2 as u64)) + .saturating_add(RocksDbWeight::get().writes(2 as u64)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Sudo Key (r:1 w:0) + fn request_payment() -> Weight { + Weight::from_parts(17_000_000, 0) + .saturating_add(RocksDbWeight::get().reads(2 as u64)) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:1) + fn accept_and_pay() -> Weight { + Weight::from_parts(58_000_000, 0) + .saturating_add(RocksDbWeight::get().reads(4 as u64)) + .saturating_add(RocksDbWeight::get().writes(4 as u64)) + } + // Storage: Payment ScheduledTasks (r:1 w:1) + fn remove_task() -> Weight { + Weight::from_parts(4_000_000, 0) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) + } +} diff --git a/blockchain/modules/vesting/src/lib.rs b/blockchain/modules/vesting/src/lib.rs index 7adb224a..4a6429f3 100644 --- a/blockchain/modules/vesting/src/lib.rs +++ b/blockchain/modules/vesting/src/lib.rs @@ -67,7 +67,7 @@ use sp_std::{ use orml_traits::{ LockIdentifier, MultiCurrency, MultiLockableCurrency, }; -use primitives::CurrencyId; +use primitives::{ CurrencyId, VestingSchedule }; mod mock; mod tests; @@ -78,23 +78,6 @@ pub use weights::WeightInfo; pub const VESTING_LOCK_ID: LockIdentifier = *b"set/vest"; -/// The vesting schedule. -/// -/// Benefits would be granted gradually, `per_period` amount every `period` -/// of blocks after `start`. -#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] -pub struct VestingSchedule { - /// Vesting starting block - pub start: BlockNumber, - /// Number of blocks between vest - pub period: BlockNumber, - /// Number of vest - pub period_count: u32, - /// Amount of tokens to release per vest - #[codec(compact)] - pub per_period: Balance, -} - impl VestingSchedule { diff --git a/blockchain/modules/x-wallet/README.md b/blockchain/modules/x-wallet/README.md deleted file mode 100644 index 2233159c..00000000 --- a/blockchain/modules/x-wallet/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# X Wallet Module -Provides a Cross-chain Multichain Wallet on the Setheum Network. - -## Overview - -This module is used to abstract cross-chain multichain accounts by aggregating them on the Setheum network. Built on `Setheum Bridge`. diff --git a/blockchain/modules/x-wallet/TODO.md b/blockchain/modules/x-wallet/TODO.md deleted file mode 100644 index c8c94c13..00000000 --- a/blockchain/modules/x-wallet/TODO.md +++ /dev/null @@ -1,56 +0,0 @@ -# To-Do List - -This list contains all TODOs in the Repo - - - -- [ToDo List - The Monofile for Setheum Repo ToDos](#to-do-list) - - [1. Introduction](#1-guidelines) - - [2. Contribution](#2-contribution) - - [3. Lists](#3-lists) - - [4. Tasks](#4-tasks) - - - -## 1. Guidelines - -Note: Before you write a ToDo in this repo, please read the below guidelines carefully. - -Whenever you write a ToDo, you need to follow this standard syntax - -```rust -//TODO:[file_name:task_number] - task_details -``` - -for example: - -```rust -//TODO:[TODO.md:0] - Add Todo Guidelines -``` - -Note > the `//TODO:[filename:task_number] - ` is what we call the `task_prefix`. - -Whenever adding/writing a Task/ToDo, you need to describe the task on this list. Whenever you write a TODO in any file, add a reference to it here. Please make sure the task reference here is titled correctly and as detailed as possible\. - -Whenever you `complete` a task/TODO from any file, please tick/complete its reference here and make sure you do it in the same `commit` that completes the task. - -Whenever a task is cancelled (discontinued or not needed for w/e reason), please note in the details why it is cancelled, make sure you do it in the same `commit` that removes/cancels the TODO, and add this `-C` as a suffix to its `file_name` in the list here, for example: - -```rust -//TODO:[TODO.md-C:0] - Add Todo Guidelines -``` - -## 2. Contribution - -You can contribute to this list by completing tasks or by adding tasks(TODOs) that are currently in the repo but not on the list. You can also contribute by updating old tasks to the new Standard. - -## 3. Lists - -Each package/module/directory has its own `TODO.md`. - -## 4. Tasks - -These tasks are just for this file specifically. - -- [x] [[TODO.md:0] - Add TODO.md File](TODO.md): Add a TODO.md file to organise TODOs in the repo. -- [x] [[TODO.md:1] - Add a `task_title`](/TODO.md/#tasks): Adda `task_title`. diff --git a/interfaces/setheum-client/src/pallets/aleph.rs b/interfaces/setheum-client/src/pallets/aleph.rs index f4a2f52f..07f7ca7c 100644 --- a/interfaces/setheum-client/src/pallets/aleph.rs +++ b/interfaces/setheum-client/src/pallets/aleph.rs @@ -150,7 +150,7 @@ impl AlephRpc for C { hash: BlockHash, key_pair: AlephKeyPair, ) -> anyhow::Result<()> { - let method = "alephNode_emergencyFinalize"; + let method = "setheumNode_emergencyFinalize"; let signature = key_pair.sign(&hash.encode()); let raw_signature = Bytes::from(signature.0.to_vec()); let params = rpc_params![raw_signature, hash, number]; diff --git a/primitives/src/currency.rs b/primitives/src/currency.rs index 1dbfbbae..f117ebf3 100644 --- a/primitives/src/currency.rs +++ b/primitives/src/currency.rs @@ -179,7 +179,7 @@ create_currency_id! { #[derive(Encode, Decode, Eq, PartialEq, Copy, Clone, RuntimeDebug, PartialOrd, Ord, TypeInfo, MaxEncodedLen, Serialize, Deserialize)] #[repr(u8)] pub enum TokenSymbol { - // 0 - 128: Reserved for Setheum Native Assets + // 0 - 100: Reserved for Setheum Native Assets // Primary Protocol Tokens SEE("Setheum", 12) = 0, EDF("Ethical DeFi", 12) = 1, @@ -189,7 +189,110 @@ create_currency_id! { // ECDP Stablecoin Tokens SETR("Setter", 12) = 4, USSD("Slick USD", 12) = 5, - // 128 - 255: Reserved for future usage + + // 101-255: Reserved for Fiat Currencies + AED("UAE Dirham", 2) = 101, + AMD("Armenian Dram", 2) = 102, + AOA("Angolan Kwanza", 2) = 103, + ARS("Argentine Peso", 2) = 104, + AUD("Australian Dollar", 2) = 105, + AZN("Azerbaijani Manat", 2) = 106, + BHD("Bahraini Dinar", 3) = 107, + BIF("Burundian Franc", 2) = 108, + BND("Brunei Dollar", 2) = 109, + BRL("Brazilian Real", 2) = 110, + BSD("Bahamian Dollar", 2) = 111, + BWP("Botswana Pula", 2) = 112, + BYN("Belarusian Ruble", 2) = 113, + CAD("Canadian Dollar", 2) = 114, + CHF("Swiss Franc", 2) = 115, + CLP("Chilean Peso", 2) = 116, + CNY("Chinese Renminbi", 2) = 117, + COM("Comorian Franc", 2) = 118, + COP("Colombian Peso", 2) = 119, + CRC("Costa Rican Colón", 2) = 120, + CUP("Cuban Peso", 2) = 121, + CVE("Cape Verdean Escudo", 2) = 122, + CZK("Czech Koruna", 2) = 123, + DJF("Djiboutian Franc", 2) = 124, + DKK("Danish Krone", 2) = 125, + DOP("Dominican Peso", 2) = 126, + DZD("Algerian Dinar", 2) = 127, + EGP("Egyptian Pound", 2) = 128, + ERN("Eritrean Nakfa", 2) = 129, + ETB("Ethiopian Birr", 2) = 130, + EUR("Euro", 2) = 131, + GBP("British Pound", 2) = 132, + GEL("Georgian Lari", 2) = 133, + GHS("Ghanaian Cedi", 2) = 134, + GMD("Gambian Dalasi", 2) = 135, + GNF("Guinean Franc", 2) = 136, + HKD("Hong Kong Dollar", 2) = 137, + HUF("Hungarian Forint", 2) = 138, + IDR("Indonesian Rupiah", 2) = 139, + INR("Indian Rupee", 2) = 140, + ISK("Icelandic Krona", 2) = 141, + JOD("Jordanian Dinar", 2) = 142, + JPY("Japanese Yen", 2) = 143, + KES("Kenyan Shilling", 2) = 144, + KHR("Cambodian Riel", 2) = 145, + KMF("Comorian Franc", 2) = 146, + KRW("South Korean Won", 2) = 147, + KWD("Kuwaiti Dinar", 2) = 148, + KZT("Kazakhstani Tenge", 2) = 149, + LBP("Lebanese Pound", 2) = 150, + LKR("Sri Lankan Rupee", 2) = 151, + LSL("Lesotho Loti", 2) = 152, + LRD("Liberian Dollar", 2) = 153, + MAD("Moroccan Dirham", 2) = 154, + MDL("Moldovan Leu", 2) = 155, + MGA("Malagasy Ariary", 2) = 156, + MNT("Mongolian Tugrik", 2) = 157, + MRU("Mauritanian Ouguiya", 2) = 158, + MUR("Mauritian Rupee", 2) = 159, + MWK("Malawian Kwacha", 2) = 160, + MXN("Mexican Peso", 2) = 161, + MYR("Malaysian Ringgit", 2) = 162, + MZN("Mozambican Metical", 2) = 163, + NAD("Namibian Dollar", 2) = 164, + NGN("Nigerian Naira", 2) = 165, + NOK("Norwegian Krone", 2) = 166, + NPR("Nepalese Rupee", 2) = 167, + NZD("New Zealand Dollar", 2) = 168, + OMR("Omani Rial", 2) = 169, + PEN("Peruvian Sol", 2) = 170, + PHP("Philippine Peso", 2) = 171, + PKR("Pakistani Rupee", 2) = 172, + QAR("Qatari Riyal", 2) = 173, + RON("Romanian Leu", 2) = 174, + RSD("Serbian Dinar", 2) = 175, + RUB("Russian Ruble", 2) = 176, + RWF("Rwandan Franc", 2) = 177, + SAR("Saudi Riyal", 2) = 178, + SCR("Seychellois Rupee", 2) = 179, + SEK("Swedish Krona", 2) = 180, + SGD("Singapore Dollar", 2) = 181, + SHP("Saint Helena Pound", 2) = 182, + SLE("Sierra Leonean Leone", 2) = 183, + SZL("Swazi Lilangeni", 2) = 184, + THB("Thai Baht", 2) = 185, + TJS("Tajikistani Somoni", 2) = 186, + TND("Tunisian Dinar", 2) = 187, + TTD("Trinidadian Dollar", 2) = 188, + TWD("New Taiwan Dollar", 2) = 189, + TZS("Tanzanian Shilling", 2) = 190, + TRY("Turkish Lira", 2) = 191, + UAH("Ukrainian Hryvnia", 2) = 192, + UGX("Ugandan Shilling", 2) = 193, + USD("United States Dollar", 2) = 194, + UZS("Uzbekistani Som", 2) = 195, + VES("Venezuelan Bolivar", 2) = 196, + VND("Vietnamese Dong", 2) = 197, + XAF("Central African CFA Franc", 2) = 198, + XOF("West African CFA Franc", 2) = 199, + ZAR("South African Rand", 2) = 200, + ZMW("Zambian Kwacha", 2) = 201, + ZWL("Zimbabwean Dollar", 2) = 202, } } @@ -200,6 +303,7 @@ pub trait TokenInfo { fn decimals(&self) -> Option; } +pub type FiatCurrencyId = u8; pub type ForeignAssetId = u16; pub type Erc20Id = u32; @@ -246,6 +350,7 @@ pub enum CurrencyId { DexShare(DexShare, DexShare), Erc20(EvmAddress), ForeignAsset(ForeignAssetId), + FiatCurrency(FiatCurrencyId), } impl CurrencyId { @@ -265,6 +370,10 @@ impl CurrencyId { matches!(self, CurrencyId::ForeignAsset(_)) } + pub fn is_fiat_asset_currency_id(&self) -> bool { + matches!(self, CurrencyId::FiatCurrency(_)) + } + pub fn is_trading_pair_currency_id(&self) -> bool { matches!( self, @@ -354,6 +463,7 @@ pub enum CurrencyIdType { Token = 1, // 0 is prefix of precompile and predeploy DexShare, ForeignAsset, + FiatCurrency, } #[derive( @@ -381,6 +491,7 @@ pub enum AssetIds { Erc20(EvmAddress), ForeignAssetId(ForeignAssetId), NativeAssetId(CurrencyId), + FiatCurrencyId,(FiatCurrencyId), } #[derive(Clone, Eq, PartialEq, RuntimeDebug, Encode, Decode, TypeInfo)] diff --git a/primitives/src/vesting.rs b/primitives/src/vesting.rs new file mode 100644 index 00000000..3f08dc23 --- /dev/null +++ b/primitives/src/vesting.rs @@ -0,0 +1,40 @@ +// بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيم + +// This file is part of Setheum. + +// Copyright (C) 2019-Present Setheum Labs. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// 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 . + +use parity_scale_codec::{Decode, Encode}; +use sp_runtime::RuntimeDebug; +use parity_scale_codec::{HasCompact, MaxEncodedLen}; + +/// The vesting schedule. +/// +/// Benefits would be granted gradually, `per_period` amount every `period` +/// of blocks after `start`. +#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct VestingSchedule { + /// Vesting starting block + pub start: BlockNumber, + /// Number of blocks between vest + pub period: BlockNumber, + /// Number of vest + pub period_count: u32, + /// Amount of tokens to release per vest + #[codec(compact)] + pub per_period: Balance, +}