diff --git a/pallet/deposit/src/lib.rs b/pallet/deposit/src/lib.rs index 5d17a611f..02de5689d 100644 --- a/pallet/deposit/src/lib.rs +++ b/pallet/deposit/src/lib.rs @@ -55,13 +55,16 @@ use sp_runtime::traits::AccountIdConversion; /// Milliseconds per month. pub const MILLISECS_PER_MONTH: Moment = MILLISECS_PER_YEAR / 12; -/// Asset's minting API. -pub trait Minting { +/// Simple asset APIs. +pub trait SimpleAsset { /// Account type. type AccountId; /// Mint API. fn mint(beneficiary: &Self::AccountId, amount: Balance) -> DispatchResult; + + /// Burn API. + fn burn(who: &Self::AccountId, amount: Balance) -> DispatchResult; } /// Deposit identifier. @@ -79,6 +82,8 @@ pub struct Deposit { pub id: DepositId, /// Deposited RING. pub value: Balance, + /// Start timestamp. + pub start_time: Moment, /// Expired timestamp. pub expired_time: Moment, /// Deposit state. @@ -102,7 +107,7 @@ pub mod pallet { type Ring: Currency; /// KTON asset. - type Kton: Minting; + type Kton: SimpleAsset; /// Minimum amount to lock at least. #[pallet::constant] @@ -115,10 +120,28 @@ pub mod pallet { type MaxDeposits: Get; } + #[allow(missing_docs)] #[pallet::event] - // TODO: event? - // #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event {} + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A new deposit has been created. + DepositCreated { + owner: T::AccountId, + deposit_id: DepositId, + value: Balance, + start_time: Moment, + expired_time: Moment, + kton_reward: Balance, + }, + /// An expired deposit has been claimed. + DepositClaimed { owner: T::AccountId, deposit_id: DepositId }, + /// An unexpired deposit has been claimed by paying the KTON penalty. + DepositClaimedWithPenalty { + owner: T::AccountId, + deposit_id: DepositId, + kton_penalty: Balance, + }, + } #[pallet::error] pub enum Error { @@ -136,6 +159,8 @@ pub mod pallet { DepositInUse, /// Deposit is not in use. DepositNotInUse, + /// Deposit is already expired. + DepositAlreadyExpired, } /// All deposits. @@ -169,7 +194,7 @@ pub mod pallet { Err(>::ExceedMaxDeposits)?; } - >::try_mutate(&who, |ds| { + let (deposit_id, start_time, expired_time) = >::try_mutate(&who, |ds| { let ds = if let Some(ds) = ds { ds } else { @@ -190,25 +215,32 @@ pub mod pallet { Continue(c) => c, Break(b) => b, }; + let start_time = T::UnixTime::now().as_millis(); + let expired_time = start_time + MILLISECS_PER_MONTH * months as Moment; ds.try_insert( id as _, - Deposit { - id, - value: amount, - expired_time: T::UnixTime::now().as_millis() - + MILLISECS_PER_MONTH * months as Moment, - in_use: false, - }, + Deposit { id, value: amount, start_time, expired_time, in_use: false }, ) .map_err(|_| >::ExceedMaxDeposits)?; - DispatchResult::Ok(()) + >::Ok((id, start_time, expired_time)) })?; + T::Ring::transfer(&who, &account_id(), amount, KeepAlive)?; - T::Kton::mint(&who, dc_inflation::deposit_interest(amount, months))?; - // TODO: event? + let kton_reward = dc_inflation::deposit_interest(amount, months); + + T::Kton::mint(&who, kton_reward)?; + + Self::deposit_event(Event::DepositCreated { + owner: who, + deposit_id, + value: amount, + start_time, + expired_time, + kton_reward, + }); Ok(()) } @@ -226,6 +258,11 @@ pub mod pallet { if d.expired_time <= now && !d.in_use { claimed += d.value; + Self::deposit_event(Event::DepositClaimed { + owner: who.clone(), + deposit_id: d.id, + }); + false } else { true @@ -243,12 +280,47 @@ pub mod pallet { T::Ring::transfer(&account_id(), &who, claimed, AllowDeath)?; - // TODO: event? - Ok(()) } - // TODO: claim_with_penalty + /// Claim the unexpired-locked RING by paying the KTON penalty. + #[pallet::weight(0)] + pub fn claim_with_penalty(origin: OriginFor, id: DepositId) -> DispatchResult { + let who = ensure_signed(origin)?; + let d = >::try_mutate(&who, |maybe_ds| { + let ds = maybe_ds.as_mut().ok_or(>::DepositNotFound)?; + let d = ds + .remove(ds.iter().position(|d| d.id == id).ok_or(>::DepositNotFound)?); + + if ds.is_empty() { + >::dec_consumers(&who); + + *maybe_ds = None; + } + + >::Ok(d) + })?; + let now = T::UnixTime::now().as_millis(); + + if d.expired_time <= now { + Err(>::DepositAlreadyExpired)?; + } + + let promise_m = (d.expired_time - d.start_time) / MILLISECS_PER_MONTH; + let elapsed_m = (now - d.start_time) / MILLISECS_PER_MONTH; + let kton_penalty = dc_inflation::deposit_interest(d.value, promise_m as _) + .saturating_sub(dc_inflation::deposit_interest(d.value, elapsed_m as _)) + .max(1) * 3; + + T::Kton::burn(&who, kton_penalty)?; + Self::deposit_event(Event::DepositClaimedWithPenalty { + owner: who, + deposit_id: id, + kton_penalty, + }); + + Ok(()) + } } } pub use pallet::*; diff --git a/pallet/deposit/tests/mock.rs b/pallet/deposit/tests/mock.rs index 6e5675cad..06d3a808d 100644 --- a/pallet/deposit/tests/mock.rs +++ b/pallet/deposit/tests/mock.rs @@ -82,16 +82,27 @@ impl pallet_assets::Config for Runtime { type WeightInfo = (); } -pub enum KtonMinting {} -impl darwinia_deposit::Minting for KtonMinting { +pub enum KtonAsset {} +impl darwinia_deposit::SimpleAsset for KtonAsset { type AccountId = u32; fn mint(beneficiary: &Self::AccountId, amount: Balance) -> sp_runtime::DispatchResult { Assets::mint(RuntimeOrigin::signed(0), 0, *beneficiary, amount) } + + fn burn( + who: &Self::AccountId, + amount: Balance, + ) -> sp_runtime::DispatchResult { + if Assets::balance(0, who) < amount { + Err(>::BalanceLow)?; + } + + Assets::burn(RuntimeOrigin::signed(0), 0, *who, amount) + } } impl darwinia_deposit::Config for Runtime { - type Kton = KtonMinting; + type Kton = KtonAsset; type MaxDeposits = frame_support::traits::ConstU32<16>; type MinLockingAmount = frame_support::traits::ConstU128; type Ring = Balances; diff --git a/pallet/deposit/tests/tests.rs b/pallet/deposit/tests/tests.rs index f7ceba134..3b46999b6 100644 --- a/pallet/deposit/tests/tests.rs +++ b/pallet/deposit/tests/tests.rs @@ -64,28 +64,38 @@ fn unique_identity_should_work() { assert_eq!( Deposit::deposit_of(&1).unwrap().as_slice(), &[ - DepositS { id: 0, value: UNIT, expired_time: MILLISECS_PER_MONTH, in_use: false }, + DepositS { + id: 0, + value: UNIT, + start_time: 0, + expired_time: MILLISECS_PER_MONTH, + in_use: false + }, DepositS { id: 1, value: 2 * UNIT, + start_time: 0, expired_time: 2 * MILLISECS_PER_MONTH, in_use: false }, DepositS { id: 2, value: 3 * UNIT, + start_time: 0, expired_time: MILLISECS_PER_MONTH, in_use: false }, DepositS { id: 3, value: 4 * UNIT, + start_time: 0, expired_time: 2 * MILLISECS_PER_MONTH, in_use: false }, DepositS { id: 4, value: 5 * UNIT, + start_time: 0, expired_time: MILLISECS_PER_MONTH, in_use: false } @@ -102,18 +112,21 @@ fn unique_identity_should_work() { DepositS { id: 0, value: 6 * UNIT, + start_time: MILLISECS_PER_MONTH, expired_time: 2 * MILLISECS_PER_MONTH, in_use: false }, DepositS { id: 1, value: 2 * UNIT, + start_time: 0, expired_time: 2 * MILLISECS_PER_MONTH, in_use: false }, DepositS { id: 3, value: 4 * UNIT, + start_time: 0, expired_time: 2 * MILLISECS_PER_MONTH, in_use: false }, @@ -127,24 +140,28 @@ fn unique_identity_should_work() { DepositS { id: 0, value: 6 * UNIT, + start_time: MILLISECS_PER_MONTH, expired_time: 2 * MILLISECS_PER_MONTH, in_use: false }, DepositS { id: 1, value: 2 * UNIT, + start_time: 0, expired_time: 2 * MILLISECS_PER_MONTH, in_use: false }, DepositS { id: 2, value: 7 * UNIT, + start_time: MILLISECS_PER_MONTH, expired_time: 2 * MILLISECS_PER_MONTH, in_use: false }, DepositS { id: 3, value: 4 * UNIT, + start_time: 0, expired_time: 2 * MILLISECS_PER_MONTH, in_use: false }, @@ -158,30 +175,35 @@ fn unique_identity_should_work() { DepositS { id: 0, value: 6 * UNIT, + start_time: MILLISECS_PER_MONTH, expired_time: 2 * MILLISECS_PER_MONTH, in_use: false }, DepositS { id: 1, value: 2 * UNIT, + start_time: 0, expired_time: 2 * MILLISECS_PER_MONTH, in_use: false }, DepositS { id: 2, value: 7 * UNIT, + start_time: MILLISECS_PER_MONTH, expired_time: 2 * MILLISECS_PER_MONTH, in_use: false }, DepositS { id: 3, value: 4 * UNIT, + start_time: 0, expired_time: 2 * MILLISECS_PER_MONTH, in_use: false }, DepositS { id: 4, value: 8 * UNIT, + start_time: MILLISECS_PER_MONTH, expired_time: 2 * MILLISECS_PER_MONTH, in_use: false }, @@ -203,6 +225,7 @@ fn expire_time_should_work() { .map(|i| DepositS { id: i - 1, value: UNIT, + start_time: (i - 1) as Moment * MILLISECS_PER_MONTH, expired_time: i as Moment * MILLISECS_PER_MONTH, in_use: false }) @@ -282,3 +305,31 @@ fn claim_should_work() { assert!(Deposit::deposit_of(&1).is_none()); }); } + +#[test] +fn claim_with_penalty_should_work() { + new_test_ext().execute_with(|| { + assert!(Deposit::deposit_of(&1).is_none()); + assert_ok!(Deposit::lock(RuntimeOrigin::signed(1), UNIT, 1)); + assert!(Deposit::deposit_of(&1).is_some()); + + assert_noop!( + Deposit::claim_with_penalty(RuntimeOrigin::signed(1), 0), + >::BalanceLow + ); + + assert_ok!(KtonAsset::mint(&1, UNIT)); + assert_ok!(Deposit::claim_with_penalty(RuntimeOrigin::signed(1), 0)); + assert_eq!(Assets::balance(0, 1), 999_984_771_573_604_062); + assert!(Deposit::deposit_of(&1).is_none()); + + assert_ok!(Deposit::lock(RuntimeOrigin::signed(1), UNIT, 1)); + efflux(MILLISECS_PER_MONTH); + assert!(Deposit::deposit_of(&1).is_some()); + + assert_noop!( + Deposit::claim_with_penalty(RuntimeOrigin::signed(1), 0), + >::DepositAlreadyExpired + ); + }); +} diff --git a/pallet/staking/src/lib.rs b/pallet/staking/src/lib.rs index dbfd80656..6912238c0 100644 --- a/pallet/staking/src/lib.rs +++ b/pallet/staking/src/lib.rs @@ -390,7 +390,7 @@ pub mod pallet { DispatchResult::Ok(()) })?; - Self::deposit_event(Event::::Staked { + Self::deposit_event(Event::Staked { staker: who, ring_amount, kton_amount, @@ -784,7 +784,7 @@ pub mod pallet { if let Ok(_i) = T::RingCurrency::deposit_into_existing(&c, c_commission_payout) { actual_payout += c_commission_payout; - Self::deposit_event(Event::::Payout { + Self::deposit_event(Event::Payout { staker: c, ring_amount: c_commission_payout, }); @@ -799,7 +799,7 @@ pub mod pallet { { actual_payout += n_payout; - Self::deposit_event(Event::::Payout { + Self::deposit_event(Event::Payout { staker: n_exposure.who, ring_amount: n_payout, }); @@ -893,7 +893,7 @@ where let collators = Self::elect(); - Self::deposit_event(Event::::Elected { collators: collators.clone() }); + Self::deposit_event(Event::Elected { collators: collators.clone() }); Some(collators) } diff --git a/pallet/staking/tests/mock.rs b/pallet/staking/tests/mock.rs index bfdc1c79b..8358b0e8a 100644 --- a/pallet/staking/tests/mock.rs +++ b/pallet/staking/tests/mock.rs @@ -97,16 +97,27 @@ impl frame_support::traits::UnixTime for Time { Time::get() } } -pub enum KtonMinting {} -impl darwinia_deposit::Minting for KtonMinting { +pub enum KtonAsset {} +impl darwinia_deposit::SimpleAsset for KtonAsset { type AccountId = u32; fn mint(beneficiary: &Self::AccountId, amount: Balance) -> sp_runtime::DispatchResult { Assets::mint(RuntimeOrigin::signed(0), 0, *beneficiary, amount) } + + fn burn( + who: &Self::AccountId, + amount: Balance, + ) -> sp_runtime::DispatchResult { + if Assets::balance(0, who) < amount { + Err(>::BalanceLow)?; + } + + Assets::burn(RuntimeOrigin::signed(0), 0, *who, amount) + } } impl darwinia_deposit::Config for Runtime { - type Kton = KtonMinting; + type Kton = KtonAsset; type MaxDeposits = frame_support::traits::ConstU32<16>; type MinLockingAmount = frame_support::traits::ConstU128; type Ring = Balances; diff --git a/precompile/deposit/src/mock.rs b/precompile/deposit/src/mock.rs index be699ad57..0761e5898 100644 --- a/precompile/deposit/src/mock.rs +++ b/precompile/deposit/src/mock.rs @@ -93,17 +93,21 @@ impl pallet_timestamp::Config for TestRuntime { type WeightInfo = (); } -pub enum KtonMinting {} -impl darwinia_deposit::Minting for KtonMinting { +pub enum KtonAsset {} +impl darwinia_deposit::SimpleAsset for KtonAsset { type AccountId = AccountId; fn mint(_beneficiary: &Self::AccountId, _amount: Balance) -> sp_runtime::DispatchResult { Ok(()) } + + fn burn(who: &Self::AccountId, amount: Balance) -> sp_runtime::DispatchResult { + Ok(()) + } } impl darwinia_deposit::Config for TestRuntime { - type Kton = KtonMinting; + type Kton = KtonAsset; type MaxDeposits = frame_support::traits::ConstU32<16>; type MinLockingAmount = frame_support::traits::ConstU128<100>; type Ring = Balances; diff --git a/precompile/staking/src/mock.rs b/precompile/staking/src/mock.rs index d7ad75908..10ebd68c7 100644 --- a/precompile/staking/src/mock.rs +++ b/precompile/staking/src/mock.rs @@ -95,17 +95,21 @@ impl pallet_timestamp::Config for TestRuntime { type WeightInfo = (); } -pub enum KtonMinting {} -impl darwinia_deposit::Minting for KtonMinting { +pub enum KtonAsset {} +impl darwinia_deposit::SimpleAsset for KtonAsset { type AccountId = AccountId; fn mint(_beneficiary: &Self::AccountId, _amount: Balance) -> sp_runtime::DispatchResult { Ok(()) } + + fn burn(who: &Self::AccountId, amount: Balance) -> sp_runtime::DispatchResult { + Ok(()) + } } impl darwinia_deposit::Config for TestRuntime { - type Kton = KtonMinting; + type Kton = KtonAsset; type MaxDeposits = frame_support::traits::ConstU32<16>; type MinLockingAmount = frame_support::traits::ConstU128<100>; type Ring = Balances; diff --git a/runtime/crab/src/pallets/deposit.rs b/runtime/crab/src/pallets/deposit.rs index 696b789e8..c0c7ab85f 100644 --- a/runtime/crab/src/pallets/deposit.rs +++ b/runtime/crab/src/pallets/deposit.rs @@ -20,11 +20,21 @@ use crate::*; pub enum CKtonMinting {} -impl darwinia_deposit::Minting for CKtonMinting { +impl darwinia_deposit::SimpleAsset for CKtonMinting { type AccountId = AccountId; fn mint(beneficiary: &Self::AccountId, amount: Balance) -> sp_runtime::DispatchResult { - Assets::mint(RuntimeOrigin::signed(ROOT), AssetIds::CKton as AssetId, *beneficiary, amount) + Assets::mint(RuntimeOrigin::signed(ROOT), AssetIds::CKton as _, *beneficiary, amount) + } + + fn burn(who: &Self::AccountId, amount: Balance) -> sp_runtime::DispatchResult { + let asset_id = AssetIds::CKton as _; + + if Assets::balance(asset_id, who) < amount { + Err(>::BalanceLow)?; + } + + Assets::burn(RuntimeOrigin::signed(ROOT), asset_id, *who, amount) } } diff --git a/runtime/darwinia/src/pallets/deposit.rs b/runtime/darwinia/src/pallets/deposit.rs index 4c46dd164..95904729c 100644 --- a/runtime/darwinia/src/pallets/deposit.rs +++ b/runtime/darwinia/src/pallets/deposit.rs @@ -19,17 +19,27 @@ // darwinia use crate::*; -pub enum KtonMinting {} -impl darwinia_deposit::Minting for KtonMinting { +pub enum KtonAsset {} +impl darwinia_deposit::SimpleAsset for KtonAsset { type AccountId = AccountId; fn mint(beneficiary: &Self::AccountId, amount: Balance) -> sp_runtime::DispatchResult { - Assets::mint(RuntimeOrigin::signed(ROOT), AssetIds::Kton as AssetId, *beneficiary, amount) + Assets::mint(RuntimeOrigin::signed(ROOT), AssetIds::Kton as _, *beneficiary, amount) + } + + fn burn(who: &Self::AccountId, amount: Balance) -> sp_runtime::DispatchResult { + let asset_id = AssetIds::Kton as _; + + if Assets::balance(asset_id, who) < amount { + Err(>::BalanceLow)?; + } + + Assets::burn(RuntimeOrigin::signed(ROOT), asset_id, *who, amount) } } impl darwinia_deposit::Config for Runtime { - type Kton = KtonMinting; + type Kton = KtonAsset; type MaxDeposits = frame_support::traits::ConstU32<16>; type MinLockingAmount = frame_support::traits::ConstU128; type Ring = Balances; diff --git a/runtime/pangolin/src/pallets/deposit.rs b/runtime/pangolin/src/pallets/deposit.rs index aacc925bd..5793f7187 100644 --- a/runtime/pangolin/src/pallets/deposit.rs +++ b/runtime/pangolin/src/pallets/deposit.rs @@ -20,11 +20,21 @@ use crate::*; pub enum PKtonMinting {} -impl darwinia_deposit::Minting for PKtonMinting { +impl darwinia_deposit::SimpleAsset for PKtonMinting { type AccountId = AccountId; fn mint(beneficiary: &Self::AccountId, amount: Balance) -> sp_runtime::DispatchResult { - Assets::mint(RuntimeOrigin::signed(ROOT), AssetIds::PKton as AssetId, *beneficiary, amount) + Assets::mint(RuntimeOrigin::signed(ROOT), AssetIds::PKton as _, *beneficiary, amount) + } + + fn burn(who: &Self::AccountId, amount: Balance) -> sp_runtime::DispatchResult { + let asset_id = AssetIds::PKton as _; + + if Assets::balance(asset_id, who) < amount { + Err(>::BalanceLow)?; + } + + Assets::burn(RuntimeOrigin::signed(ROOT), asset_id, *who, amount) } }