From ef4aadd5660b9aed65c676531ef17d5dafdafccc Mon Sep 17 00:00:00 2001 From: wangjj9219 <183318287@qq.com> Date: Tue, 9 Jul 2024 00:55:12 +0800 Subject: [PATCH] earning precompile --- Cargo.lock | 3 + modules/earning/Cargo.toml | 2 + modules/earning/src/lib.rs | 199 ++++--- modules/support/src/earning.rs | 34 ++ modules/support/src/lib.rs | 2 + primitives/src/bonding/ledger.rs | 10 +- runtime/common/Cargo.toml | 4 + runtime/common/src/precompile/earning.rs | 664 +++++++++++++++++++++++ runtime/common/src/precompile/mock.rs | 46 +- runtime/common/src/precompile/mod.rs | 9 + 10 files changed, 899 insertions(+), 74 deletions(-) create mode 100644 modules/support/src/earning.rs create mode 100644 runtime/common/src/precompile/earning.rs diff --git a/Cargo.lock b/Cargo.lock index 89220a1218..a90363956d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6544,6 +6544,7 @@ dependencies = [ "acala-primitives", "frame-support", "frame-system", + "module-support", "orml-traits", "pallet-balances", "parity-scale-codec", @@ -11821,6 +11822,7 @@ dependencies = [ "module-cdp-treasury", "module-currencies", "module-dex", + "module-earning", "module-evm", "module-evm-accounts", "module-evm-bridge", @@ -11841,6 +11843,7 @@ dependencies = [ "orml-currencies", "orml-nft", "orml-oracle", + "orml-parameters", "orml-rewards", "orml-tokens", "orml-traits", diff --git a/modules/earning/Cargo.toml b/modules/earning/Cargo.toml index 78da5c7f5f..58babc835d 100644 --- a/modules/earning/Cargo.toml +++ b/modules/earning/Cargo.toml @@ -15,6 +15,7 @@ sp-std = { workspace = true } orml-traits = { workspace = true } primitives = { workspace = true } +module-support = { workspace = true } [dev-dependencies] sp-io = { workspace = true, features = ["std"] } @@ -32,6 +33,7 @@ std = [ "sp-core/std", "sp-runtime/std", "sp-std/std", + "module-support/std", ] try-runtime = [ "frame-support/try-runtime", diff --git a/modules/earning/src/lib.rs b/modules/earning/src/lib.rs index b59916b16f..4f51ad2178 100644 --- a/modules/earning/src/lib.rs +++ b/modules/earning/src/lib.rs @@ -26,6 +26,7 @@ use frame_support::{ traits::{Currency, ExistenceRequirement, LockIdentifier, LockableCurrency, OnUnbalanced, WithdrawReasons}, }; use frame_system::pallet_prelude::*; +use module_support::EarningManager; use orml_traits::{define_parameters, parameters::ParameterStore, Handler}; use primitives::{ bonding::{self, BondingController}, @@ -138,16 +139,7 @@ pub mod module { pub fn bond(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { let who = ensure_signed(origin)?; - let change = ::bond(&who, amount)?; - - if let Some(change) = change { - T::OnBonded::handle(&(who.clone(), change.change))?; - Self::deposit_event(Event::Bonded { - who, - amount: change.change, - }); - } - Ok(()) + Self::do_bond(&who, amount) } /// Start unbonding tokens up to `amount`. @@ -158,18 +150,7 @@ pub mod module { pub fn unbond(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { let who = ensure_signed(origin)?; - let unbond_at = frame_system::Pallet::::block_number().saturating_add(T::UnbondingPeriod::get()); - let change = ::unbond(&who, amount, unbond_at)?; - - if let Some(change) = change { - T::OnUnbonded::handle(&(who.clone(), change.change))?; - Self::deposit_event(Event::Unbonded { - who, - amount: change.change, - }); - } - - Ok(()) + Self::do_unbond(&who, amount) } /// Unbond up to `amount` tokens instantly by paying a `InstantUnstakeFee` fee. @@ -180,29 +161,7 @@ pub mod module { pub fn unbond_instant(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { let who = ensure_signed(origin)?; - let fee_ratio = T::ParameterStore::get(InstantUnstakeFee).ok_or(Error::::NotAllowed)?; - - let change = ::unbond_instant(&who, amount)?; - - if let Some(change) = change { - let amount = change.change; - let fee = fee_ratio.mul_ceil(amount); - let final_amount = amount.saturating_sub(fee); - - let unbalance = - T::Currency::withdraw(&who, fee, WithdrawReasons::TRANSFER, ExistenceRequirement::KeepAlive)?; - T::OnUnstakeFee::on_unbalanced(unbalance); - - // remove all shares of the change amount. - T::OnUnbonded::handle(&(who.clone(), amount))?; - Self::deposit_event(Event::InstantUnbonded { - who, - amount: final_amount, - fee, - }); - } - - Ok(()) + Self::do_unbond_instant(&who, amount) } /// Rebond up to `amount` tokens from unbonding period. @@ -213,17 +172,7 @@ pub mod module { pub fn rebond(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { let who = ensure_signed(origin)?; - let change = ::rebond(&who, amount)?; - - if let Some(change) = change { - T::OnBonded::handle(&(who.clone(), change.change))?; - Self::deposit_event(Event::Rebonded { - who, - amount: change.change, - }); - } - - Ok(()) + Self::do_rebond(&who, amount) } /// Withdraw all unbonded tokens. @@ -232,22 +181,93 @@ pub mod module { pub fn withdraw_unbonded(origin: OriginFor) -> DispatchResult { let who = ensure_signed(origin)?; - let change = - ::withdraw_unbonded(&who, frame_system::Pallet::::block_number())?; + Self::do_withdraw_unbonded(&who) + } + } +} - if let Some(change) = change { - Self::deposit_event(Event::Withdrawn { - who, - amount: change.change, - }); - } +impl Pallet { + fn do_bond(who: &T::AccountId, amount: Balance) -> DispatchResult { + let change = ::bond(who, amount)?; - Ok(()) + if let Some(change) = change { + T::OnBonded::handle(&(who.clone(), change.change))?; + Self::deposit_event(Event::Bonded { + who: who.clone(), + amount: change.change, + }); } + Ok(()) } -} -impl Pallet {} + fn do_unbond(who: &T::AccountId, amount: Balance) -> DispatchResult { + let unbond_at = frame_system::Pallet::::block_number().saturating_add(T::UnbondingPeriod::get()); + let change = ::unbond(who, amount, unbond_at)?; + + if let Some(change) = change { + T::OnUnbonded::handle(&(who.clone(), change.change))?; + Self::deposit_event(Event::Unbonded { + who: who.clone(), + amount: change.change, + }); + } + + Ok(()) + } + + fn do_unbond_instant(who: &T::AccountId, amount: Balance) -> DispatchResult { + let fee_ratio = T::ParameterStore::get(InstantUnstakeFee).ok_or(Error::::NotAllowed)?; + + let change = ::unbond_instant(who, amount)?; + + if let Some(change) = change { + let amount = change.change; + let fee = fee_ratio.mul_ceil(amount); + let final_amount = amount.saturating_sub(fee); + + let unbalance = + T::Currency::withdraw(who, fee, WithdrawReasons::TRANSFER, ExistenceRequirement::KeepAlive)?; + T::OnUnstakeFee::on_unbalanced(unbalance); + + // remove all shares of the change amount. + T::OnUnbonded::handle(&(who.clone(), amount))?; + Self::deposit_event(Event::InstantUnbonded { + who: who.clone(), + amount: final_amount, + fee, + }); + } + + Ok(()) + } + + fn do_rebond(who: &T::AccountId, amount: Balance) -> DispatchResult { + let change = ::rebond(who, amount)?; + + if let Some(change) = change { + T::OnBonded::handle(&(who.clone(), change.change))?; + Self::deposit_event(Event::Rebonded { + who: who.clone(), + amount: change.change, + }); + } + + Ok(()) + } + + fn do_withdraw_unbonded(who: &T::AccountId) -> DispatchResult { + let change = ::withdraw_unbonded(who, frame_system::Pallet::::block_number())?; + + if let Some(change) = change { + Self::deposit_event(Event::Withdrawn { + who: who.clone(), + amount: change.change, + }); + } + + Ok(()) + } +} impl BondingController for Pallet { type MinBond = T::MinBond; @@ -279,3 +299,48 @@ impl BondingController for Pallet { } } } + +impl EarningManager> for Pallet { + type Moment = BlockNumberFor; + type FeeRatio = Permill; + + fn bond(who: T::AccountId, amount: Balance) -> DispatchResult { + Self::do_bond(&who, amount) + } + + fn unbond(who: T::AccountId, amount: Balance) -> DispatchResult { + Self::do_unbond(&who, amount) + } + + fn unbond_instant(who: T::AccountId, amount: Balance) -> DispatchResult { + Self::do_unbond_instant(&who, amount) + } + + fn rebond(who: T::AccountId, amount: Balance) -> DispatchResult { + Self::do_rebond(&who, amount) + } + + fn withdraw_unbonded(who: T::AccountId) -> DispatchResult { + Self::do_withdraw_unbonded(&who) + } + + fn get_bonding_ledger(who: T::AccountId) -> BondingLedgerOf { + Self::ledger(who).unwrap_or_default() + } + + fn get_instant_unstake_fee() -> Permill { + T::ParameterStore::get(InstantUnstakeFee).unwrap_or_default() + } + + fn get_min_bond() -> Balance { + T::MinBond::get() + } + + fn get_unbonding_period() -> BlockNumberFor { + T::UnbondingPeriod::get() + } + + fn get_max_unbonding_chunks() -> u32 { + T::MaxUnbondingChunks::get() + } +} diff --git a/modules/support/src/earning.rs b/modules/support/src/earning.rs new file mode 100644 index 0000000000..78513636d0 --- /dev/null +++ b/modules/support/src/earning.rs @@ -0,0 +1,34 @@ +// This file is part of Acala. + +// Copyright (C) 2020-2024 Acala Foundation. +// 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 sp_runtime::DispatchResult; + +pub trait EarningManager { + type Moment; + type FeeRatio; + fn bond(who: AccountId, amount: Balance) -> DispatchResult; + fn unbond(who: AccountId, amount: Balance) -> DispatchResult; + fn unbond_instant(who: AccountId, amount: Balance) -> DispatchResult; + fn rebond(who: AccountId, amount: Balance) -> DispatchResult; + fn withdraw_unbonded(who: AccountId) -> DispatchResult; + fn get_bonding_ledger(who: AccountId) -> BondingLedger; + fn get_min_bond() -> Balance; + fn get_unbonding_period() -> Self::Moment; + fn get_max_unbonding_chunks() -> u32; + fn get_instant_unstake_fee() -> Self::FeeRatio; +} diff --git a/modules/support/src/lib.rs b/modules/support/src/lib.rs index 666ae37c18..8b4c6e569c 100644 --- a/modules/support/src/lib.rs +++ b/modules/support/src/lib.rs @@ -31,6 +31,7 @@ use xcm::prelude::*; pub mod bounded; pub mod dex; +pub mod earning; pub mod evm; pub mod homa; pub mod honzon; @@ -42,6 +43,7 @@ pub mod stable_asset; pub use crate::bounded::*; pub use crate::dex::*; +pub use crate::earning::*; pub use crate::evm::*; pub use crate::homa::*; pub use crate::honzon::*; diff --git a/primitives/src/bonding/ledger.rs b/primitives/src/bonding/ledger.rs index 367f060788..4a18030ba2 100644 --- a/primitives/src/bonding/ledger.rs +++ b/primitives/src/bonding/ledger.rs @@ -29,9 +29,9 @@ use frame_support::pallet_prelude::*; #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, MaxEncodedLen, TypeInfo)] pub struct UnlockChunk { /// Amount of funds to be unlocked. - value: Balance, + pub value: Balance, /// Era number at which point it'll be unlocked. - unlock_at: Moment, + pub unlock_at: Moment, } /// The ledger of a (bonded) account. @@ -45,13 +45,13 @@ where /// The total amount of the account's balance that we are currently /// accounting for. It's just `active` plus all the `unlocking` /// balances. - total: Balance, + pub total: Balance, /// The total amount of the account's balance that will be at stake in /// any forthcoming rounds. - active: Balance, + pub active: Balance, /// Any balance that is becoming free, which may eventually be /// transferred out of the account. - unlocking: BoundedVec, MaxUnlockingChunks>, + pub unlocking: BoundedVec, MaxUnlockingChunks>, _phantom: PhantomData, } diff --git a/runtime/common/Cargo.toml b/runtime/common/Cargo.toml index c0c5fe923a..2d2de77cc5 100644 --- a/runtime/common/Cargo.toml +++ b/runtime/common/Cargo.toml @@ -58,6 +58,7 @@ module-prices = { workspace = true } module-transaction-payment = { workspace = true } module-nft = { workspace = true } module-dex = { workspace = true } +module-earning = { workspace = true } module-evm-accounts = { workspace = true } module-homa = { workspace = true } module-asset-registry = { workspace = true, optional = true } @@ -81,6 +82,7 @@ wasm-bencher = { workspace = true, optional = true } orml-nft = { workspace = true, optional = true } orml-currencies = { workspace = true, optional = true } orml-rewards = { workspace = true, optional = true } +orml-parameters = { workspace = true } [dev-dependencies] orml-utilities = { workspace = true, features = ["std"] } @@ -128,12 +130,14 @@ std = [ "orml-tokens/std", "orml-traits/std", "orml-xtokens/std", + "orml-parameters/std", "module-asset-registry/std", "module-cdp-engine/std", "module-cdp-treasury/std", "module-currencies/std", "module-dex/std", + "module-earning/std", "module-evm-accounts/std", "module-evm-bridge/std", "module-evm/std", diff --git a/runtime/common/src/precompile/earning.rs b/runtime/common/src/precompile/earning.rs new file mode 100644 index 0000000000..8215f6e443 --- /dev/null +++ b/runtime/common/src/precompile/earning.rs @@ -0,0 +1,664 @@ +// This file is part of Acala. + +// Copyright (C) 2020-2024 Acala Foundation. +// 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 super::input::{Input, InputPricer, InputT, Output}; +use crate::WeightToGas; +use frame_support::traits::Get; +use module_evm::{ + precompiles::Precompile, ExitRevert, ExitSucceed, PrecompileFailure, PrecompileHandle, PrecompileOutput, + PrecompileResult, +}; +use module_support::EarningManager; + +use ethabi::Token; +use frame_system::pallet_prelude::BlockNumberFor; +use module_earning::{BondingLedgerOf, WeightInfo}; +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use primitives::Balance; +use sp_core::U256; +use sp_runtime::{traits::Convert, Permill, RuntimeDebug}; +use sp_std::{marker::PhantomData, prelude::*}; + +/// The Earning precompile +/// +/// `input` data starts with `action`. +/// +/// Actions: +/// - Mint. `input` bytes: `who`. +/// - Unbond. `input` bytes: `who`. +/// - Unbond instantly. `input` bytes: `who`. +/// - Rebond. `input` bytes: `who`. +/// - Withdraw unbonded. `input` bytes: `who`. +/// - Get bonding ledger. `input` bytes: `who`. +/// - Get minimum bond amount. +/// - Get unbonding period. +/// - Get maximum unbonding chunks amount. + +pub struct EarningPrecompile(PhantomData); + +#[module_evm_utility_macro::generate_function_selector] +#[derive(RuntimeDebug, Eq, PartialEq, TryFromPrimitive, IntoPrimitive)] +#[repr(u32)] +pub enum Action { + Bond = "bond(address,uint256)", + Unbond = "unbond(address,uint256)", + UnbondInstant = "unbondInstant(address,uint256)", + Rebond = "rebond(address,uint256)", + WithdrawUnbonded = "withdrawUnbonded(address)", + GetBondingLedger = "getBondingLedger(address)", + GetInstantUnstakeFee = "getInstantUnstakeFee()", + GetMinBond = "getMinBond()", + GetUnbondingPeriod = "getUnbondingPeriod()", + GetMaxUnbondingChunks = "getMaxUnbondingChunks()", +} + +impl Precompile for EarningPrecompile +where + Runtime: module_evm::Config + module_earning::Config + module_prices::Config, + module_earning::Pallet: EarningManager< + Runtime::AccountId, + Balance, + BondingLedgerOf, + FeeRatio = Permill, + Moment = BlockNumberFor, + >, +{ + fn execute(handle: &mut impl PrecompileHandle) -> PrecompileResult { + let gas_cost = Pricer::::cost(handle)?; + handle.record_cost(gas_cost)?; + + let input = Input::< + Action, + Runtime::AccountId, + ::AddressMapping, + Runtime::Erc20InfoMapping, + >::new(handle.input()); + + let action = input.action()?; + + match action { + Action::Bond => { + let who = input.account_id_at(1)?; + let amount = input.balance_at(2)?; + + log::debug!( + target: "evm", + "earning: bond, who: {:?}, amount: {:?}", + &who, amount + ); + + as EarningManager<_, _, _>>::bond(who, amount).map_err(|e| { + PrecompileFailure::Revert { + exit_status: ExitRevert::Reverted, + output: Output::encode_error_msg("Earning bond failed", e), + } + })?; + + Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: vec![], + }) + } + Action::Unbond => { + let who = input.account_id_at(1)?; + let amount = input.balance_at(2)?; + + log::debug!( + target: "evm", + "earning: unbond, who: {:?}, amount: {:?}", + &who, amount + ); + + as EarningManager<_, _, _>>::unbond(who, amount).map_err(|e| { + PrecompileFailure::Revert { + exit_status: ExitRevert::Reverted, + output: Output::encode_error_msg("Earning unbond failed", e), + } + })?; + + Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: vec![], + }) + } + Action::UnbondInstant => { + let who = input.account_id_at(1)?; + let amount = input.balance_at(2)?; + + log::debug!( + target: "evm", + "earning: unbond_instant, who: {:?}, amount: {:?}", + &who, amount + ); + + as EarningManager<_, _, _>>::unbond_instant(who, amount).map_err( + |e| PrecompileFailure::Revert { + exit_status: ExitRevert::Reverted, + output: Output::encode_error_msg("Earning unbond instantly failed", e), + }, + )?; + + Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: vec![], + }) + } + Action::Rebond => { + let who = input.account_id_at(1)?; + let amount = input.balance_at(2)?; + + log::debug!( + target: "evm", + "earning: rebond, who: {:?}, amount: {:?}", + &who, amount + ); + + as EarningManager<_, _, _>>::rebond(who, amount).map_err(|e| { + PrecompileFailure::Revert { + exit_status: ExitRevert::Reverted, + output: Output::encode_error_msg("Earning rebond failed", e), + } + })?; + + Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: vec![], + }) + } + Action::WithdrawUnbonded => { + let who = input.account_id_at(1)?; + + log::debug!( + target: "evm", + "earning: withdraw_unbonded, who: {:?}", + &who + ); + + as EarningManager<_, _, _>>::withdraw_unbonded(who).map_err(|e| { + PrecompileFailure::Revert { + exit_status: ExitRevert::Reverted, + output: Output::encode_error_msg("Earning withdraw unbonded failed", e), + } + })?; + + Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: vec![], + }) + } + Action::GetBondingLedger => { + let who = input.account_id_at(1)?; + let ledger = as EarningManager<_, _, _>>::get_bonding_ledger(who); + let unlocking_token: Vec = ledger + .unlocking + .iter() + .cloned() + .map(|chunk| { + Token::Tuple(vec![ + Token::Uint(Into::::into(chunk.value)), + Token::Uint(Into::::into(chunk.unlock_at)), + ]) + }) + .collect(); + let ledger_token: Token = Token::Tuple(vec![ + Token::Uint(Into::::into(ledger.total)), + Token::Uint(Into::::into(ledger.active)), + Token::Array(unlocking_token), + ]); + + Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: ethabi::encode(&[ledger_token]), + }) + } + Action::GetInstantUnstakeFee => { + let rate = as EarningManager<_, _, _>>::get_instant_unstake_fee(); + Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: Output::encode_uint(rate.deconstruct()), + }) + } + Action::GetMinBond => { + let amount = as EarningManager<_, _, _>>::get_min_bond(); + Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: Output::encode_uint(amount), + }) + } + Action::GetUnbondingPeriod => { + let period = as EarningManager<_, _, _>>::get_unbonding_period(); + Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: Output::encode_uint(Into::::into(period)), + }) + } + Action::GetMaxUnbondingChunks => { + let amount = as EarningManager<_, _, _>>::get_max_unbonding_chunks(); + Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: Output::encode_uint(amount), + }) + } + } + } +} + +struct Pricer(PhantomData); + +impl Pricer +where + Runtime: module_evm::Config + module_earning::Config + module_prices::Config, +{ + const BASE_COST: u64 = 200; + + fn cost(handle: &mut impl PrecompileHandle) -> Result { + let input = Input::::new( + handle.input(), + ); + let action = input.action()?; + + let cost: u64 = match action { + Action::Bond => { + let cost = InputPricer::::read_accounts(1); + let weight = ::WeightInfo::bond(); + + cost.saturating_add(WeightToGas::convert(weight)) + } + Action::Unbond => { + let cost = InputPricer::::read_accounts(1); + let weight = ::WeightInfo::unbond(); + + cost.saturating_add(WeightToGas::convert(weight)) + } + Action::UnbondInstant => { + let cost = InputPricer::::read_accounts(1); + let weight = ::WeightInfo::unbond_instant(); + + cost.saturating_add(WeightToGas::convert(weight)) + } + Action::Rebond => { + let cost = InputPricer::::read_accounts(1); + let weight = ::WeightInfo::rebond(); + + cost.saturating_add(WeightToGas::convert(weight)) + } + Action::WithdrawUnbonded => { + let cost = InputPricer::::read_accounts(1); + let weight = ::WeightInfo::withdraw_unbonded(); + + cost.saturating_add(WeightToGas::convert(weight)) + } + Action::GetBondingLedger => { + // Earning::Leger (r: 1) + WeightToGas::convert(::DbWeight::get().reads(1)) + } + Action::GetInstantUnstakeFee => { + // Runtime Config + Default::default() + } + Action::GetMinBond => { + // Runtime Config + Default::default() + } + Action::GetUnbondingPeriod => { + // Runtime Config + Default::default() + } + Action::GetMaxUnbondingChunks => { + // Runtime Config + Default::default() + } + }; + Ok(Self::BASE_COST.saturating_add(cost)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::precompile::mock::{ + alice, alice_evm_addr, new_test_ext, Currencies, Earning, RuntimeOrigin, System, Test, UnbondingPeriod, ACA, + }; + use frame_support::assert_ok; + use hex_literal::hex; + use module_evm::{precompiles::tests::MockPrecompileHandle, Context}; + use orml_traits::MultiCurrency; + + type EarningPrecompile = super::EarningPrecompile; + + #[test] + fn bond_works() { + new_test_ext().execute_with(|| { + let context = Context { + address: Default::default(), + caller: alice_evm_addr(), + apparent_value: Default::default(), + }; + + assert_ok!(Currencies::update_balance( + RuntimeOrigin::root(), + alice(), + ACA, + 99_000_000_000_000 + )); + + assert_eq!(Currencies::free_balance(ACA, &alice()), 100_000_000_000_000); + + // bond(address,uint256) -> 0xa515366a + // who 0x1000000000000000000000000000000000000001 + // amount 20_000_000_000_000 + let input = hex! {" + a515366a + 000000000000000000000000 1000000000000000000000000000000000000001 + 00000000000000000000000000000000 0000000000000000000012309ce54000 + "}; + + let res = + EarningPrecompile::execute(&mut MockPrecompileHandle::new(&input, None, &context, false)).unwrap(); + + assert_eq!(res.exit_status, ExitSucceed::Returned); + assert_eq!(Earning::ledger(&alice()).unwrap().active, 20_000_000_000_000); + }); + } + + #[test] + fn unbond_works() { + new_test_ext().execute_with(|| { + let context = Context { + address: Default::default(), + caller: alice_evm_addr(), + apparent_value: Default::default(), + }; + + assert_ok!(Currencies::update_balance( + RuntimeOrigin::root(), + alice(), + ACA, + 99_000_000_000_000 + )); + assert_ok!(Earning::bond(RuntimeOrigin::signed(alice()), 20_000_000_000_000)); + assert_eq!(Earning::ledger(&alice()).unwrap().active, 20_000_000_000_000); + + // unbond(address,uint256) -> 0xa5d059ca + // who 0x1000000000000000000000000000000000000001 + // amount 20_000_000_000_000 + let input = hex! {" + a5d059ca + 000000000000000000000000 1000000000000000000000000000000000000001 + 00000000000000000000000000000000 0000000000000000000012309ce54000 + "}; + + let res = + EarningPrecompile::execute(&mut MockPrecompileHandle::new(&input, None, &context, false)).unwrap(); + + assert_eq!(res.exit_status, ExitSucceed::Returned); + assert_eq!(Earning::ledger(&alice()).unwrap().active, 0); + }); + } + + #[test] + fn unbond_instant_works() { + new_test_ext().execute_with(|| { + let context = Context { + address: Default::default(), + caller: alice_evm_addr(), + apparent_value: Default::default(), + }; + + assert_ok!(Currencies::update_balance( + RuntimeOrigin::root(), + alice(), + ACA, + 99_000_000_000_000 + )); + assert_ok!(Earning::bond(RuntimeOrigin::signed(alice()), 20_000_000_000_000)); + assert_eq!(Earning::ledger(&alice()).unwrap().active, 20_000_000_000_000); + + // unbondInstant(address,uint256) -> 0xd15a4d60 + // who 0x1000000000000000000000000000000000000001 + // amount 20_000_000_000_000 + let input = hex! {" + d15a4d60 + 000000000000000000000000 1000000000000000000000000000000000000001 + 00000000000000000000000000000000 0000000000000000000012309ce54000 + "}; + + let res = + EarningPrecompile::execute(&mut MockPrecompileHandle::new(&input, None, &context, false)).unwrap(); + + assert_eq!(res.exit_status, ExitSucceed::Returned); + assert_eq!(Earning::ledger(&alice()).unwrap().active, 0); + }); + } + + #[test] + fn rebond_works() { + new_test_ext().execute_with(|| { + let context = Context { + address: Default::default(), + caller: alice_evm_addr(), + apparent_value: Default::default(), + }; + + assert_ok!(Currencies::update_balance( + RuntimeOrigin::root(), + alice(), + ACA, + 99_000_000_000_000 + )); + assert_ok!(Earning::bond(RuntimeOrigin::signed(alice()), 20_000_000_000_000)); + assert_ok!(Earning::unbond(RuntimeOrigin::signed(alice()), 20_000_000_000_000)); + assert_eq!(Earning::ledger(&alice()).unwrap().total, 20_000_000_000_000); + assert_eq!(Earning::ledger(&alice()).unwrap().active, 0); + + // rebond(address,uint256) -> 0x92d1b784 + // who 0x1000000000000000000000000000000000000001 + // amount 20_000_000_000_000 + let input = hex! {" + 92d1b784 + 000000000000000000000000 1000000000000000000000000000000000000001 + 00000000000000000000000000000000 0000000000000000000012309ce54000 + "}; + + let res = + EarningPrecompile::execute(&mut MockPrecompileHandle::new(&input, None, &context, false)).unwrap(); + + assert_eq!(res.exit_status, ExitSucceed::Returned); + assert_eq!(Earning::ledger(&alice()).unwrap().total, 20_000_000_000_000); + assert_eq!(Earning::ledger(&alice()).unwrap().active, 20_000_000_000_000); + }); + } + + #[test] + fn withdraw_unbonded_works() { + new_test_ext().execute_with(|| { + let context = Context { + address: Default::default(), + caller: alice_evm_addr(), + apparent_value: Default::default(), + }; + + assert_ok!(Currencies::update_balance( + RuntimeOrigin::root(), + alice(), + ACA, + 99_000_000_000_000 + )); + assert_ok!(Earning::bond(RuntimeOrigin::signed(alice()), 20_000_000_000_000)); + assert_ok!(Earning::unbond(RuntimeOrigin::signed(alice()), 20_000_000_000_000)); + assert_eq!(Earning::ledger(&alice()).unwrap().total, 20_000_000_000_000); + assert_eq!(Earning::ledger(&alice()).unwrap().active, 0); + + System::set_block_number(1 + 2 * UnbondingPeriod::get()); + + // withdrawUnbonded(address) -> 0xaeffaa47 + // who 0x1000000000000000000000000000000000000001 + let input = hex! {" + aeffaa47 + 000000000000000000000000 1000000000000000000000000000000000000001 + "}; + + let res = + EarningPrecompile::execute(&mut MockPrecompileHandle::new(&input, None, &context, false)).unwrap(); + assert_eq!(res.exit_status, ExitSucceed::Returned); + }); + } + + #[test] + fn get_min_bond_works() { + new_test_ext().execute_with(|| { + let context = Context { + address: Default::default(), + caller: alice_evm_addr(), + apparent_value: Default::default(), + }; + + // getMinBond() -> 0x5990dc2b + let input = hex! { + "5990dc2b" + }; + + // encoded value of 1_000_000_000; + let expected_output = hex! {"00000000000000000000000000000000 0000000000000000000000003b9aca00"}.to_vec(); + + let res = + EarningPrecompile::execute(&mut MockPrecompileHandle::new(&input, None, &context, false)).unwrap(); + assert_eq!(res.exit_status, ExitSucceed::Returned); + assert_eq!(res.output, expected_output); + }); + } + + #[test] + fn get_instant_unstake_fee_works() { + new_test_ext().execute_with(|| { + let context = Context { + address: Default::default(), + caller: alice_evm_addr(), + apparent_value: Default::default(), + }; + + // getInstantUnstakeFee() -> 0xc3e07c04 + let input = hex! { + "c3e07c04" + }; + + // encoded value of Permill::from_percent(10); + let expected_output = hex! {"00000000000000000000000000000000 000000000000000000000000000186a0"}.to_vec(); + + let res = + EarningPrecompile::execute(&mut MockPrecompileHandle::new(&input, None, &context, false)).unwrap(); + assert_eq!(res.exit_status, ExitSucceed::Returned); + assert_eq!(res.output, expected_output); + }); + } + + #[test] + fn get_unbonding_period_works() { + new_test_ext().execute_with(|| { + let context = Context { + address: Default::default(), + caller: alice_evm_addr(), + apparent_value: Default::default(), + }; + + // getUnbondingPeriod() -> 0x6fd2c80b + let input = hex! { + "6fd2c80b" + }; + + // encoded value of 10_000; + let expected_output = hex! {"00000000000000000000000000000000 00000000000000000000000000002710"}.to_vec(); + + let res = + EarningPrecompile::execute(&mut MockPrecompileHandle::new(&input, None, &context, false)).unwrap(); + assert_eq!(res.exit_status, ExitSucceed::Returned); + assert_eq!(res.output, expected_output); + + // let hex_string: String = res.output.iter().map(|byte| format!("{:02x}", + // byte)).collect(); assert_eq!(hex_string, ""); + }); + } + + #[test] + fn get_max_unbonding_chunks_works() { + new_test_ext().execute_with(|| { + let context = Context { + address: Default::default(), + caller: alice_evm_addr(), + apparent_value: Default::default(), + }; + + // getMaxUnbondingChunks() -> 0x09bfc8a1 + let input = hex! { + "09bfc8a1" + }; + + // encoded value of 10; + let expected_output = hex! {"00000000000000000000000000000000 0000000000000000000000000000000a"}.to_vec(); + + let res = + EarningPrecompile::execute(&mut MockPrecompileHandle::new(&input, None, &context, false)).unwrap(); + assert_eq!(res.exit_status, ExitSucceed::Returned); + assert_eq!(res.output, expected_output); + + // let hex_string: String = res.output.iter().map(|byte| format!("{:02x}", + // byte)).collect(); assert_eq!(hex_string, ""); + }); + } + + #[test] + fn get_bonding_ledger_works() { + new_test_ext().execute_with(|| { + let context = Context { + address: Default::default(), + caller: alice_evm_addr(), + apparent_value: Default::default(), + }; + + assert_ok!(Currencies::update_balance( + RuntimeOrigin::root(), + alice(), + ACA, + 99_000_000_000_000 + )); + assert_ok!(Earning::bond(RuntimeOrigin::signed(alice()), 20_000_000_000_000)); + + // getBondingLedger(address) -> 0x361592d7 + // who 0x1000000000000000000000000000000000000001 + let input = hex! {" + 361592d7 + 000000000000000000000000 1000000000000000000000000000000000000001 + "}; + + // encoded value of ledger of alice; + let expected_output = hex! {" + 0000000000000000000000000000000000000000000000000000000000000020 + 000000000000000000000000000000000000000000000000000012309ce54000 + 000000000000000000000000000000000000000000000000000012309ce54000 + 0000000000000000000000000000000000000000000000000000000000000060 + 0000000000000000000000000000000000000000000000000000000000000000 + "} + .to_vec(); + + let res = + EarningPrecompile::execute(&mut MockPrecompileHandle::new(&input, None, &context, false)).unwrap(); + assert_eq!(res.exit_status, ExitSucceed::Returned); + assert_eq!(res.output, expected_output); + }); + } +} diff --git a/runtime/common/src/precompile/mock.rs b/runtime/common/src/precompile/mock.rs index 474ac692f5..5aa130e1fa 100644 --- a/runtime/common/src/precompile/mock.rs +++ b/runtime/common/src/precompile/mock.rs @@ -22,8 +22,8 @@ use crate::{AllPrecompiles, Ratio, RuntimeBlockWeights, Weight}; use frame_support::{ derive_impl, ord_parameter_types, parameter_types, traits::{ - ConstU128, ConstU32, ConstU64, EqualPrivilegeOnly, Everything, InstanceFilter, Nothing, OnFinalize, - OnInitialize, SortedMembers, + ConstU128, ConstU32, ConstU64, EqualPrivilegeOnly, Everything, InstanceFilter, LockIdentifier, Nothing, + OnFinalize, OnInitialize, SortedMembers, }, weights::{ConstantMultiplier, IdentityFee}, PalletId, @@ -951,6 +951,47 @@ impl module_liquid_crowdloan::Config for Test { type WeightInfo = (); } +pub struct ParameterStoreImpl; +impl orml_traits::parameters::ParameterStore for ParameterStoreImpl { + fn get(key: K) -> Option + where + K: orml_traits::parameters::Key + + Into<::AggregratedKey>, + ::AggregratedValue: + TryInto, + { + let key = key.into(); + match key { + module_earning::ParametersKey::InstantUnstakeFee(_) => Some( + module_earning::ParametersValue::InstantUnstakeFee(sp_runtime::Permill::from_percent(10)) + .try_into() + .ok()? + .into(), + ), + } + } +} + +parameter_types! { + pub const MinBond: Balance = 1_000_000_000; + pub const UnbondingPeriod: BlockNumber = 10_000; + pub const EarningLockIdentifier: LockIdentifier = *b"aca/earn"; +} + +impl module_earning::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type ParameterStore = ParameterStoreImpl; + type OnBonded = module_incentives::OnEarningBonded; + type OnUnbonded = module_incentives::OnEarningUnbonded; + type OnUnstakeFee = (); + type MinBond = MinBond; + type UnbondingPeriod = UnbondingPeriod; + type MaxUnbondingChunks = ConstU32<10>; + type LockIdentifier = EarningLockIdentifier; + type WeightInfo = (); +} + pub const ALICE: AccountId = AccountId::new([1u8; 32]); pub const BOB: AccountId = AccountId::new([2u8; 32]); pub const EVA: AccountId = AccountId::new([5u8; 32]); @@ -1026,6 +1067,7 @@ frame_support::construct_runtime!( XTokens: orml_xtokens, StableAsset: nutsfinance_stable_asset, LiquidCrowdloan: module_liquid_crowdloan, + Earning: module_earning, } ); diff --git a/runtime/common/src/precompile/mod.rs b/runtime/common/src/precompile/mod.rs index 3203c58d3b..4102a80d78 100644 --- a/runtime/common/src/precompile/mod.rs +++ b/runtime/common/src/precompile/mod.rs @@ -39,6 +39,7 @@ use sp_runtime::traits::Zero; use sp_std::{collections::btree_set::BTreeSet, marker::PhantomData}; pub mod dex; +pub mod earning; pub mod evm; pub mod evm_accounts; pub mod homa; @@ -55,6 +56,7 @@ pub mod xtokens; use crate::SystemContractsFilter; pub use dex::DEXPrecompile; +pub use earning::EarningPrecompile; pub use evm::EVMPrecompile; pub use evm_accounts::EVMAccountsPrecompile; pub use homa::HomaPrecompile; @@ -97,6 +99,7 @@ pub const HONZON: H160 = H160(hex!("0000000000000000000000000000000000000409")); pub const INCENTIVES: H160 = H160(hex!("000000000000000000000000000000000000040a")); pub const XTOKENS: H160 = H160(hex!("000000000000000000000000000000000000040b")); pub const LIQUID_CROWDLOAN: H160 = H160(hex!("000000000000000000000000000000000000040c")); +pub const EARNING: H160 = H160(hex!("000000000000000000000000000000000000040d")); pub struct AllPrecompiles { set: BTreeSet, @@ -138,6 +141,7 @@ where INCENTIVES, XTOKENS, LIQUID_CROWDLOAN, + EARNING, ]), _marker: Default::default(), } @@ -173,6 +177,7 @@ where INCENTIVES, XTOKENS, // LIQUID_CROWDLOAN, + EARNING, ]), _marker: Default::default(), } @@ -208,6 +213,7 @@ where INCENTIVES, XTOKENS, // LIQUID_CROWDLOAN, + EARNING, ]), _marker: Default::default(), } @@ -231,6 +237,7 @@ where HonzonPrecompile: Precompile, IncentivesPrecompile: Precompile, XtokensPrecompile: Precompile, + EarningPrecompile: Precompile, { fn execute(&self, handle: &mut impl PrecompileHandle) -> Option { let context = handle.context(); @@ -336,6 +343,8 @@ where Some(IncentivesPrecompile::::execute(handle)) } else if address == XTOKENS { Some(XtokensPrecompile::::execute(handle)) + } else if address == EARNING { + Some(EarningPrecompile::::execute(handle)) } else { E::execute(&Default::default(), handle) }