From 2a6f8e6a8da95f86f14a7b17012df5a7a5c450d1 Mon Sep 17 00:00:00 2001 From: Roznovjak Date: Thu, 20 Jul 2023 17:15:02 +0200 Subject: [PATCH] add configurable origins and unlock extrinsic --- pallets/bonds/src/lib.rs | 32 ++++++++++- pallets/bonds/src/tests/mock.rs | 5 +- pallets/bonds/src/tests/mod.rs | 1 + pallets/bonds/src/tests/redeem.rs | 43 +++++++++++++++ pallets/bonds/src/tests/unlock.rs | 90 +++++++++++++++++++++++++++++++ pallets/bonds/src/weights.rs | 9 ++++ 6 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 pallets/bonds/src/tests/unlock.rs diff --git a/pallets/bonds/src/lib.rs b/pallets/bonds/src/lib.rs index 2919b0001..eaddce134 100644 --- a/pallets/bonds/src/lib.rs +++ b/pallets/bonds/src/lib.rs @@ -115,6 +115,12 @@ pub mod pallet { /// Min number of blocks for maturity. type MinMaturity: Get; + /// The origin which can issue new bonds. + type IssueOrigin: EnsureOrigin; + + /// The origin which can issue new bonds. + type UnlockOrigin: EnsureOrigin; + /// Protocol Fee for type ProtocolFee: Get; @@ -147,6 +153,8 @@ pub mod pallet { bond_id: T::AssetId, amount: T::Balance, }, + /// Bonds were unlocked + BondsUnlocked { bond_id: T::AssetId }, } #[pallet::error] @@ -190,7 +198,7 @@ pub mod pallet { // not covered in the tests. Create an asset with empty name should always work let bond_asset_id = T::AssetRegistry::create_bond_asset(&vec![], asset_details.existential_deposit)?; - let fee = T::ProtocolFee::get().mul_ceil(amount); // TODO + let fee = T::ProtocolFee::get().mul_ceil(amount); // TODO: check let amount_without_fee = amount.checked_sub(&fee).ok_or(ArithmeticError::Overflow)?; let pallet_account = Self::account_id(); @@ -254,6 +262,28 @@ pub mod pallet { Ok(()) } + + /// + #[pallet::call_index(2)] + #[pallet::weight(::WeightInfo::redeem())] + pub fn unlock(origin: OriginFor, bond_id: T::AssetId) -> DispatchResult { + T::UnlockOrigin::ensure_origin(origin)?; + + RegisteredBonds::::try_mutate_exists(bond_id, |maybe_bond_data| -> DispatchResult { + let bond_data = maybe_bond_data.as_mut().ok_or(Error::::BondNotRegistered)?; + + let now = T::TimestampProvider::now(); + // do nothing if the bonds are already mature + if bond_data.maturity > now { + bond_data.maturity = now; + + Self::deposit_event(Event::BondsUnlocked { bond_id }); + } + Ok(()) + })?; + + Ok(()) + } } } diff --git a/pallets/bonds/src/tests/mock.rs b/pallets/bonds/src/tests/mock.rs index 42fe04f6a..ea8b5f1a5 100644 --- a/pallets/bonds/src/tests/mock.rs +++ b/pallets/bonds/src/tests/mock.rs @@ -20,6 +20,7 @@ use crate::*; use frame_support::traits::{ConstU128, Everything, GenesisBuild}; use frame_support::{construct_runtime, parameter_types, traits::ConstU32}; +use frame_system::{EnsureRoot, EnsureSigned}; use hydradx_traits::{BondRegistry, Registry}; use orml_traits::parameter_type_with_key; use sp_core::H256; @@ -88,6 +89,8 @@ impl Config for Test { type TimestampProvider = DummyTimestampProvider; type PalletId = BondsPalletId; type MinMaturity = MinMaturity; + type IssueOrigin = EnsureSigned; + type UnlockOrigin = EnsureRoot; type ProtocolFee = ProtocolFee; type FeeReceiver = TreasuryAccount; type WeightInfo = (); @@ -122,7 +125,7 @@ impl frame_system::Config for Test { type SystemWeightInfo = (); type SS58Prefix = SS58Prefix; type OnSetCode = (); - type MaxConsumers = frame_support::traits::ConstU32<16>; + type MaxConsumers = ConstU32<16>; } impl pallet_balances::Config for Test { diff --git a/pallets/bonds/src/tests/mod.rs b/pallets/bonds/src/tests/mod.rs index 7119df845..c21940bc7 100644 --- a/pallets/bonds/src/tests/mod.rs +++ b/pallets/bonds/src/tests/mod.rs @@ -1,3 +1,4 @@ mod issue; pub mod mock; mod redeem; +mod unlock; diff --git a/pallets/bonds/src/tests/redeem.rs b/pallets/bonds/src/tests/redeem.rs index 23bac647e..9ae8e690a 100644 --- a/pallets/bonds/src/tests/redeem.rs +++ b/pallets/bonds/src/tests/redeem.rs @@ -198,6 +198,49 @@ fn fully_redeem_bonds_should_work_when_with_non_zero_fee() { }); } +#[test] +fn redeem_bonds_should_work_when_redeemed_from_non_issuer_account() { + ExtBuilder::default().build().execute_with(|| { + // Arrange + let now = DummyTimestampProvider::::now(); + let maturity = now.checked_add(MONTH).unwrap(); + let amount = ONE; + let redeem_amount = ONE.checked_div(4).unwrap(); + let bond_id = next_asset_id(); + + // Act + assert_ok!(Bonds::issue(RuntimeOrigin::signed(ALICE), HDX, amount, maturity)); + assert_ok!(Tokens::transfer( + RuntimeOrigin::signed(ALICE), + BOB, + bond_id, + redeem_amount + )); + + System::set_block_number(2 * MONTH); + + assert_ok!(Bonds::redeem(RuntimeOrigin::signed(BOB), bond_id, redeem_amount)); + + // Assert + expect_events(vec![Event::BondsRedeemed { + who: BOB, + bond_id, + amount: redeem_amount, + } + .into()]); + + assert_eq!(Tokens::free_balance(HDX, &BOB), redeem_amount); + assert_eq!(Tokens::free_balance(bond_id, &BOB), 0); + + assert_eq!(Tokens::free_balance(HDX, &::FeeReceiver::get()), 0); + + assert_eq!( + Tokens::free_balance(HDX, &Bonds::account_id()), + amount.checked_sub(redeem_amount).unwrap() + ); + }); +} + #[test] fn redeem_bonds_should_fail_when_bond_not_exists() { ExtBuilder::default().build().execute_with(|| { diff --git a/pallets/bonds/src/tests/unlock.rs b/pallets/bonds/src/tests/unlock.rs new file mode 100644 index 000000000..dfd7ee2d0 --- /dev/null +++ b/pallets/bonds/src/tests/unlock.rs @@ -0,0 +1,90 @@ +// This file is part of HydraDX. + +// Copyright (C) 2020-2022 Intergalactic, Limited (GIB). +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::tests::mock::*; +use crate::*; +use frame_support::{assert_noop, assert_ok, assert_storage_noop}; +pub use pretty_assertions::{assert_eq, assert_ne}; +use sp_runtime::DispatchError::BadOrigin; + +#[test] +fn unlock_should_work_when_bonds_are_not_mature() { + ExtBuilder::default().build().execute_with(|| { + // Arrange + let now = DummyTimestampProvider::::now(); + let maturity = now.checked_add(MONTH).unwrap(); + let amount = ONE; + let bond_id = next_asset_id(); + + // Act + assert_ok!(Bonds::issue(RuntimeOrigin::signed(ALICE), HDX, amount, maturity,)); + + System::set_block_number(2 * WEEK); + let now = DummyTimestampProvider::::now(); + + assert_ok!(Bonds::unlock(RuntimeOrigin::root(), bond_id)); + + // Assert + expect_events(vec![Event::BondsUnlocked { bond_id }.into()]); + + assert_eq!( + Bonds::bonds(bond_id).unwrap(), + Bond { + maturity: now, + asset_id: HDX, + amount, + } + ); + }); +} + +#[test] +fn unlock_should_be_storage_noop_if_bonds_are_already_mature() { + ExtBuilder::default().build().execute_with(|| { + // Arrange + let now = DummyTimestampProvider::::now(); + let maturity = now.checked_add(MONTH).unwrap(); + let amount = ONE; + let bond_id = next_asset_id(); + + // Act + assert_ok!(Bonds::issue(RuntimeOrigin::signed(ALICE), HDX, amount, maturity,)); + + System::set_block_number(2 * MONTH); + + // Assert + assert_storage_noop!(Bonds::unlock(RuntimeOrigin::root(), bond_id).unwrap()); + }); +} + +#[test] +fn unlock_should_fail_when_called_from_wrong_origin() { + ExtBuilder::default().build().execute_with(|| { + // Arrange + let now = DummyTimestampProvider::::now(); + let maturity = now.checked_add(MONTH).unwrap(); + let amount = ONE; + let bond_id = next_asset_id(); + + // Act + assert_ok!(Bonds::issue(RuntimeOrigin::signed(ALICE), HDX, amount, maturity,)); + + System::set_block_number(2 * MONTH); + + assert_noop!(Bonds::unlock(RuntimeOrigin::signed(ALICE), bond_id), BadOrigin); + }); +} diff --git a/pallets/bonds/src/weights.rs b/pallets/bonds/src/weights.rs index bfe468dfb..e68d7e166 100644 --- a/pallets/bonds/src/weights.rs +++ b/pallets/bonds/src/weights.rs @@ -49,6 +49,7 @@ use sp_std::marker::PhantomData; pub trait WeightInfo { fn issue() -> Weight; fn redeem() -> Weight; + fn unlock() -> Weight; } /// Weights for pallet_otc using the hydraDX node and recommended hardware. @@ -62,6 +63,10 @@ impl WeightInfo for HydraWeight { fn redeem() -> Weight { Weight::zero() } + + fn unlock() -> Weight { + Weight::zero() + } } // For backwards compatibility and tests @@ -73,4 +78,8 @@ impl WeightInfo for () { fn redeem() -> Weight { Weight::zero() } + + fn unlock() -> Weight { + Weight::zero() + } }