From 58b4c6aa359691e478aa80c5e3afecfd32e5a616 Mon Sep 17 00:00:00 2001 From: brenzi Date: Mon, 15 Jul 2024 16:55:34 +0200 Subject: [PATCH] pallet teerdays (#263) * first dispatchable for teerdays * basic bonding and unbonding without unlockingperiod. type trouble * fix type trouble * fix unbonding * cosmetics * implement unlock period * minimal benchmarks * add bond_extra call * refactor * add unhappy tests * pallet code docs * cleanup * cleanup * doc pimp * taplo * taplo * zepter+fmt * fmt nightly * fmt * clippy * review fixes * clippy again * clippy version mismatch? * clippy version mismatch? --------- Co-authored-by: Christian Langenbacher --- .github/workflows/ci.yml | 5 +- Cargo.lock | 34 +++ Cargo.toml | 18 +- asset-registry/src/lib.rs | 3 +- claims/src/lib.rs | 8 +- enclave-bridge/src/lib.rs | 2 +- primitives/teerdays/Cargo.toml | 25 ++ primitives/teerdays/src/lib.rs | 24 ++ primitives/teerex/src/lib.rs | 2 +- primitives/xcm/src/lib.rs | 3 +- rust-toolchain.toml | 2 +- teerdays/Cargo.toml | 72 ++++++ teerdays/src/benchmarking.rs | 79 ++++++ teerdays/src/lib.rs | 339 +++++++++++++++++++++++++ teerdays/src/mock.rs | 150 +++++++++++ teerdays/src/tests.rs | 290 +++++++++++++++++++++ teerdays/src/weights.rs | 63 +++++ teerex/sgx-verify/src/ephemeral_key.rs | 1 + 18 files changed, 1102 insertions(+), 18 deletions(-) create mode 100644 primitives/teerdays/Cargo.toml create mode 100644 primitives/teerdays/src/lib.rs create mode 100644 teerdays/Cargo.toml create mode 100644 teerdays/src/benchmarking.rs create mode 100644 teerdays/src/lib.rs create mode 100644 teerdays/src/mock.rs create mode 100644 teerdays/src/tests.rs create mode 100644 teerdays/src/weights.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5bd5ebc..3f1c00b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,12 +38,15 @@ jobs: cargo test --all --features runtime-benchmarks, cargo test --all --features dot, cargo test --all --features ksm, - cargo fmt --all -- --check, + cargo +nightly fmt --all -- --check, cargo clippy -- -D warnings ] steps: - uses: actions/checkout@v3 + - name: Install nightly toolchain + run: rustup toolchain install nightly --profile minimal --component rustfmt + # With rustup's nice new toml format, we just need to run rustup show to install the toolchain # https://github.com/actions-rs/toolchain/issues/126#issuecomment-782989659 - name: Setup Rust toolchain diff --git a/Cargo.lock b/Cargo.lock index 6741b579..6e44a83b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2671,6 +2671,29 @@ dependencies = [ "test-utils", ] +[[package]] +name = "pallet-teerdays" +version = "0.1.0" +dependencies = [ + "env_logger", + "frame-benchmarking", + "frame-support", + "frame-system", + "hex-literal", + "log", + "pallet-balances", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-keyring", + "sp-runtime", + "sp-std", + "teerdays-primitives", +] + [[package]] name = "pallet-teerex" version = "0.10.0" @@ -4729,6 +4752,17 @@ dependencies = [ "substrate-fixed", ] +[[package]] +name = "teerdays-primitives" +version = "0.1.0" +dependencies = [ + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-runtime", +] + [[package]] name = "teerex-primitives" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index a104be06..2d81cb46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,20 +4,22 @@ members = [ "asset-registry", "claims", "enclave-bridge", - "teerex", "parentchain", - "sidechain", - "teerex/sgx-verify", - "teeracle", - "test-utils", - "xcm-transactor", "primitives/claims", + "primitives/common", "primitives/enclave-bridge", - "primitives/teerex", "primitives/teeracle", - "primitives/common", + "primitives/teerdays", + "primitives/teerex", "primitives/xcm", "primitives/xcm-transactor", + "sidechain", + "teeracle", + "teerdays", + "teerex", + "teerex/sgx-verify", + "test-utils", + "xcm-transactor", ] [workspace.dependencies] diff --git a/asset-registry/src/lib.rs b/asset-registry/src/lib.rs index e5d64356..e5ca8961 100644 --- a/asset-registry/src/lib.rs +++ b/asset-registry/src/lib.rs @@ -155,7 +155,8 @@ pub mod pallet { Some(AccountId32 { .. }) | Some(AccountKey20 { .. }) | Some(PalletInstance(_)) | - Some(Parachain(_)) | None + Some(Parachain(_)) | + None ); check | diff --git a/claims/src/lib.rs b/claims/src/lib.rs index df11e96d..b29c96f7 100644 --- a/claims/src/lib.rs +++ b/claims/src/lib.rs @@ -423,7 +423,7 @@ pub mod pallet { priority: PRIORITY, requires: vec![], provides: vec![("claims", signer).encode()], - longevity: TransactionLongevity::max_value(), + longevity: TransactionLongevity::MAX, propagate: true, }) } @@ -480,7 +480,7 @@ impl Pallet { } // We first need to deposit the balance to ensure that the account exists. - CurrencyOf::::deposit_creating(&dest, balance_due); + let _ = CurrencyOf::::deposit_creating(&dest, balance_due); // Check if this claim should have a vesting schedule. if let Some(vs) = vesting { @@ -1317,7 +1317,7 @@ mod tests { priority: 100, requires: vec![], provides: vec![("claims", eth(&alice())).encode()], - longevity: TransactionLongevity::max_value(), + longevity: TransactionLongevity::MAX, propagate: true, }) ); @@ -1350,7 +1350,7 @@ mod tests { priority: 100, requires: vec![], provides: vec![("claims", eth(&dave())).encode()], - longevity: TransactionLongevity::max_value(), + longevity: TransactionLongevity::MAX, propagate: true, }) ); diff --git a/enclave-bridge/src/lib.rs b/enclave-bridge/src/lib.rs index 7d64947f..b224eb60 100644 --- a/enclave-bridge/src/lib.rs +++ b/enclave-bridge/src/lib.rs @@ -386,8 +386,8 @@ pub mod pallet { let new_status: ShardSignerStatusVec = Self::shard_status(shard) .ok_or(Error::::ShardNotFound)? .iter() + .filter(|&signer_status| signer_status.signer != subject) .cloned() - .filter(|signer_status| signer_status.signer != subject) .collect::>>() .try_into() .expect("can only become smaller by filtering"); diff --git a/primitives/teerdays/Cargo.toml b/primitives/teerdays/Cargo.toml new file mode 100644 index 00000000..92de8339 --- /dev/null +++ b/primitives/teerdays/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "teerdays-primitives" +version = "0.1.0" +authors = ["Integritee AG "] +homepage = "https://integritee.network/" +repository = "https://github.com/integritee-network/pallets/" +license = "Apache-2.0" +edition = "2021" + +[dependencies] +parity-scale-codec = { workspace = true } +scale-info = { workspace = true } +serde = { workspace = true } +sp-core = { workspace = true } +sp-runtime = { workspace = true } + +[features] +default = ["std"] +std = [ + "parity-scale-codec/std", + "scale-info/std", + "serde/std", + "sp-core/std", + "sp-runtime/std", +] diff --git a/primitives/teerdays/src/lib.rs b/primitives/teerdays/src/lib.rs new file mode 100644 index 00000000..2e5e6c2b --- /dev/null +++ b/primitives/teerdays/src/lib.rs @@ -0,0 +1,24 @@ +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_runtime::{traits::AtLeast32BitUnsigned, Saturating}; + +#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, Default, sp_core::RuntimeDebug, TypeInfo)] +pub struct TeerDayBond { + pub value: Balance, + pub last_updated: Moment, + // the unit here is actually balance * moments. + pub accumulated_tokentime: Balance, +} + +impl TeerDayBond +where + Moment: Clone + Copy + Encode + Decode + Saturating, + Balance: AtLeast32BitUnsigned + Clone + Copy + Encode + Decode + Default + From, +{ + pub fn update(self, now: Moment) -> Self { + let elapsed: Balance = now.saturating_sub(self.last_updated).into(); + let new_tokentime = + self.accumulated_tokentime.saturating_add(self.value.saturating_mul(elapsed)); + Self { value: self.value, last_updated: now, accumulated_tokentime: new_tokentime } + } +} diff --git a/primitives/teerex/src/lib.rs b/primitives/teerex/src/lib.rs index 228f335b..79520b23 100644 --- a/primitives/teerex/src/lib.rs +++ b/primitives/teerex/src/lib.rs @@ -159,7 +159,7 @@ where MultiEnclave::Sgx(enclave) => match enclave.maybe_pubkey() { Some(pubkey) => AnySigner::from(MultiSigner::from(sp_core::ed25519::Public::from_raw(pubkey))), - None => AnySigner::try_from(enclave.report_data.d).unwrap_or_default(), + None => AnySigner::from(enclave.report_data.d), }, } } diff --git a/primitives/xcm/src/lib.rs b/primitives/xcm/src/lib.rs index e832a5e1..9cfb85fc 100644 --- a/primitives/xcm/src/lib.rs +++ b/primitives/xcm/src/lib.rs @@ -115,7 +115,8 @@ impl where + > +where AssetIdInfoGetter: AssetLocationGetter, AssetsPallet: Inspect, BalancesPallet: Currency, diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 317bf8c4..f7868d17 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "nightly-2023-06-01" +channel = "stable" targets = ["wasm32-unknown-unknown"] profile = "default" # include rustfmt, clippy diff --git a/teerdays/Cargo.toml b/teerdays/Cargo.toml new file mode 100644 index 00000000..015d4101 --- /dev/null +++ b/teerdays/Cargo.toml @@ -0,0 +1,72 @@ +[package] +name = "pallet-teerdays" +version = "0.1.0" +authors = ["Integritee AG "] +homepage = "https://integritee.network/" +repository = "https://github.com/integritee-network/pallets/" +license = "(GPL-3.0-only)" +edition = "2021" + +[dependencies] +log = { workspace = true } +parity-scale-codec = { workspace = true } +scale-info = { workspace = true } +serde = { workspace = true, optional = true } + +teerdays-primitives = { path = "../primitives/teerdays", default-features = false } + +# substrate dependencies +frame-support = { workspace = true } +frame-system = { workspace = true } +pallet-balances = { workspace = true } +pallet-timestamp = { workspace = true } +sp-core = { workspace = true } +sp-io = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +# benchmarking +frame-benchmarking = { workspace = true, optional = true } +hex-literal = { workspace = true, optional = true } + +[dev-dependencies] +env_logger = { workspace = true } +sp-keyring = { workspace = true } + +[features] +default = ["std"] +std = [ + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-balances/std", + "pallet-timestamp/std", + "parity-scale-codec/std", + "scale-info/std", + "serde/std", + "sp-core/std", + "sp-io/std", + "sp-keyring/std", + "sp-runtime/std", + "sp-std/std", + "teerdays-primitives/std", +] + +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-balances/try-runtime", + "pallet-timestamp/try-runtime", + "sp-runtime/try-runtime", +] + +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "hex-literal", + "pallet-balances/runtime-benchmarks", + "pallet-timestamp/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] diff --git a/teerdays/src/benchmarking.rs b/teerdays/src/benchmarking.rs new file mode 100644 index 00000000..a115c402 --- /dev/null +++ b/teerdays/src/benchmarking.rs @@ -0,0 +1,79 @@ +/* + Copyright 2021 Integritee AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +//! TeerDays pallet benchmarking + +#![cfg(any(test, feature = "runtime-benchmarks"))] + +use super::*; + +use crate::Pallet as TeerDays; +use frame_benchmarking::{account, benchmarks}; +use frame_system::RawOrigin; +use sp_std::prelude::*; + +benchmarks! { + where_clause { where T::AccountId: From<[u8; 32]>, T::Hash: From<[u8; 32]> } + bond { + pallet_timestamp::Pallet::::set_timestamp(0u32.into()); + let signer: T::AccountId = account("alice", 1, 1); + T::Currency::make_free_balance_be(&signer, 10_000u32.into()); + }: _(RawOrigin::Signed(signer.clone()), 1_000u32.into()) + verify { + assert!(TeerDays::::teerday_bonds(&signer).is_some()); + } + + unbond { + pallet_timestamp::Pallet::::set_timestamp(0u32.into()); + let signer: T::AccountId = account("alice", 1, 1); + T::Currency::make_free_balance_be(&signer, 10_000u32.into()); + TeerDays::::bond(RawOrigin::Signed(signer.clone()).into(), 1_000u32.into())?; + }: _(RawOrigin::Signed(signer.clone()), 500u32.into()) + verify { + assert!(TeerDays::::teerday_bonds(&signer).is_some()); + } + + update_other { + pallet_timestamp::Pallet::::set_timestamp(0u32.into()); + let signer: T::AccountId = account("alice", 1, 1); + T::Currency::make_free_balance_be(&signer, 10_000u32.into()); + TeerDays::::bond(RawOrigin::Signed(signer.clone()).into(), 1_000u32.into())?; + }: _(RawOrigin::Signed(signer.clone()), signer.clone()) + verify { + assert!(TeerDays::::teerday_bonds(&signer).is_some()); + } + withdraw_unbonded { + pallet_timestamp::Pallet::::set_timestamp(42u32.into()); + let signer: T::AccountId = account("alice", 1, 1); + T::Currency::make_free_balance_be(&signer, 10_000u32.into()); + T::Currency::set_lock(TEERDAYS_ID, &signer, 1_000u32.into(), WithdrawReasons::all()); + PendingUnlock::::insert::<_, (T::Moment, BalanceOf)>(&signer, (42u32.into(), 1_000u32.into())); + }: _(RawOrigin::Signed(signer.clone())) + verify { + assert!(TeerDays::::pending_unlock(&signer).is_none()); + } + +} + +#[cfg(test)] +use crate::{Config, Pallet as PalletModule}; + +#[cfg(test)] +use frame_benchmarking::impl_benchmark_test_suite; + +#[cfg(test)] +impl_benchmark_test_suite!(PalletModule, crate::mock::new_test_ext(), crate::mock::Test,); diff --git a/teerdays/src/lib.rs b/teerdays/src/lib.rs new file mode 100644 index 00000000..5f8a8281 --- /dev/null +++ b/teerdays/src/lib.rs @@ -0,0 +1,339 @@ +/* + Copyright 2021 Integritee AG + + Licenced under GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. You may obtain a copy of the + License at + + . + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +//! # Teerdays Pallet +//! A pallet which allows bonding native TEER tokens and accumulate TEERdays the longer the tokens are bonded +//! TEERdays will serve as a basis for governance and other features in the future +//! +//! ### Terminology +//! - **Bonding**: Locking up TEER tokens for a certain period of time into the future. +//! Bonded TEER tokens are not liquid and appear in "frozen" balance but can still be used for voting in network governance +//! - **Unbonding**: Starting the unlock process of bonded TEER tokens +//! - **TEERdays**: Accumulated time of bonded TEER tokens +//! - **TokenTime**: The technical unit of TEERdays storage: TokenTime = Balance (TEER with its 12 digits) * Moment (Milliseconds) +//! +//! ### Usage Lifecycle +//! +//! 1. Bond TEER tokens: `bond(value)` +//! 2. Increase Bond if you like: `bond_extra(value)` +//! 3. *Use your accumulated TEERdays for governance or other features* +//! 4. Unbond TEER tokens: `unbond(value)`. +//! - unbonding is only possible if no unlock is pending +//! - unbonding burns accumulated TEERdays pro rata bonded amount before and after unbonding +//! 5. wait for `UnlockPeriod` to pass +//! 6. Withdraw unbonded TEER tokens: `withdraw_unbonded()` +//! +//! ### Developer Notes +//! +//! Accumulated TokenTime is updated lazily. This means that the `update_other` function must be called if the +//! total amount of accumulated TEERdays is relevant for i.e. determining the electorate in +//! TEERday-based voting. If necessary, this update can be enforced by other pallets using `do_update_teerdays(account)` +//! Failing to update all bonded accounts may lead to underestimation of total electorate voting power +//! +//! #### Numerical stability +//! Assuming: +//! - Balance is u128, decimals: 12, total supply cap: 10^7 TEER +//! - Moment is u64, unit: ms +//! +//! 100years in milliseconds (Moment) are 42bits +//! 10MTEER with 12 digits are 60bits +//! 100years of total supply locked still fits u128 +//! therefore, it is safe to use the Balance type for TEERdays +//! + +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::traits::{ + Currency, InspectLockableCurrency, LockIdentifier, LockableCurrency, WithdrawReasons, +}; +pub use pallet::*; +use sp_runtime::Saturating; +use teerdays_primitives::TeerDayBond; + +pub(crate) const TEERDAYS_ID: LockIdentifier = *b"teerdays"; +pub type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + +pub type TeerDayBondOf = TeerDayBond, ::Moment>; +#[frame_support::pallet] +pub mod pallet { + use crate::{weights::WeightInfo, BalanceOf, TeerDayBondOf, TEERDAYS_ID}; + use frame_support::{ + pallet_prelude::*, + traits::{Currency, InspectLockableCurrency, LockableCurrency, WithdrawReasons}, + }; + use frame_system::pallet_prelude::*; + use sp_runtime::{ + traits::{CheckedDiv, Zero}, + Saturating, + }; + + const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + #[pallet::without_storage_info] + pub struct Pallet(PhantomData); + + /// Configuration trait. + #[pallet::config] + pub trait Config: frame_system::Config + pallet_timestamp::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + type WeightInfo: WeightInfo; + + /// The bonding balance. + type Currency: LockableCurrency< + Self::AccountId, + Moment = BlockNumberFor, + Balance = Self::CurrencyBalance, + > + InspectLockableCurrency; + + /// Just the `Currency::Balance` type; we have this item to allow us to constrain it to + /// `CheckedDiv` and `From`. + type CurrencyBalance: sp_runtime::traits::AtLeast32BitUnsigned + + parity_scale_codec::FullCodec + + Copy + + MaybeSerializeDeserialize + + sp_std::fmt::Debug + + Default + + CheckedDiv + + From + + TypeInfo + + MaxEncodedLen; + + /// The period of time that must pass before a bond can be unbonded. + /// Must use the same unit which the timestamp pallet uses + #[pallet::constant] + type UnlockPeriod: Get; + } + + #[pallet::event] + #[pallet::generate_deposit(pub (super) fn deposit_event)] + pub enum Event { + /// An account's bond has been increased by an amount + Bonded { account: T::AccountId, amount: BalanceOf }, + /// An account's bond has been decreased by an amount + Unbonded { account: T::AccountId, amount: BalanceOf }, + /// An account's accumulated tokentime has been updated + TokenTimeUpdated { account: T::AccountId, bond: TeerDayBondOf }, + /// An account has successfully withdrawn a previously unbonded amount after unlock period has passed + Withdrawn { account: T::AccountId, amount: BalanceOf }, + } + + #[pallet::error] + pub enum Error { + /// Each account can only bond once + AlreadyBonded, + /// Insufficient bond + InsufficientBond, + /// Insufficient unbond + InsufficientUnbond, + /// account has no bond + NoBond, + /// Can't unbond while unlock is pending + PendingUnlock, + /// no unlock is pending + NotUnlocking, + /// Some corruption in internal state. + BadState, + } + + /// a store for all active bonds. tokentime is updated lazily + #[pallet::storage] + #[pallet::getter(fn teerday_bonds)] + pub type TeerDayBonds = + StorageMap<_, Blake2_128Concat, T::AccountId, TeerDayBondOf, OptionQuery>; + + /// a store for all pending unlocks which are awaiting the unlock period to pass. + /// Withdrawal happens lazily and causes entry removal from this store + #[pallet::storage] + #[pallet::getter(fn pending_unlock)] + pub type PendingUnlock = + StorageMap<_, Blake2_128Concat, T::AccountId, (T::Moment, BalanceOf), OptionQuery>; + + #[pallet::call] + impl Pallet { + /// Bond TEER tokens. This will lock the tokens in order to start accumulating TEERdays + /// The minimum bond is the existential deposit + #[pallet::call_index(0)] + #[pallet::weight(< T as Config >::WeightInfo::bond())] + pub fn bond( + origin: OriginFor, + #[pallet::compact] value: BalanceOf, + ) -> DispatchResult { + let signer = ensure_signed(origin)?; + ensure!(!TeerDayBonds::::contains_key(&signer), Error::::AlreadyBonded); + ensure!(value >= T::Currency::minimum_balance(), Error::::InsufficientBond); + + let free_balance = T::Currency::free_balance(&signer); + let value = value.min(free_balance); + Self::deposit_event(Event::::Bonded { account: signer.clone(), amount: value }); + T::Currency::set_lock(TEERDAYS_ID, &signer, value, WithdrawReasons::all()); + let teerday_bond = TeerDayBondOf:: { + value, + last_updated: pallet_timestamp::Pallet::::get(), + accumulated_tokentime: BalanceOf::::zero(), + }; + TeerDayBonds::::insert(&signer, teerday_bond); + Ok(()) + } + + /// Increase an existing bond on the signer's account + /// The minimum additional bond specified by `value` must exceed the existential deposit + #[pallet::call_index(1)] + #[pallet::weight(< T as Config >::WeightInfo::bond())] + pub fn bond_extra( + origin: OriginFor, + #[pallet::compact] value: BalanceOf, + ) -> DispatchResult { + let signer = ensure_signed(origin)?; + ensure!(value >= T::Currency::minimum_balance(), Error::::InsufficientBond); + let bond = Self::do_update_teerdays(&signer)?; + let free_balance = T::Currency::free_balance(&signer); + // free confusingly includes the already bonded amount, so we need to subtract it + let value = value.min(free_balance.saturating_sub(bond.value)); + let new_bond_value = bond.value.saturating_add(value); + Self::deposit_event(Event::::Bonded { account: signer.clone(), amount: value }); + T::Currency::set_lock(TEERDAYS_ID, &signer, new_bond_value, WithdrawReasons::all()); + let teerday_bond = TeerDayBondOf:: { + value: new_bond_value, + last_updated: bond.last_updated, + accumulated_tokentime: bond.accumulated_tokentime, + }; + TeerDayBonds::::insert(&signer, teerday_bond); + Ok(()) + } + + /// Decrease an existing bond on the signer's account + /// The minimum unbond specified by `value` must exceed the existential deposit + /// If `value` is equal or greater than the current bond, the bond will be removed + /// The unbonded amount will still be subject to an unbonding period before the amount can be withdrawn + /// Unbonding will burn accumulated TEERdays pro rata. + #[pallet::call_index(2)] + #[pallet::weight(< T as Config >::WeightInfo::unbond())] + pub fn unbond( + origin: OriginFor, + #[pallet::compact] value: BalanceOf, + ) -> DispatchResult { + let signer = ensure_signed(origin)?; + ensure!(Self::pending_unlock(&signer).is_none(), Error::::PendingUnlock); + ensure!(value >= T::Currency::minimum_balance(), Error::::InsufficientUnbond); + let bond = Self::do_update_teerdays(&signer)?; + let now = bond.last_updated; + + let new_bonded_amount = bond.value.saturating_sub(value); + let unbonded_amount = bond.value.saturating_sub(new_bonded_amount); + + // burn tokentime pro rata + let new_tokentime = bond + .accumulated_tokentime + .checked_div(&bond.value) + .unwrap_or_default() + .saturating_mul(new_bonded_amount); + + let new_bond = TeerDayBondOf:: { + value: new_bonded_amount, + last_updated: now, + accumulated_tokentime: new_tokentime, + }; + + if new_bond.value < T::Currency::minimum_balance() { + TeerDayBonds::::remove(&signer); + } else { + TeerDayBonds::::insert(&signer, new_bond); + } + PendingUnlock::::insert(&signer, (now + T::UnlockPeriod::get(), unbonded_amount)); + + Self::deposit_event(Event::::Unbonded { + account: signer.clone(), + amount: unbonded_amount, + }); + Ok(()) + } + + /// Update the accumulated tokentime for an account lazily + /// This can be helpful if other pallets use TEERdays and need to ensure the total + /// accumulated tokentime is up to date. + #[pallet::call_index(3)] + #[pallet::weight(< T as Config >::WeightInfo::update_other())] + pub fn update_other(origin: OriginFor, who: T::AccountId) -> DispatchResult { + let _signer = ensure_signed(origin)?; + let _bond = Self::do_update_teerdays(&who)?; + Ok(()) + } + + /// Withdraw an unbonded amount after the unbonding period has expired + #[pallet::call_index(4)] + #[pallet::weight(< T as Config >::WeightInfo::withdraw_unbonded())] + pub fn withdraw_unbonded(origin: OriginFor) -> DispatchResult { + let signer = ensure_signed(origin)?; + let unlocked = Self::try_withdraw_unbonded(&signer)?; + Self::deposit_event(Event::::Withdrawn { + account: signer.clone(), + amount: unlocked, + }); + Ok(()) + } + } +} + +impl Pallet { + /// accumulates pending tokentime and updates state + /// bond must exist or will err. + /// returns the updated bond and deposits an event `BondUpdated` + fn do_update_teerdays( + account: &T::AccountId, + ) -> Result, sp_runtime::DispatchError> { + let bond = Self::teerday_bonds(account).ok_or(Error::::NoBond)?; + let now = pallet_timestamp::Pallet::::get(); + let bond = bond.update(now); + TeerDayBonds::::insert(account, bond); + Self::deposit_event(Event::::TokenTimeUpdated { account: account.clone(), bond }); + Ok(bond) + } + + fn try_withdraw_unbonded( + account: &T::AccountId, + ) -> Result, sp_runtime::DispatchError> { + let (due, amount) = Self::pending_unlock(account).ok_or(Error::::NotUnlocking)?; + let now = pallet_timestamp::Pallet::::get(); + if now < due { + return Err(Error::::PendingUnlock.into()) + } + let locked = T::Currency::balance_locked(TEERDAYS_ID, account); + let amount = amount.min(locked); + if amount == locked { + T::Currency::remove_lock(TEERDAYS_ID, account); + } else { + T::Currency::set_lock( + TEERDAYS_ID, + account, + locked.saturating_sub(amount), + WithdrawReasons::all(), + ); + } + PendingUnlock::::remove(account); + Ok(amount) + } +} + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; +pub mod weights; diff --git a/teerdays/src/mock.rs b/teerdays/src/mock.rs new file mode 100644 index 00000000..a96e779a --- /dev/null +++ b/teerdays/src/mock.rs @@ -0,0 +1,150 @@ +/* + Copyright 2021 Integritee AG + + Licenced under GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. You may obtain a copy of the + License at + + . + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Creating mock runtime here +use crate as pallet_teerdays; +use frame_support::{derive_impl, parameter_types}; +use frame_system as system; +use pallet_teerdays::Config; +use sp_core::H256; +use sp_keyring::AccountKeyring; +use sp_runtime::{ + generic, + traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, Verify}, + BuildStorage, +}; + +pub type Signature = sp_runtime::MultiSignature; +pub type AccountId = <::Signer as IdentifyAccount>::AccountId; +pub type Address = sp_runtime::MultiAddress; + +pub type BlockNumber = u32; +pub type Header = generic::Header; +pub type UncheckedExtrinsic = + generic::UncheckedExtrinsic; + +pub type SignedExtra = ( + frame_system::CheckSpecVersion, + frame_system::CheckTxVersion, + frame_system::CheckGenesis, + frame_system::CheckEra, + frame_system::CheckNonce, + frame_system::CheckWeight, +); + +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent}, + TeerDays: crate::{Pallet, Call, Storage, Event}, + } +); + +parameter_types! { + pub const BlockHashCount: u32 = 250; +} +#[derive_impl(frame_system::config_preludes::SolochainDefaultConfig as frame_system::DefaultConfig)] +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type Block = generic::Block; + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Nonce = u64; + type RuntimeCall = RuntimeCall; + type RuntimeTask = RuntimeTask; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +pub type Balance = u128; + +parameter_types! { + pub const ExistentialDeposit: u128 = 1; +} + +impl pallet_balances::Config for Test { + type MaxLocks = (); + type Balance = Balance; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxReserves = (); + type ReserveIdentifier = (); + type RuntimeFreezeReason = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type RuntimeHoldReason = (); +} + +parameter_types! { + pub const MinimumPeriod: u64 = 6000 / 2; +} + +pub type Moment = u64; + +impl pallet_timestamp::Config for Test { + type Moment = Moment; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +parameter_types! { + pub const UnlockPeriod: u64 = 86_400_000; // [ms] +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type UnlockPeriod = UnlockPeriod; + type WeightInfo = (); + type Currency = Balances; + type CurrencyBalance = Balance; +} + +// This function basically just builds a genesis storage key/value store according to +// our desired mockup. RA from enclave compiled in debug mode is allowed +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = system::GenesisConfig::::default().build_storage().unwrap(); + pallet_balances::GenesisConfig:: { + balances: vec![(AccountKeyring::Alice.to_account_id(), 1 << 60)], + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext: sp_io::TestExternalities = t.into(); + ext.execute_with(|| System::set_block_number(1)); + ext +} diff --git a/teerdays/src/tests.rs b/teerdays/src/tests.rs new file mode 100644 index 00000000..1f8dfa7f --- /dev/null +++ b/teerdays/src/tests.rs @@ -0,0 +1,290 @@ +use crate::{mock::*, pallet, BalanceOf, Error, Event as TeerDaysEvent}; +use frame_support::{ + assert_noop, assert_ok, + traits::{Currency, OnFinalize, OnInitialize}, +}; +use sp_keyring::AccountKeyring; + +pub fn run_to_block(n: u32) { + while System::block_number() < n { + if System::block_number() > 1 { + System::on_finalize(System::block_number()); + } + Timestamp::on_finalize(System::block_number()); + System::reset_events(); + System::set_block_number(System::block_number() + 1); + System::on_initialize(System::block_number()); + } +} + +pub fn set_timestamp(t: u64) { + let _ = pallet_timestamp::Pallet::::set(RuntimeOrigin::none(), t); +} + +#[test] +fn bond_works() { + new_test_ext().execute_with(|| { + let now: Moment = 42; + set_timestamp(now); + let alice = AccountKeyring::Alice.to_account_id(); + let amount: BalanceOf = 10_000_000_000_000; + assert_ok!(TeerDays::bond(RuntimeOrigin::signed(alice.clone()), amount)); + + let expected_event = + RuntimeEvent::TeerDays(TeerDaysEvent::Bonded { account: alice.clone(), amount }); + assert!(System::events().iter().any(|a| a.event == expected_event)); + + let teerdays = TeerDays::teerday_bonds(&alice) + .expect("TeerDays entry for bonded account should exist"); + assert_eq!(teerdays.value, amount); + assert_eq!(teerdays.accumulated_tokentime, 0); + assert_eq!(teerdays.last_updated, now); + + let account_info = System::account(&alice); + assert_eq!(account_info.consumers, 1); + assert_eq!(account_info.data.frozen, amount); + }) +} + +#[test] +fn bond_saturates_at_free() { + new_test_ext().execute_with(|| { + let now: Moment = 42; + set_timestamp(now); + let alice = AccountKeyring::Alice.to_account_id(); + let alice_free: BalanceOf = 5_000_000_000_000; + ::Currency::make_free_balance_be(&alice, alice_free); + let amount: BalanceOf = 10_000_000_000_000; + assert_ok!(TeerDays::bond(RuntimeOrigin::signed(alice.clone()), amount)); + + let expected_event = RuntimeEvent::TeerDays(TeerDaysEvent::Bonded { + account: alice.clone(), + amount: alice_free, + }); + assert!(System::events().iter().any(|a| a.event == expected_event)); + + let teerdays = TeerDays::teerday_bonds(&alice) + .expect("TeerDays entry for bonded account should exist"); + assert_eq!(teerdays.value, alice_free); + assert_eq!(teerdays.accumulated_tokentime, 0); + assert_eq!(teerdays.last_updated, now); + + let account_info = System::account(&alice); + assert_eq!(account_info.consumers, 1); + assert_eq!(account_info.data.frozen, alice_free); + }) +} + +#[test] +fn bond_extra_works() { + new_test_ext().execute_with(|| { + run_to_block(1); + let now: Moment = 42; + set_timestamp(now); + + let alice = AccountKeyring::Alice.to_account_id(); + let amount: BalanceOf = 10_000_000_000_000; + assert_ok!(TeerDays::bond(RuntimeOrigin::signed(alice.clone()), amount)); + + run_to_block(2); + let now = now + 10_000; + set_timestamp(now); + + let extra_amount = amount / 2; + assert_ok!(TeerDays::bond_extra(RuntimeOrigin::signed(alice.clone()), extra_amount)); + + let expected_event = RuntimeEvent::TeerDays(TeerDaysEvent::Bonded { + account: alice.clone(), + amount: extra_amount, + }); + assert!(System::events().iter().any(|a| a.event == expected_event)); + + let teerdays = TeerDays::teerday_bonds(&alice) + .expect("TeerDays entry for bonded account should exist"); + assert_eq!(teerdays.value, amount + extra_amount); + assert_eq!(teerdays.accumulated_tokentime, amount * 10_000); + assert_eq!(teerdays.last_updated, now); + + let account_info = System::account(&alice); + assert_eq!(account_info.data.frozen, amount + extra_amount); + }) +} + +#[test] +fn bond_extra_saturates_at_free_margin() { + new_test_ext().execute_with(|| { + run_to_block(1); + let now: Moment = 42; + set_timestamp(now); + + let alice = AccountKeyring::Alice.to_account_id(); + let alice_free: BalanceOf = 11_000_000_000_000; + ::Currency::make_free_balance_be(&alice, alice_free); + let amount: BalanceOf = 10_000_000_000_000; + assert_ok!(TeerDays::bond(RuntimeOrigin::signed(alice.clone()), amount)); + + let teerdays = TeerDays::teerday_bonds(&alice) + .expect("TeerDays entry for bonded account should exist"); + assert_eq!(teerdays.value, amount); + assert_eq!(teerdays.accumulated_tokentime, 0); + assert_eq!(teerdays.last_updated, now); + + let account_info = System::account(&alice); + assert_eq!(account_info.consumers, 1); + assert_eq!(account_info.data.frozen, amount); + + run_to_block(2); + let now = now + 10_000; + set_timestamp(now); + + let extra_amount = amount / 2; + assert_ok!(TeerDays::bond_extra(RuntimeOrigin::signed(alice.clone()), extra_amount)); + + let expected_event = RuntimeEvent::TeerDays(TeerDaysEvent::Bonded { + account: alice.clone(), + amount: 1_000_000_000_000, + }); + assert_eq!(System::events().get(1).unwrap().event, expected_event); + + let teerdays = TeerDays::teerday_bonds(&alice) + .expect("TeerDays entry for bonded account should exist"); + assert_eq!(teerdays.value, amount + 1_000_000_000_000); + assert_eq!(teerdays.accumulated_tokentime, amount * 10_000); + assert_eq!(teerdays.last_updated, now); + + let account_info = System::account(&alice); + assert_eq!(account_info.data.frozen, amount + 1_000_000_000_000); + }) +} + +#[test] +fn withrawing_unbonded_after_unlock_period_works() { + new_test_ext().execute_with(|| { + run_to_block(1); + let now: Moment = 42; + set_timestamp(now); + let alice = AccountKeyring::Alice.to_account_id(); + + let account_info = System::account(&alice); + assert_eq!(account_info.consumers, 0); + assert_eq!(account_info.data.frozen, 0); + + let amount: BalanceOf = 10_000_000_000_000; + assert_ok!(TeerDays::bond(RuntimeOrigin::signed(alice.clone()), amount)); + + run_to_block(2); + let now = now + UnlockPeriod::get(); + set_timestamp(now); + + let tokentime_accumulated = amount.saturating_mul(UnlockPeriod::get() as Balance); + + let unbond_amount = amount / 3; + assert_ok!(TeerDays::unbond(RuntimeOrigin::signed(alice.clone()), unbond_amount)); + + let expected_event = RuntimeEvent::TeerDays(TeerDaysEvent::Unbonded { + account: alice.clone(), + amount: unbond_amount, + }); + assert!(System::events().iter().any(|a| a.event == expected_event)); + + let teerdays = TeerDays::teerday_bonds(&alice) + .expect("TeerDays entry for bonded account should exist"); + assert_eq!(teerdays.value, amount - unbond_amount); + // accumulated tokentime is reduced pro-rata + assert_eq!( + teerdays.accumulated_tokentime, + tokentime_accumulated.saturating_mul(amount - unbond_amount) / amount + ); + + // can't unbond again + assert_noop!( + TeerDays::unbond(RuntimeOrigin::signed(alice.clone()), unbond_amount), + Error::::PendingUnlock + ); + // withdrawing not yet possible. + assert_noop!( + TeerDays::withdraw_unbonded(RuntimeOrigin::signed(alice.clone())), + Error::::PendingUnlock + ); + + run_to_block(3); + let now = now + UnlockPeriod::get(); + set_timestamp(now); + assert_ok!(TeerDays::withdraw_unbonded(RuntimeOrigin::signed(alice.clone()))); + + let account_info = System::account(&alice); + assert_eq!(account_info.consumers, 1); + assert_eq!(account_info.data.frozen, amount - unbond_amount); + + run_to_block(4); + let now = now + UnlockPeriod::get(); + set_timestamp(now); + + // unbond more than we have -> should saturate + assert_ok!(TeerDays::unbond(RuntimeOrigin::signed(alice.clone()), amount)); + assert!(TeerDays::teerday_bonds(&alice).is_none()); + + run_to_block(5); + let now = now + UnlockPeriod::get(); + set_timestamp(now); + assert_ok!(TeerDays::withdraw_unbonded(RuntimeOrigin::signed(alice.clone()))); + + let account_info = System::account(&alice); + assert_eq!(account_info.consumers, 0); + assert_eq!(account_info.data.frozen, 0); + }) +} + +#[test] +fn unbonding_saturates_at_bonded() { + new_test_ext().execute_with(|| { + run_to_block(1); + let now: Moment = 42; + set_timestamp(now); + let alice = AccountKeyring::Alice.to_account_id(); + + let account_info = System::account(&alice); + assert_eq!(account_info.consumers, 0); + assert_eq!(account_info.data.frozen, 0); + + let amount: BalanceOf = 10_000_000_000_000; + assert_ok!(TeerDays::bond(RuntimeOrigin::signed(alice.clone()), amount)); + + let unbond_amount = amount * 2; + assert_ok!(TeerDays::unbond(RuntimeOrigin::signed(alice.clone()), unbond_amount)); + + let expected_event = + RuntimeEvent::TeerDays(TeerDaysEvent::Unbonded { account: alice.clone(), amount }); + assert!(System::events().iter().any(|a| a.event == expected_event)); + assert!(TeerDays::teerday_bonds(&alice).is_none()); + assert_eq!(TeerDays::pending_unlock(&alice).unwrap().1, amount); + }) +} + +#[test] +fn update_other_works() { + new_test_ext().execute_with(|| { + run_to_block(1); + let now: Moment = 42; + set_timestamp(now); + let alice = AccountKeyring::Alice.to_account_id(); + let amount: BalanceOf = 10_000_000_000_000; + assert_ok!(TeerDays::bond(RuntimeOrigin::signed(alice.clone()), amount)); + + run_to_block(2); + + let now = now + UnlockPeriod::get(); + set_timestamp(now); + + assert_ok!(TeerDays::update_other(RuntimeOrigin::signed(alice.clone()), alice.clone())); + + let teerdays = TeerDays::teerday_bonds(&alice) + .expect("TeerDays entry for bonded account should exist"); + assert_eq!(teerdays.value, amount); + assert_eq!(teerdays.last_updated, now); + assert_eq!( + teerdays.accumulated_tokentime, + amount.saturating_mul(UnlockPeriod::get() as Balance) + ); + }) +} diff --git a/teerdays/src/weights.rs b/teerdays/src/weights.rs new file mode 100644 index 00000000..a7cae0dd --- /dev/null +++ b/teerdays/src/weights.rs @@ -0,0 +1,63 @@ +/* + Copyright 2021 Integritee AG + + Licenced under GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. You may obtain a copy of the + License at + + . + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +use frame_support::weights::Weight; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_teerdays. +pub trait WeightInfo { + fn bond() -> Weight; + fn unbond() -> Weight; + fn update_other() -> Weight; + fn withdraw_unbonded() -> Weight; +} + +/// Weights for pallet_sidechain using the Integritee parachain node and recommended hardware. +pub struct IntegriteeWeight(PhantomData); +impl WeightInfo for IntegriteeWeight { + fn bond() -> Weight { + todo!() + } + fn unbond() -> Weight { + todo!() + } + + fn update_other() -> Weight { + todo!() + } + + fn withdraw_unbonded() -> Weight { + todo!() + } +} + +// For tests +impl WeightInfo for () { + fn bond() -> Weight { + todo!() + } + fn unbond() -> Weight { + todo!() + } + fn update_other() -> Weight { + todo!() + } + + fn withdraw_unbonded() -> Weight { + todo!() + } +} diff --git a/teerex/sgx-verify/src/ephemeral_key.rs b/teerex/sgx-verify/src/ephemeral_key.rs index 72998af3..3682b438 100644 --- a/teerex/sgx-verify/src/ephemeral_key.rs +++ b/teerex/sgx-verify/src/ephemeral_key.rs @@ -18,6 +18,7 @@ use crate::{utils::length_from_raw_data, CertDer}; use sp_std::convert::TryFrom; +#[allow(dead_code)] pub struct EphemeralKey<'a>(&'a [u8]); pub const PRIME256V1_OID: &[u8; 10] = &[0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07];