From 66af6593ad6a2a91b0f83020cc8f606cfae55143 Mon Sep 17 00:00:00 2001 From: wangjj9219 <183318287@qq.com> Date: Fri, 16 Aug 2024 09:45:31 +0800 Subject: [PATCH] earning precompile (#2776) * earning precompile * update EarningManager * fix tests * update predeploy-contracts * fix fmt * fix tests --- Cargo.lock | 3 + modules/earning/Cargo.toml | 2 + modules/earning/src/lib.rs | 196 ++++-- modules/earning/src/tests.rs | 138 +++++ modules/support/src/earning.rs | 34 ++ modules/support/src/lib.rs | 2 + predeploy-contracts | 2 +- primitives/src/bonding/ledger.rs | 8 + runtime/common/Cargo.toml | 4 + runtime/common/src/precompile/earning.rs | 691 ++++++++++++++++++++++ runtime/common/src/precompile/mock.rs | 46 +- runtime/common/src/precompile/mod.rs | 9 + runtime/common/src/precompile/schedule.rs | 2 +- 13 files changed, 1074 insertions(+), 63 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 b28939969c..5ed92d117c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6540,6 +6540,7 @@ dependencies = [ "acala-primitives", "frame-support", "frame-system", + "module-support", "orml-traits", "pallet-balances", "parity-scale-codec", @@ -11817,6 +11818,7 @@ dependencies = [ "module-cdp-treasury", "module-currencies", "module-dex", + "module-earning", "module-evm", "module-evm-accounts", "module-evm-bridge", @@ -11837,6 +11839,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..d5f2898686 100644 --- a/modules/earning/src/lib.rs +++ b/modules/earning/src/lib.rs @@ -26,12 +26,16 @@ 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}, Balance, }; -use sp_runtime::{traits::Saturating, Permill}; +use sp_runtime::{ + traits::{Saturating, Zero}, + DispatchError, Permill, +}; pub use module::*; @@ -138,15 +142,8 @@ pub mod module { pub fn bond(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { let who = ensure_signed(origin)?; - let change = ::bond(&who, amount)?; + let _ = Self::do_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(()) } @@ -158,16 +155,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, - }); - } + let _ = Self::do_unbond(&who, amount)?; Ok(()) } @@ -180,27 +168,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, - }); - } + let _ = Self::do_unbond_instant(&who, amount)?; Ok(()) } @@ -213,15 +181,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, - }); - } + let _ = Self::do_rebond(&who, amount)?; Ok(()) } @@ -232,22 +192,95 @@ 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())?; - - if let Some(change) = change { - Self::deposit_event(Event::Withdrawn { - who, - amount: change.change, - }); - } + let _ = Self::do_withdraw_unbonded(&who)?; Ok(()) } } } -impl Pallet {} +impl Pallet { + fn do_bond(who: &T::AccountId, amount: Balance) -> Result { + let change = ::bond(who, amount)?; + + if let Some(ref change) = change { + T::OnBonded::handle(&(who.clone(), change.change))?; + Self::deposit_event(Event::Bonded { + who: who.clone(), + amount: change.change, + }); + } + Ok(change.map_or(Zero::zero(), |c| c.change)) + } + + fn do_unbond(who: &T::AccountId, amount: Balance) -> Result { + let unbond_at = frame_system::Pallet::::block_number().saturating_add(T::UnbondingPeriod::get()); + let change = ::unbond(who, amount, unbond_at)?; + + if let Some(ref change) = change { + T::OnUnbonded::handle(&(who.clone(), change.change))?; + Self::deposit_event(Event::Unbonded { + who: who.clone(), + amount: change.change, + }); + } + + Ok(change.map_or(Zero::zero(), |c| c.change)) + } + + fn do_unbond_instant(who: &T::AccountId, amount: Balance) -> Result { + let fee_ratio = T::ParameterStore::get(InstantUnstakeFee).ok_or(Error::::NotAllowed)?; + + let change = ::unbond_instant(who, amount)?; + + if let Some(ref 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(change.map_or(Zero::zero(), |c| c.change)) + } + + fn do_rebond(who: &T::AccountId, amount: Balance) -> Result { + let change = ::rebond(who, amount)?; + + if let Some(ref change) = change { + T::OnBonded::handle(&(who.clone(), change.change))?; + Self::deposit_event(Event::Rebonded { + who: who.clone(), + amount: change.change, + }); + } + + Ok(change.map_or(Zero::zero(), |c| c.change)) + } + + fn do_withdraw_unbonded(who: &T::AccountId) -> Result { + let change = ::withdraw_unbonded(who, frame_system::Pallet::::block_number())?; + + if let Some(ref change) = change { + Self::deposit_event(Event::Withdrawn { + who: who.clone(), + amount: change.change, + }); + } + + Ok(change.map_or(Zero::zero(), |c| c.change)) + } +} impl BondingController for Pallet { type MinBond = T::MinBond; @@ -279,3 +312,48 @@ impl BondingController for Pallet { } } } + +impl EarningManager> for Pallet { + type Moment = BlockNumberFor; + type FeeRatio = Permill; + + fn bond(who: T::AccountId, amount: Balance) -> Result { + Self::do_bond(&who, amount) + } + + fn unbond(who: T::AccountId, amount: Balance) -> Result { + Self::do_unbond(&who, amount) + } + + fn unbond_instant(who: T::AccountId, amount: Balance) -> Result { + Self::do_unbond_instant(&who, amount) + } + + fn rebond(who: T::AccountId, amount: Balance) -> Result { + Self::do_rebond(&who, amount) + } + + fn withdraw_unbonded(who: T::AccountId) -> Result { + Self::do_withdraw_unbonded(&who) + } + + fn get_bonding_ledger(who: T::AccountId) -> BondingLedgerOf { + Self::ledger(who).unwrap_or_default() + } + + fn get_instant_unstake_fee() -> Option { + T::ParameterStore::get(InstantUnstakeFee) + } + + 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/earning/src/tests.rs b/modules/earning/src/tests.rs index 90e695a8c9..e895d23f24 100644 --- a/modules/earning/src/tests.rs +++ b/modules/earning/src/tests.rs @@ -237,3 +237,141 @@ fn rebond_works() { assert_no_handler_events(); }); } + +#[test] +fn earning_manager_getter_works() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(>::bond(ALICE, 1000), Ok(1000)); + assert_eq!(>::unbond(ALICE, 200), Ok(200)); + + assert_eq!( + >::get_bonding_ledger(ALICE).total(), + 1000 + ); + assert_eq!( + >::get_bonding_ledger(ALICE).active(), + 800 + ); + assert_eq!( + >::get_bonding_ledger(ALICE).unlocking(), + vec![(200, 4)] + ); + + assert_eq!( + >::get_instant_unstake_fee(), + Some(Permill::from_percent(10)) + ); + + assert_eq!(>::get_min_bond(), 100); + + assert_eq!(>::get_unbonding_period(), 3); + + assert_eq!(>::get_max_unbonding_chunks(), 3); + }); +} + +#[test] +fn earning_manager_handler_works() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + >::unbond(ALICE, 1000), + Error::::NotBonded + ); + assert_eq!(>::bond(ALICE, 1000), Ok(1000)); + + assert_noop!( + >::unbond(ALICE, 999), + Error::::BelowMinBondThreshold + ); + + clear_handler_events(); + + // Won't unbond before unbonding period passes + assert_eq!(>::unbond(ALICE, 1001), Ok(1000)); + System::assert_last_event( + Event::Unbonded { + who: ALICE, + amount: 1000, + } + .into(), + ); + OnUnbonded::assert_eq_and_clear(vec![(ALICE, 1000)]); + System::reset_events(); + assert_eq!(>::withdraw_unbonded(ALICE), Ok(0)); + assert_eq!(System::events(), vec![]); + assert_eq!( + Balances::reducible_balance(&ALICE, Preservation::Expendable, Fortitude::Polite), + 0 + ); + + System::set_block_number(4); + + assert_eq!(>::withdraw_unbonded(ALICE), Ok(1000)); + System::assert_last_event( + Event::Withdrawn { + who: ALICE, + amount: 1000, + } + .into(), + ); + assert_eq!( + Balances::reducible_balance(&ALICE, Preservation::Expendable, Fortitude::Polite), + 1000 + ); + + assert_noop!( + >::unbond_instant(ALICE, 1000), + Error::::NotBonded + ); + + assert_no_handler_events(); + + assert_eq!(>::bond(ALICE, 1000), Ok(1000)); + assert_eq!( + Balances::reducible_balance(&ALICE, Preservation::Expendable, Fortitude::Polite), + 0 + ); + assert_eq!(>::unbond(ALICE, 1000), Ok(1000)); + + System::reset_events(); + clear_handler_events(); + + // unbond instant will not work on pending unbond funds + assert_eq!(>::unbond_instant(ALICE, 1001), Ok(0)); + assert_eq!(System::events(), vec![]); + clear_handler_events(); + + assert_eq!(>::rebond(ALICE, 1000), Ok(1000)); + OnBonded::assert_eq_and_clear(vec![(ALICE, 1000)]); + assert_eq!( + Balances::reducible_balance(&ALICE, Preservation::Expendable, Fortitude::Polite), + 0 + ); + + assert_noop!( + >::unbond_instant(ALICE, 999), + Error::::BelowMinBondThreshold + ); + assert_eq!( + >::unbond_instant(ALICE, 1001), + Ok(1000) + ); + System::assert_last_event( + Event::InstantUnbonded { + who: ALICE, + amount: 900, + fee: 100, + } + .into(), + ); + OnUnbonded::assert_eq_and_clear(vec![(ALICE, 1000)]); + OnUnstakeFee::assert_eq_and_clear(vec![100]); + // takes instant unbonding fee + assert_eq!( + Balances::reducible_balance(&ALICE, Preservation::Expendable, Fortitude::Polite), + 900 + ); + + assert_no_handler_events(); + }); +} diff --git a/modules/support/src/earning.rs b/modules/support/src/earning.rs new file mode 100644 index 0000000000..56ccc91f73 --- /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::DispatchError; + +pub trait EarningManager { + type Moment; + type FeeRatio; + fn bond(who: AccountId, amount: Balance) -> Result; + fn unbond(who: AccountId, amount: Balance) -> Result; + fn unbond_instant(who: AccountId, amount: Balance) -> Result; + fn rebond(who: AccountId, amount: Balance) -> Result; + fn withdraw_unbonded(who: AccountId) -> Result; + 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() -> Option; +} 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/predeploy-contracts b/predeploy-contracts index a5ef0f2726..95e0d858ac 160000 --- a/predeploy-contracts +++ b/predeploy-contracts @@ -1 +1 @@ -Subproject commit a5ef0f27263296006164b1aceef962d7bece0bd3 +Subproject commit 95e0d858ac4a748c5072e4ad55413c306d5bb5e1 diff --git a/primitives/src/bonding/ledger.rs b/primitives/src/bonding/ledger.rs index 367f060788..e52366954e 100644 --- a/primitives/src/bonding/ledger.rs +++ b/primitives/src/bonding/ledger.rs @@ -74,6 +74,14 @@ where self.total } + pub fn unlocking(&self) -> sp_std::vec::Vec<(Balance, Moment)> { + self.unlocking + .iter() + .cloned() + .map(|chunk| (chunk.value, chunk.unlock_at)) + .collect() + } + pub fn unlocking_len(&self) -> usize { self.unlocking.len() } diff --git a/runtime/common/Cargo.toml b/runtime/common/Cargo.toml index 719f4eb157..f47432a516 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..459a87df0f --- /dev/null +++ b/runtime/common/src/precompile/earning.rs @@ -0,0 +1,691 @@ +// 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, Zero}, + Permill, RuntimeDebug, +}; +use sp_std::{marker::PhantomData, prelude::*}; + +/// The Earning precompile +/// +/// `input` data starts with `action`. +/// +/// Actions: +/// - Bond. `input` bytes: `who`. +/// - Unbond. `input` bytes: `who`. +/// - UnbondInstant. `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 + ); + + let bonded_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: Output::encode_uint(bonded_amount), + }) + } + 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 + ); + + let unbonded_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: Output::encode_uint(unbonded_amount), + }) + } + 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 + ); + + let unbonded_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: Output::encode_uint(unbonded_amount), + }) + } + 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 + ); + + let rebonded_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: Output::encode_uint(rebonded_amount), + }) + } + Action::WithdrawUnbonded => { + let who = input.account_id_at(1)?; + + log::debug!( + target: "evm", + "earning: withdraw_unbonded, who: {:?}", + &who + ); + + let withdrawed_amount = + 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: Output::encode_uint(withdrawed_amount), + }) + } + 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(|(value, unlock_at)| { + Token::Tuple(vec![ + Token::Uint(Into::::into(value)), + Token::Uint(Into::::into(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 (ratio, accuracy) = if let Some(ratio) = + as EarningManager<_, _, _>>::get_instant_unstake_fee() + { + (ratio.deconstruct(), Permill::one().deconstruct()) + } else { + (Zero::zero(), Zero::zero()) + }; + + Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: Output::encode_uint_tuple(vec![ratio, accuracy]), + }) + } + 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); + + // encoded value of 20_000_000_000_000; + let expected_output = hex! {"00000000000000000000000000000000 0000000000000000000012309ce54000"}.to_vec(); + assert_eq!(res.output, expected_output); + }); + } + + #[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); + + // encoded value of 20_000_000_000_000; + let expected_output = hex! {"00000000000000000000000000000000 0000000000000000000012309ce54000"}.to_vec(); + assert_eq!(res.output, expected_output); + }); + } + + #[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); + + // encoded value of 20_000_000_000_000; + let expected_output = hex! {"00000000000000000000000000000000 0000000000000000000012309ce54000"}.to_vec(); + assert_eq!(res.output, expected_output); + }); + } + + #[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); + + // encoded value of 20_000_000_000_000; + let expected_output = hex! {"00000000000000000000000000000000 0000000000000000000012309ce54000"}.to_vec(); + assert_eq!(res.output, expected_output); + }); + } + + #[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); + + // encoded value of 20_000_000_000_000; + let expected_output = hex! {"00000000000000000000000000000000 0000000000000000000012309ce54000"}.to_vec(); + assert_eq!(res.output, expected_output); + }); + } + + #[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 + 00000000000000000000000000000000 000000000000000000000000000f4240 + "} + .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); + }); + } + + #[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); + }); + } + + #[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) } diff --git a/runtime/common/src/precompile/schedule.rs b/runtime/common/src/precompile/schedule.rs index 9483aab3c9..7818cfe704 100644 --- a/runtime/common/src/precompile/schedule.rs +++ b/runtime/common/src/precompile/schedule.rs @@ -554,7 +554,7 @@ mod tests { run_to_block(4); #[cfg(not(feature = "with-ethereum-compatibility"))] { - assert_eq!(Balances::free_balance(from_account.clone()), 999999978576); + assert_eq!(Balances::free_balance(from_account.clone()), 999999978554); assert_eq!(Balances::reserved_balance(from_account), 0); assert_eq!(Balances::free_balance(to_account), 1000000000000); }