From c66ebf3182117d5ac7b4fe2e1bcea7acd83acdd3 Mon Sep 17 00:00:00 2001 From: Bryan Chen Date: Wed, 12 Jul 2023 16:58:30 +1200 Subject: [PATCH] precompile for liquid-crowdloan --- Cargo.lock | 1 + modules/liquid-crowdloan/src/lib.rs | 66 ++-- runtime/common/Cargo.toml | 2 + .../common/src/precompile/liquid_crowdloan.rs | 292 ++++++++++++++++++ runtime/common/src/precompile/mock.rs | 37 ++- runtime/common/src/precompile/mod.rs | 11 + 6 files changed, 378 insertions(+), 31 deletions(-) create mode 100644 runtime/common/src/precompile/liquid_crowdloan.rs diff --git a/Cargo.lock b/Cargo.lock index f587d4d92f..c1e4acbdde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11591,6 +11591,7 @@ dependencies = [ "module-honzon", "module-idle-scheduler", "module-incentives", + "module-liquid-crowdloan", "module-loans", "module-nft", "module-prices", diff --git a/modules/liquid-crowdloan/src/lib.rs b/modules/liquid-crowdloan/src/lib.rs index 8971dd2fdd..efebf08c41 100644 --- a/modules/liquid-crowdloan/src/lib.rs +++ b/modules/liquid-crowdloan/src/lib.rs @@ -76,7 +76,7 @@ pub mod module { } #[pallet::event] - #[pallet::generate_deposit(fn deposit_event)] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] pub enum Event { /// Liquid Crowdloan asset was redeemed. Redeemed { currency_id: CurrencyId, amount: Balance }, @@ -100,33 +100,7 @@ pub mod module { pub fn redeem(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { let who = ensure_signed(origin)?; - let (currency_id, redeem_amount) = if let Some(redeem_currency_id) = RedeemCurrencyId::::get() { - // redeem the RedeemCurrencyId - // amount_pect = amount / lcdot_total_supply - // amount_redeem = amount_pect * redeem_currency_balance - - let redeem_currency_balance = T::Currency::free_balance(redeem_currency_id, &Self::account_id()); - let lcdot_total_supply = T::Currency::total_issuance(T::LiquidCrowdloanCurrencyId::get()); - - let amount_redeem = amount - .checked_mul(redeem_currency_balance) - .and_then(|x| x.checked_div(lcdot_total_supply)) - .ok_or(ArithmeticError::Overflow)?; - - (redeem_currency_id, amount_redeem) - } else { - // redeem DOT - let currency_id = T::RelayChainCurrencyId::get(); - (currency_id, amount) - }; - - T::Currency::withdraw(T::LiquidCrowdloanCurrencyId::get(), &who, amount)?; - T::Currency::transfer(currency_id, &Self::account_id(), &who, redeem_amount)?; - - Self::deposit_event(Event::Redeemed { - currency_id, - amount: redeem_amount, - }); + Self::do_redeem(&who, amount)?; Ok(()) } @@ -175,4 +149,40 @@ impl Pallet { pub fn account_id() -> T::AccountId { T::PalletId::get().into_account_truncating() } + + pub fn do_redeem(who: &T::AccountId, amount: Balance) -> Result { + let (currency_id, redeem_amount) = if let Some(redeem_currency_id) = RedeemCurrencyId::::get() { + // redeem the RedeemCurrencyId + // amount_pect = amount / lcdot_total_supply + // amount_redeem = amount_pect * redeem_currency_balance + + let redeem_currency_balance = T::Currency::free_balance(redeem_currency_id, &Self::account_id()); + let lcdot_total_supply = T::Currency::total_issuance(T::LiquidCrowdloanCurrencyId::get()); + + let amount_redeem = amount + .checked_mul(redeem_currency_balance) + .and_then(|x| x.checked_div(lcdot_total_supply)) + .ok_or(ArithmeticError::Overflow)?; + + (redeem_currency_id, amount_redeem) + } else { + // redeem DOT + let currency_id = T::RelayChainCurrencyId::get(); + (currency_id, amount) + }; + + T::Currency::withdraw(T::LiquidCrowdloanCurrencyId::get(), &who, amount)?; + T::Currency::transfer(currency_id, &Self::account_id(), &who, redeem_amount)?; + + Self::deposit_event(Event::Redeemed { + currency_id, + amount: redeem_amount, + }); + + Ok(redeem_amount) + } + + pub fn redeem_currency() -> CurrencyId { + RedeemCurrencyId::::get().unwrap_or_else(|| T::RelayChainCurrencyId::get()) + } } diff --git a/runtime/common/Cargo.toml b/runtime/common/Cargo.toml index cd9e7e60af..6d010f864f 100644 --- a/runtime/common/Cargo.toml +++ b/runtime/common/Cargo.toml @@ -67,6 +67,7 @@ module-cdp-engine = { path = "../../modules/cdp-engine", default-features = fals module-cdp-treasury = { path = "../../modules/cdp-treasury", default-features = false, optional = true } module-incentives = { path = "../../modules/incentives", default-features = false } module-transaction-pause = { path = "../../modules/transaction-pause", default-features = false } +module-liquid-crowdloan = { path = "../../modules/liquid-crowdloan", default-features = false } # orml orml-oracle = { path = "../../orml/oracle", default-features = false } @@ -142,6 +143,7 @@ std = [ "module-support/std", "module-transaction-pause/std", "module-transaction-payment/std", + "module-liquid-crowdloan/std", "primitives/std", "nutsfinance-stable-asset/std", diff --git a/runtime/common/src/precompile/liquid_crowdloan.rs b/runtime/common/src/precompile/liquid_crowdloan.rs new file mode 100644 index 0000000000..ca06f32833 --- /dev/null +++ b/runtime/common/src/precompile/liquid_crowdloan.rs @@ -0,0 +1,292 @@ +// This file is part of Acala. + +// Copyright (C) 2020-2023 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}, + target_gas_limit, +}; +use crate::WeightToGas; +use frame_support::log; +use module_evm::{ + precompiles::Precompile, + runner::state::{PrecompileFailure, PrecompileOutput, PrecompileResult}, + Context, ExitError, ExitRevert, ExitSucceed, +}; +use module_liquid_crowdloan::WeightInfo; +use module_support::Erc20InfoMapping as _; +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use sp_core::Get; +use sp_runtime::{traits::Convert, RuntimeDebug}; +use sp_std::{marker::PhantomData, prelude::*}; + +/// The `LiquidCrowdloan` impl precompile. +/// +/// +/// `input` data starts with `action`. +/// +/// Actions: +/// - Get price. Rest `input` bytes: `currency_id`. +pub struct LiquidCrowdloanPrecompile(PhantomData); + +#[module_evm_utility_macro::generate_function_selector] +#[derive(RuntimeDebug, Eq, PartialEq, TryFromPrimitive, IntoPrimitive)] +#[repr(u32)] +pub enum Action { + Redeem = "redeem(address,uint256)", + GetRedeemCurrency = "getRedeemCurrency()", +} + +impl Precompile for LiquidCrowdloanPrecompile +where + Runtime: module_evm::Config + module_prices::Config + module_liquid_crowdloan::Config, +{ + fn execute(input: &[u8], target_gas: Option, _context: &Context, _is_static: bool) -> PrecompileResult { + let input = Input::::new( + input, + target_gas_limit(target_gas), + ); + + let gas_cost = Pricer::::cost(&input)?; + + if let Some(gas_limit) = target_gas { + if gas_limit < gas_cost { + return Err(PrecompileFailure::Error { + exit_status: ExitError::OutOfGas, + }); + } + } + + let action = input.action()?; + + match action { + Action::Redeem => { + let who = input.account_id_at(1)?; + let amount = input.balance_at(2)?; + + let redeem_amount = + >::do_redeem(&who, amount).map_err(|e| { + PrecompileFailure::Revert { + exit_status: ExitRevert::Reverted, + output: Output::encode_error_msg("LiquidCrowdloan redeem failed", e), + cost: target_gas_limit(target_gas).unwrap_or_default(), + } + })?; + + log::debug!(target: "evm", "liuqid_crowdloan: Redeem who: {:?}, amount: {:?}, output: {:?}", who, amount, redeem_amount); + Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + cost: gas_cost, + output: Output::encode_uint(redeem_amount), + logs: Default::default(), + }) + } + Action::GetRedeemCurrency => { + let currency_id = >::redeem_currency(); + let address = ::Erc20InfoMapping::encode_evm_address(currency_id) + .unwrap_or_default(); + + log::debug!(target: "evm", "liuqid_crowdloan: GetRedeemCurrency output: {:?}", currency_id); + Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + cost: gas_cost, + output: Output::encode_address(address), + logs: Default::default(), + }) + } + } + } +} + +struct Pricer(PhantomData); + +impl Pricer +where + Runtime: module_evm::Config + module_prices::Config + module_liquid_crowdloan::Config, +{ + const BASE_COST: u64 = 200; + + fn cost( + input: &Input, + ) -> Result { + let action = input.action()?; + + let cost = match action { + Action::Redeem => { + let read_account = InputPricer::::read_accounts(1); + let weight = ::WeightInfo::redeem(); + + Self::BASE_COST + .saturating_add(read_account) + .saturating_add(WeightToGas::convert(weight)) + } + Action::GetRedeemCurrency => { + let weight = ::DbWeight::get().reads(1); + + Self::BASE_COST.saturating_add(WeightToGas::convert(weight)) + } + }; + Ok(Self::BASE_COST.saturating_add(cost)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::precompile::mock::{ + bob, bob_evm_addr, new_test_ext, Currencies, LiquidCrowdloan, LiquidCrowdloanPalletId, RuntimeOrigin, Test, + DOT, LCDOT, LDOT, + }; + use frame_support::assert_ok; + use hex_literal::hex; + use orml_traits::MultiCurrency; + use sp_runtime::traits::AccountIdConversion; + + type LiquidCrowdloanPrecompile = crate::precompile::LiquidCrowdloanPrecompile; + + #[test] + fn redeem_dot() { + new_test_ext().execute_with(|| { + let context = Context { + address: Default::default(), + caller: bob_evm_addr(), + apparent_value: Default::default(), + }; + + assert_ok!(Currencies::update_balance( + RuntimeOrigin::root(), + bob(), + LCDOT, + 1_000_000_000 + )); + + assert_ok!(Currencies::update_balance( + RuntimeOrigin::root(), + LiquidCrowdloanPalletId::get().into_account_truncating(), + DOT, + 1_000_000_000 + )); + + // redeem(address,uint256) -> 1e9a6950 + // who + // amount 1e9 + let input = hex!( + " + 1e9a6950 + 000000000000000000000000 1000000000000000000000000000000000000002 + 00000000000000000000000000000000 0000000000000000000000003b9aca00 + " + ); + + // 1e9 + let expected_output = hex! {" + 00000000000000000000000000000000 0000000000000000000000003b9aca00 + "}; + + let res = LiquidCrowdloanPrecompile::execute(&input, None, &context, false).unwrap(); + assert_eq!(res.exit_status, ExitSucceed::Returned); + assert_eq!(res.output, expected_output.to_vec()); + + assert_eq!(Currencies::free_balance(DOT, &bob()), 1_000_000_000); + assert_eq!(Currencies::free_balance(LCDOT, &bob()), 0); + }); + } + + #[test] + fn redeem_ldot() { + new_test_ext().execute_with(|| { + let context = Context { + address: Default::default(), + caller: bob_evm_addr(), + apparent_value: Default::default(), + }; + + assert_ok!(Currencies::update_balance( + RuntimeOrigin::root(), + bob(), + LCDOT, + 1_000_000_000 + )); + + assert_ok!(Currencies::update_balance( + RuntimeOrigin::root(), + LiquidCrowdloanPalletId::get().into_account_truncating(), + LDOT, + 11_000_000_000 + )); + + assert_ok!(LiquidCrowdloan::set_redeem_currency_id(RuntimeOrigin::root(), LDOT)); + + // redeem(address,uint256) -> 1e9a6950 + // who + // amount 1e9 + let input = hex!( + " + 1e9a6950 + 000000000000000000000000 1000000000000000000000000000000000000002 + 00000000000000000000000000000000 0000000000000000000000003b9aca00 + " + ); + + // 11e9 + let expected_output = hex! {" + 00000000000000000000000000000000 0000000000000000000000028fa6ae00 + "}; + + let res = LiquidCrowdloanPrecompile::execute(&input, None, &context, false).unwrap(); + assert_eq!(res.exit_status, ExitSucceed::Returned); + assert_eq!(res.output, expected_output.to_vec()); + + assert_eq!(Currencies::free_balance(LDOT, &bob()), 11_000_000_000); + assert_eq!(Currencies::free_balance(LCDOT, &bob()), 0); + }); + } + + #[test] + fn redeem_currency() { + new_test_ext().execute_with(|| { + let context = Context { + address: Default::default(), + caller: bob_evm_addr(), + apparent_value: Default::default(), + }; + + // getRedeemCurrency() -> 785ad4c3 + let input = hex!("785ad4c3"); + + // DOT + let expected_output = hex! {" + 000000000000000000000000 0000000000000000000100000000000000000002 + "}; + + let res = LiquidCrowdloanPrecompile::execute(&input, None, &context, false).unwrap(); + assert_eq!(res.exit_status, ExitSucceed::Returned); + assert_eq!(res.output, expected_output.to_vec()); + + assert_ok!(LiquidCrowdloan::set_redeem_currency_id(RuntimeOrigin::root(), LDOT)); + + // LDOT + let expected_output = hex! {" + 000000000000000000000000 0000000000000000000100000000000000000003 + "}; + + let res = LiquidCrowdloanPrecompile::execute(&input, None, &context, false).unwrap(); + assert_eq!(res.exit_status, ExitSucceed::Returned); + assert_eq!(res.output, expected_output.to_vec()); + }); + } +} diff --git a/runtime/common/src/precompile/mock.rs b/runtime/common/src/precompile/mock.rs index a4942068d2..2bd5bb81f6 100644 --- a/runtime/common/src/precompile/mock.rs +++ b/runtime/common/src/precompile/mock.rs @@ -34,9 +34,9 @@ use module_cdp_engine::CollateralCurrencyIds; use module_evm::{EvmChainId, EvmTask}; use module_evm_accounts::EvmAddressMapping; use module_support::{ - mocks::MockStableAsset, AddressMapping as AddressMappingT, AuctionManager, DEXIncentives, DispatchableTask, - EmergencyShutdown, ExchangeRate, ExchangeRateProvider, FractionalRate, HomaSubAccountXcm, PoolId, PriceProvider, - Rate, SpecificJointsSwap, + mocks::MockStableAsset, AddressMapping as AddressMappingT, AuctionManager, CrowdloanVaultXcm, DEXIncentives, + DispatchableTask, EmergencyShutdown, ExchangeRate, ExchangeRateProvider, FractionalRate, HomaSubAccountXcm, PoolId, + PriceProvider, Rate, SpecificJointsSwap, }; use orml_traits::{location::AbsoluteReserveProvider, parameter_type_with_key, MultiCurrency, MultiReservableCurrency}; pub use primitives::{ @@ -165,6 +165,7 @@ pub const ACA: CurrencyId = CurrencyId::Token(TokenSymbol::ACA); pub const AUSD: CurrencyId = CurrencyId::Token(TokenSymbol::AUSD); pub const DOT: CurrencyId = CurrencyId::Token(TokenSymbol::DOT); pub const LDOT: CurrencyId = CurrencyId::Token(TokenSymbol::LDOT); +pub const LCDOT: CurrencyId = CurrencyId::LiquidCrowdloan(13); pub const LP_ACA_AUSD: CurrencyId = CurrencyId::DexShare(DexShare::Token(TokenSymbol::ACA), DexShare::Token(TokenSymbol::AUSD)); @@ -902,6 +903,35 @@ impl orml_xtokens::Config for Test { type ReserveProvider = AbsoluteReserveProvider; } +parameter_types!( + pub CrowdloanVault: AccountId = AccountId::new([0u8; 32]); + pub const LiquidCrowdloanCurrencyId: CurrencyId = LCDOT; + pub LiquidCrowdloanPalletId: PalletId = PalletId(*b"aca/lqcl"); +); + +pub struct MockXcmTransfer; +impl CrowdloanVaultXcm for MockXcmTransfer { + fn transfer_to_liquid_crowdloan_module_account( + _vault: AccountId, + _recipient: AccountId, + _amount: Balance, + ) -> DispatchResult { + Ok(()) + } +} + +impl module_liquid_crowdloan::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Currency = Currencies; + type LiquidCrowdloanCurrencyId = LiquidCrowdloanCurrencyId; + type RelayChainCurrencyId = GetStakingCurrencyId; + type PalletId = LiquidCrowdloanPalletId; + type GovernanceOrigin = EnsureRoot; + type CrowdloanVault = CrowdloanVault; + type XcmTransfer = MockXcmTransfer; + 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]); @@ -980,6 +1010,7 @@ frame_support::construct_runtime!( Rewards: orml_rewards, XTokens: orml_xtokens, StableAsset: nutsfinance_stable_asset, + LiquidCrowdloan: module_liquid_crowdloan, } ); diff --git a/runtime/common/src/precompile/mod.rs b/runtime/common/src/precompile/mod.rs index a886448f95..20ba80abdb 100644 --- a/runtime/common/src/precompile/mod.rs +++ b/runtime/common/src/precompile/mod.rs @@ -46,6 +46,7 @@ pub mod homa; pub mod honzon; pub mod incentives; pub mod input; +pub mod liquid_crowdloan; pub mod multicurrency; pub mod nft; pub mod oracle; @@ -60,6 +61,7 @@ pub use evm_accounts::EVMAccountsPrecompile; pub use homa::HomaPrecompile; pub use honzon::HonzonPrecompile; pub use incentives::IncentivesPrecompile; +pub use liquid_crowdloan::LiquidCrowdloanPrecompile; pub use multicurrency::MultiCurrencyPrecompile; pub use nft::NFTPrecompile; pub use oracle::OraclePrecompile; @@ -95,6 +97,7 @@ pub const EVM_ACCOUNTS: H160 = H160(hex!("00000000000000000000000000000000000004 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 fn target_gas_limit(target_gas: Option) -> Option { target_gas.map(|x| x.saturating_div(10).saturating_mul(9)) // 90% @@ -138,6 +141,7 @@ where HONZON, INCENTIVES, XTOKENS, + // LIQUID_CROWDLOAN, ]), _marker: Default::default(), } @@ -172,6 +176,7 @@ where HONZON, INCENTIVES, XTOKENS, + // LIQUID_CROWDLOAN, ]), _marker: Default::default(), } @@ -206,6 +211,7 @@ where HONZON, INCENTIVES, XTOKENS, + LIQUID_CROWDLOAN, ]), _marker: Default::default(), } @@ -228,6 +234,7 @@ where HonzonPrecompile: Precompile, IncentivesPrecompile: Precompile, XtokensPrecompile: Precompile, + LiquidCrowdloanPrecompile: Precompile, { fn execute( &self, @@ -346,6 +353,10 @@ where )) } else if address == XTOKENS { Some(XtokensPrecompile::::execute(input, target_gas, context, is_static)) + } else if address == LIQUID_CROWDLOAN { + Some(LiquidCrowdloanPrecompile::::execute( + input, target_gas, context, is_static, + )) } else { None }