diff --git a/Cargo.lock b/Cargo.lock index 67c08ae4fb..cadbf453de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -154,6 +154,7 @@ dependencies = [ "module-honzon", "module-idle-scheduler", "module-incentives", + "module-liquid-crowdloan", "module-loans", "module-nft", "module-prices", @@ -5760,6 +5761,7 @@ dependencies = [ "module-honzon", "module-idle-scheduler", "module-incentives", + "module-liquid-crowdloan", "module-loans", "module-nft", "module-nominees-election", @@ -6564,6 +6566,26 @@ dependencies = [ "sp-std", ] +[[package]] +name = "module-liquid-crowdloan" +version = "2.16.0" +dependencies = [ + "acala-primitives", + "frame-support", + "frame-system", + "module-currencies", + "module-support", + "orml-tokens", + "orml-traits", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "module-loans" version = "2.18.0" diff --git a/modules/liquid-crowdloan/Cargo.toml b/modules/liquid-crowdloan/Cargo.toml new file mode 100644 index 0000000000..1737479b09 --- /dev/null +++ b/modules/liquid-crowdloan/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "module-liquid-crowdloan" +version = "2.16.0" +authors = ["Acala Developers"] +edition = "2021" + +[dependencies] +scale-info = { version = "2.2.0", default-features = false, features = ["derive"] } +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false } + +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.38", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.38", default-features = false } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.38", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.38", default-features = false } + +orml-traits = { path = "../../orml/traits", default-features = false } +primitives = { package = "acala-primitives", path = "../../primitives", default-features = false } +support = { package = "module-support", path = "../support", default-features = false } + +[dev-dependencies] +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.38" } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.38" } +pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.38" } +module-currencies = { path = "../currencies" } +orml-tokens = { path = "../../orml/tokens" } + +[features] +default = ["std"] +std = [ + "codec/std", + "scale-info/std", + "sp-runtime/std", + "sp-std/std", + "frame-support/std", + "frame-system/std", + "orml-traits/std", + "primitives/std", + "support/std", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", +] diff --git a/modules/liquid-crowdloan/src/lib.rs b/modules/liquid-crowdloan/src/lib.rs new file mode 100644 index 0000000000..f4b367ffe7 --- /dev/null +++ b/modules/liquid-crowdloan/src/lib.rs @@ -0,0 +1,135 @@ +// 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 . + +//! # Liquid Crowdloan Module +//! +//! Allow people to redeem lcDOT for DOT. + +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(clippy::unused_unit)] + +use frame_support::{pallet_prelude::*, traits::EnsureOrigin, PalletId}; +use frame_system::pallet_prelude::*; +use orml_traits::MultiCurrency; +use primitives::{Balance, CurrencyId}; +use sp_runtime::traits::AccountIdConversion; + +use support::CrowdloanVaultXcm; + +mod mock; +mod tests; +pub mod weights; + +pub use module::*; +pub use weights::WeightInfo; + +#[frame_support::pallet] +pub mod module { + use super::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + type Currency: MultiCurrency; + + /// Liquid crowdloan currency Id, i.e. LCDOT for Polkadot. + #[pallet::constant] + type LiquidCrowdloanCurrencyId: Get; + + /// Relay chain currency Id, i.e. DOT for Polkadot. + #[pallet::constant] + type RelayChainCurrencyId: Get; + + /// Pallet Id for liquid crowdloan module. + #[pallet::constant] + type PalletId: Get; + + /// The governance origin for liquid crowdloan module. For instance for DOT cross-chain + /// transfer DOT from relay chain crowdloan vault to liquid crowdloan module account. + type GovernanceOrigin: EnsureOrigin; + + /// The crowdloan vault account on relay chain. + #[pallet::constant] + type CrowdloanVault: Get; + + /// XCM transfer impl. + type XcmTransfer: CrowdloanVaultXcm; + + /// Weight information for the extrinsics in this module. + type WeightInfo: WeightInfo; + } + + #[pallet::event] + #[pallet::generate_deposit(fn deposit_event)] + pub enum Event { + /// Liquid Crowdloan asset was redeemed. + Redeemed { amount: Balance }, + /// The transfer from relay chain crowdloan vault was requested. + TransferFromCrowdloanVaultRequested { amount: Balance }, + } + + #[pallet::pallet] + pub struct Pallet(_); + #[pallet::call] + impl Pallet { + /// Redeem liquid crowdloan currency for relay chain currency. + #[pallet::call_index(0)] + #[pallet::weight(::WeightInfo::redeem())] + pub fn redeem(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { + let who = ensure_signed(origin)?; + + T::Currency::withdraw(T::LiquidCrowdloanCurrencyId::get(), &who, amount)?; + + T::Currency::transfer(T::RelayChainCurrencyId::get(), &Self::account_id(), &who, amount)?; + + Self::deposit_event(Event::Redeemed { amount }); + + Ok(()) + } + + /// Send an XCM message to cross-chain transfer DOT from relay chain crowdloan vault to + /// liquid crowdloan module account. + /// + /// This call requires `GovernanceOrigin`. + #[pallet::call_index(1)] + #[pallet::weight(::WeightInfo::transfer_from_crowdloan_vault())] + pub fn transfer_from_crowdloan_vault( + origin: OriginFor, + #[pallet::compact] amount: Balance, + ) -> DispatchResult { + T::GovernanceOrigin::ensure_origin(origin)?; + + T::XcmTransfer::transfer_to_liquid_crowdloan_module_account( + T::CrowdloanVault::get(), + Self::account_id(), + amount, + )?; + + Self::deposit_event(Event::TransferFromCrowdloanVaultRequested { amount }); + + Ok(()) + } + } +} + +impl Pallet { + pub fn account_id() -> T::AccountId { + T::PalletId::get().into_account_truncating() + } +} diff --git a/modules/liquid-crowdloan/src/mock.rs b/modules/liquid-crowdloan/src/mock.rs new file mode 100644 index 0000000000..0c506eb458 --- /dev/null +++ b/modules/liquid-crowdloan/src/mock.rs @@ -0,0 +1,246 @@ +// 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 . + +//! Mocks for example module. + +#![cfg(test)] + +use super::*; +use crate as liquid_crowdloan; + +use frame_support::{ + construct_runtime, ord_parameter_types, parameter_types, + traits::{ConstU128, ConstU32, ConstU64, Everything, Nothing}, +}; +use frame_system::{EnsureRoot, EnsureSignedBy}; +use orml_traits::parameter_type_with_key; +use primitives::{Amount, TokenSymbol}; +use sp_core::{H160, H256}; +use sp_runtime::{testing::Header, traits::IdentityLookup, AccountId32}; +use std::cell::RefCell; +use support::mocks::MockAddressMapping; + +pub type AccountId = AccountId32; +pub type BlockNumber = u64; + +pub const ACA: CurrencyId = CurrencyId::Token(TokenSymbol::ACA); +pub const DOT: CurrencyId = CurrencyId::Token(TokenSymbol::DOT); +pub const LDOT: CurrencyId = CurrencyId::Token(TokenSymbol::LDOT); + +pub const ALICE: AccountId = AccountId32::new([1u8; 32]); +pub const BOB: AccountId = AccountId32::new([2u8; 32]); +pub const VAULT: AccountId = AccountId32::new([3u8; 32]); + +impl frame_system::Config for Runtime { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = BlockNumber; + type Hash = H256; + type Hashing = ::sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +parameter_type_with_key! { + pub ExistentialDeposits: |_currency_id: CurrencyId| -> Balance { + Default::default() + }; +} + +impl orml_tokens::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type Amount = Amount; + type CurrencyId = CurrencyId; + type WeightInfo = (); + type ExistentialDeposits = ExistentialDeposits; + type CurrencyHooks = (); + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type DustRemovalWhitelist = Nothing; +} + +impl pallet_balances::Config for Runtime { + type Balance = Balance; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ConstU128<0>; + type AccountStore = frame_system::Pallet; + type MaxLocks = (); + type WeightInfo = (); + type MaxReserves = (); + type ReserveIdentifier = (); +} + +pub type AdaptedBasicCurrency = module_currencies::BasicCurrencyAdapter; + +parameter_types! { + pub const GetNativeCurrencyId: CurrencyId = ACA; + pub Erc20HoldingAccount: H160 = H160::from_low_u64_be(1); + pub CrowdloanVault: AccountId = VAULT; + pub LiquidCrowdloanPalletId: PalletId = PalletId(*b"aca/lqcl"); + pub const Ldot: CurrencyId = LDOT; + pub const Dot: CurrencyId = DOT; +} + +impl module_currencies::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type MultiCurrency = Tokens; + type NativeCurrency = AdaptedBasicCurrency; + type GetNativeCurrencyId = GetNativeCurrencyId; + type Erc20HoldingAccount = Erc20HoldingAccount; + type WeightInfo = (); + type AddressMapping = MockAddressMapping; + type EVMBridge = (); + type GasToWeight = (); + type SweepOrigin = EnsureRoot; + type OnDust = (); +} + +thread_local! { + pub static TRANSFER_RECORD: RefCell> = RefCell::new(None); + pub static TRANSFER_OK: RefCell = RefCell::new(true); +} + +pub struct MockXcmTransfer; +impl CrowdloanVaultXcm for MockXcmTransfer { + fn transfer_to_liquid_crowdloan_module_account( + vault: AccountId, + recipient: AccountId, + amount: Balance, + ) -> DispatchResult { + if TRANSFER_OK.with(|v| *v.borrow()) { + TRANSFER_RECORD.with(|v| *v.borrow_mut() = Some((vault, recipient, amount))); + Ok(()) + } else { + Err(DispatchError::Other("transfer failed")) + } + } +} + +ord_parameter_types! { + pub const Alice: AccountId = ALICE; +} + +impl liquid_crowdloan::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = Currencies; + type LiquidCrowdloanCurrencyId = Ldot; + type RelayChainCurrencyId = Dot; + type PalletId = LiquidCrowdloanPalletId; + type GovernanceOrigin = EnsureSignedBy; + type CrowdloanVault = CrowdloanVault; + type XcmTransfer = MockXcmTransfer; + type WeightInfo = (); +} + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub enum Runtime where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic + { + System: frame_system::{Pallet, Call, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Tokens: orml_tokens::{Pallet, Storage, Event, Config}, + Currencies: module_currencies::{Pallet, Call, Event}, + LiquidCrowdloan: liquid_crowdloan::{Pallet, Call, Event, Storage}, + } +); + +pub struct ExtBuilder { + balances: Vec<(AccountId, CurrencyId, Balance)>, + transfer_ok: bool, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { + balances: vec![], + transfer_ok: true, + } + } +} + +impl ExtBuilder { + pub fn balances(mut self, balances: Vec<(AccountId, CurrencyId, Balance)>) -> Self { + self.balances = balances; + self + } + + pub fn transfer_ok(mut self, transfer_ok: bool) -> Self { + self.transfer_ok = transfer_ok; + self + } + + pub fn build(self) -> sp_io::TestExternalities { + TRANSFER_RECORD.with(|v| *v.borrow_mut() = None); + TRANSFER_OK.with(|v| *v.borrow_mut() = self.transfer_ok); + + let mut t = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + pallet_balances::GenesisConfig:: { + balances: self + .balances + .clone() + .into_iter() + .filter(|(_, currency_id, _)| *currency_id == ACA) + .map(|(account_id, _, initial_balance)| (account_id, initial_balance)) + .collect::>(), + } + .assimilate_storage(&mut t) + .unwrap(); + + orml_tokens::GenesisConfig:: { + balances: self + .balances + .into_iter() + .filter(|(_, currency_id, _)| *currency_id != ACA) + .collect::>(), + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} diff --git a/modules/liquid-crowdloan/src/tests.rs b/modules/liquid-crowdloan/src/tests.rs new file mode 100644 index 0000000000..abbd929a84 --- /dev/null +++ b/modules/liquid-crowdloan/src/tests.rs @@ -0,0 +1,97 @@ +// 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 . + +//! Unit tests for example module. + +#![cfg(test)] + +use super::*; +use crate::mock::*; +use frame_support::{assert_err, assert_ok}; +use orml_traits::MultiCurrency; +use sp_runtime::traits::BadOrigin; + +#[test] +fn redeem_works() { + ExtBuilder::default() + .balances(vec![(BOB, LDOT, 100), (LiquidCrowdloan::account_id(), DOT, 100)]) + .build() + .execute_with(|| { + assert_ok!(LiquidCrowdloan::redeem(RuntimeOrigin::signed(BOB), 100)); + assert_eq!(Currencies::free_balance(LDOT, &BOB), 0); + assert_eq!(Currencies::free_balance(DOT, &BOB), 100); + assert_eq!(Currencies::free_balance(DOT, &LiquidCrowdloan::account_id()), 0); + System::assert_last_event(RuntimeEvent::LiquidCrowdloan(crate::Event::Redeemed { amount: 100 })); + }); +} + +#[test] +fn redeem_fails_if_not_enough_liquid_crowdloan_token() { + ExtBuilder::default().build().execute_with(|| { + assert_err!( + LiquidCrowdloan::redeem(RuntimeOrigin::signed(BOB), 100), + orml_tokens::Error::::BalanceTooLow + ); + }); +} + +#[test] +fn redeem_fails_if_not_enough_relay_chain_token() { + ExtBuilder::default() + .balances(vec![(BOB, LDOT, 100)]) + .build() + .execute_with(|| { + assert_err!( + LiquidCrowdloan::redeem(RuntimeOrigin::signed(BOB), 100), + orml_tokens::Error::::BalanceTooLow + ); + }); +} + +#[test] +fn transfer_from_crowdloan_vault_works() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(LiquidCrowdloan::transfer_from_crowdloan_vault( + RuntimeOrigin::signed(ALICE), + 100, + )); + System::assert_last_event(RuntimeEvent::LiquidCrowdloan( + crate::Event::TransferFromCrowdloanVaultRequested { amount: 100 }, + )); + }); +} + +#[test] +fn transfer_from_crowdloan_vault_fails_if_not_gov_origin() { + ExtBuilder::default().build().execute_with(|| { + assert_err!( + LiquidCrowdloan::transfer_from_crowdloan_vault(RuntimeOrigin::signed(BOB), 100,), + BadOrigin + ); + }); +} + +#[test] +fn transfer_from_crowdloan_vault_fails_if_sending_xcm_failed() { + ExtBuilder::default().transfer_ok(false).build().execute_with(|| { + assert_err!( + LiquidCrowdloan::transfer_from_crowdloan_vault(RuntimeOrigin::signed(ALICE), 100,), + DispatchError::Other("transfer failed") + ); + }) +} diff --git a/modules/liquid-crowdloan/src/weights.rs b/modules/liquid-crowdloan/src/weights.rs new file mode 100644 index 0000000000..118e470fff --- /dev/null +++ b/modules/liquid-crowdloan/src/weights.rs @@ -0,0 +1,129 @@ +// 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 . + +//! Autogenerated weights for module_liquid_crowdloan +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-06-02, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! HOSTNAME: `ip-172-31-40-233`, CPU: `Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// target/release/acala +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=module_liquid_crowdloan +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./modules/liquid-crowdloan/src/weights.rs +// --template=./templates/module-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for module_liquid_crowdloan. +pub trait WeightInfo { + fn redeem() -> Weight; + fn transfer_from_crowdloan_vault() -> Weight; +} + +/// Weights for module_liquid_crowdloan using the Acala node and recommended hardware. +pub struct AcalaWeight(PhantomData); +impl WeightInfo for AcalaWeight { + // Storage: Tokens Accounts (r:3 w:3) + // Proof: Tokens Accounts (max_values: None, max_size: Some(147), added: 2622, mode: MaxEncodedLen) + // Storage: Tokens TotalIssuance (r:1 w:1) + // Proof: Tokens TotalIssuance (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + // Storage: EvmAccounts EvmAddresses (r:2 w:0) + // Proof: EvmAccounts EvmAddresses (max_values: None, max_size: Some(60), added: 2535, mode: MaxEncodedLen) + // Storage: System Account (r:1 w:1) + // Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + fn redeem() -> Weight { + // Proof Size summary in bytes: + // Measured: `2927` + // Estimated: `22041` + // Minimum execution time: 125_572 nanoseconds. + Weight::from_parts(128_915_000, 22041) + .saturating_add(T::DbWeight::get().reads(7)) + .saturating_add(T::DbWeight::get().writes(5)) + } + // Storage: XcmInterface XcmDestWeightAndFee (r:1 w:0) + // Proof Skipped: XcmInterface XcmDestWeightAndFee (max_values: None, max_size: None, mode: Measured) + // Storage: ParachainInfo ParachainId (r:1 w:0) + // Proof: ParachainInfo ParachainId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + // Storage: ParachainSystem HostConfiguration (r:1 w:0) + // Proof Skipped: ParachainSystem HostConfiguration (max_values: Some(1), max_size: None, mode: Measured) + // Storage: ParachainSystem PendingUpwardMessages (r:1 w:1) + // Proof Skipped: ParachainSystem PendingUpwardMessages (max_values: Some(1), max_size: None, mode: Measured) + fn transfer_from_crowdloan_vault() -> Weight { + // Proof Size summary in bytes: + // Measured: `1255` + // Estimated: `11689` + // Minimum execution time: 46_004 nanoseconds. + Weight::from_parts(46_711_000, 11689) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(1)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + // Storage: Tokens Accounts (r:3 w:3) + // Proof: Tokens Accounts (max_values: None, max_size: Some(147), added: 2622, mode: MaxEncodedLen) + // Storage: Tokens TotalIssuance (r:1 w:1) + // Proof: Tokens TotalIssuance (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + // Storage: EvmAccounts EvmAddresses (r:2 w:0) + // Proof: EvmAccounts EvmAddresses (max_values: None, max_size: Some(60), added: 2535, mode: MaxEncodedLen) + // Storage: System Account (r:1 w:1) + // Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + fn redeem() -> Weight { + // Proof Size summary in bytes: + // Measured: `2927` + // Estimated: `22041` + // Minimum execution time: 125_572 nanoseconds. + Weight::from_parts(128_915_000, 22041) + .saturating_add(RocksDbWeight::get().reads(7)) + .saturating_add(RocksDbWeight::get().writes(5)) + } + // Storage: XcmInterface XcmDestWeightAndFee (r:1 w:0) + // Proof Skipped: XcmInterface XcmDestWeightAndFee (max_values: None, max_size: None, mode: Measured) + // Storage: ParachainInfo ParachainId (r:1 w:0) + // Proof: ParachainInfo ParachainId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + // Storage: ParachainSystem HostConfiguration (r:1 w:0) + // Proof Skipped: ParachainSystem HostConfiguration (max_values: Some(1), max_size: None, mode: Measured) + // Storage: ParachainSystem PendingUpwardMessages (r:1 w:1) + // Proof Skipped: ParachainSystem PendingUpwardMessages (max_values: Some(1), max_size: None, mode: Measured) + fn transfer_from_crowdloan_vault() -> Weight { + // Proof Size summary in bytes: + // Measured: `1255` + // Estimated: `11689` + // Minimum execution time: 46_004 nanoseconds. + Weight::from_parts(46_711_000, 11689) + .saturating_add(RocksDbWeight::get().reads(4)) + .saturating_add(RocksDbWeight::get().writes(1)) + } +} diff --git a/modules/relaychain/src/lib.rs b/modules/relaychain/src/lib.rs index 40bbad7cce..3a03c251cb 100644 --- a/modules/relaychain/src/lib.rs +++ b/modules/relaychain/src/lib.rs @@ -22,30 +22,32 @@ #![cfg_attr(not(feature = "std"), no_std)] #![allow(clippy::unused_unit)] +#![allow(clippy::large_enum_variant)] -use codec::{Decode, Encode, FullCodec}; -use sp_runtime::traits::StaticLookup; +use codec::{Decode, Encode}; +use sp_runtime::traits::{AccountIdLookup, StaticLookup}; use frame_support::{traits::Get, RuntimeDebug}; use module_support::CallBuilder; -use primitives::Balance; +use primitives::{AccountId, Balance}; use sp_std::{boxed::Box, marker::PhantomData, prelude::*}; pub use cumulus_primitives_core::ParaId; use xcm::{prelude::*, v3::Weight as XcmWeight}; -use frame_system::Config; - // * Since XCM V3, relaychain configs 'SafeCallFilter' to filter the call in Transact: // * https://github.com/paritytech/polkadot/blob/master/runtime/polkadot/src/xcm_config.rs #[derive(Encode, Decode, RuntimeDebug)] -pub enum BalancesCall { +pub enum BalancesCall { #[codec(index = 3)] - TransferKeepAlive(::Source, #[codec(compact)] Balance), /* TODO: because param type - * in relaychain is u64, - * need to confirm - * Balance(u128) is working. */ + TransferKeepAlive(::Source, #[codec(compact)] Balance), /* TODO: because + * param type + * in relaychain is + * u64, + * need to confirm + * Balance(u128) is + * working. */ } #[derive(Encode, Decode, RuntimeDebug)] @@ -66,6 +68,31 @@ pub enum StakingCall { WithdrawUnbonded(u32), } +/// `pallet-xcm` calls. +#[derive(Encode, Decode, RuntimeDebug)] +pub enum XcmCall { + /// `reserve_transfer_assets(dest, beneficiary, assets, fee_asset_item)` call. + #[codec(index = 2)] + ReserveTransferAssets( + VersionedMultiLocation, + VersionedMultiLocation, + VersionedMultiAssets, + u32, + ), +} + +// Same to `Polkadot` and `Kusama` runtime `Lookup` config. +pub type RelayChainLookup = AccountIdLookup; + +/// `pallet-proxy` calls. +#[derive(Encode, Decode, RuntimeDebug)] +pub enum ProxyCall { + /// `proxy(real, force_proxy_type, call)` call. Force proxy type is not supported and + /// is always set to `None`. + #[codec(index = 0)] + Proxy(::Source, Option<()>, RelayChainCall), +} + #[cfg(feature = "kusama")] mod kusama { use crate::*; @@ -73,13 +100,17 @@ mod kusama { /// The encoded index correspondes to Kusama's Runtime module configuration. /// https://github.com/paritytech/polkadot/blob/444e96ae34bcec8362f0f947a07bd912b32ca48f/runtime/kusama/src/lib.rs#L1379 #[derive(Encode, Decode, RuntimeDebug)] - pub enum RelayChainCall { + pub enum RelayChainCall { #[codec(index = 4)] - Balances(BalancesCall), + Balances(BalancesCall), #[codec(index = 6)] Staking(StakingCall), #[codec(index = 24)] Utility(Box>), + #[codec(index = 30)] + Proxy(Box>), + #[codec(index = 99)] + XcmPallet(XcmCall), } } @@ -90,13 +121,17 @@ mod polkadot { /// The encoded index correspondes to Polkadot's Runtime module configuration. /// https://github.com/paritytech/polkadot/blob/84a3962e76151ac5ed3afa4ef1e0af829531ab42/runtime/polkadot/src/lib.rs#L1040 #[derive(Encode, Decode, RuntimeDebug)] - pub enum RelayChainCall { + pub enum RelayChainCall { #[codec(index = 5)] - Balances(BalancesCall), + Balances(BalancesCall), #[codec(index = 7)] Staking(StakingCall), #[codec(index = 26)] Utility(Box>), + #[codec(index = 29)] + Proxy(Box>), + #[codec(index = 99)] + XcmPallet(XcmCall), } } @@ -106,16 +141,12 @@ pub use kusama::*; #[cfg(feature = "polkadot")] pub use polkadot::*; -pub struct RelayChainCallBuilder>(PhantomData<(T, ParachainId)>); +pub struct RelayChainCallBuilder>(PhantomData); -impl> CallBuilder for RelayChainCallBuilder -where - T::AccountId: FullCodec, - RelayChainCall: FullCodec, -{ - type AccountId = T::AccountId; +impl> CallBuilder for RelayChainCallBuilder { + type AccountId = AccountId; type Balance = Balance; - type RelayChainCall = RelayChainCall; + type RelayChainCall = RelayChainCall; fn utility_as_derivative_call(call: Self::RelayChainCall, index: u16) -> Self::RelayChainCall { RelayChainCall::Utility(Box::new(UtilityCall::AsDerivative(index, call))) @@ -134,7 +165,7 @@ where } fn balances_transfer_keep_alive(to: Self::AccountId, amount: Self::Balance) -> Self::RelayChainCall { - RelayChainCall::Balances(BalancesCall::TransferKeepAlive(T::Lookup::unlookup(to), amount)) + RelayChainCall::Balances(BalancesCall::TransferKeepAlive(RelayChainLookup::unlookup(to), amount)) } fn finalize_call_into_xcm_message( @@ -208,4 +239,22 @@ where ] .concat()) } + + fn xcm_pallet_reserve_transfer_assets( + dest: MultiLocation, + beneficiary: MultiLocation, + assets: MultiAssets, + fee_assets_item: u32, + ) -> Self::RelayChainCall { + RelayChainCall::XcmPallet(XcmCall::ReserveTransferAssets( + dest.into_versioned(), + beneficiary.into_versioned(), + assets.into(), + fee_assets_item, + )) + } + + fn proxy_call(real: Self::AccountId, call: Self::RelayChainCall) -> Self::RelayChainCall { + RelayChainCall::Proxy(Box::new(ProxyCall::Proxy(RelayChainLookup::unlookup(real), None, call))) + } } diff --git a/modules/support/src/lib.rs b/modules/support/src/lib.rs index 6482dd390a..d116b0fde6 100644 --- a/modules/support/src/lib.rs +++ b/modules/support/src/lib.rs @@ -36,6 +36,7 @@ pub mod evm; pub mod homa; pub mod honzon; pub mod incentives; +pub mod liquid_crowdloan; pub mod mocks; pub mod stable_asset; @@ -45,6 +46,7 @@ pub use crate::evm::*; pub use crate::homa::*; pub use crate::honzon::*; pub use crate::incentives::*; +pub use crate::liquid_crowdloan::*; pub use crate::stable_asset::*; pub type Price = FixedU128; @@ -147,6 +149,25 @@ pub trait CallBuilder { calls: Vec<(Self::RelayChainCall, XcmWeight)>, extra_fee: Self::Balance, ) -> Xcm<()>; + + /// Reserve transfer assets. + /// params: + /// - dest: The destination chain. + /// - beneficiary: The beneficiary. + /// - assets: The assets to be transferred. + /// - fee_assets_item: The index of assets for fees. + fn xcm_pallet_reserve_transfer_assets( + dest: MultiLocation, + beneficiary: MultiLocation, + assets: MultiAssets, + fee_assets_item: u32, + ) -> Self::RelayChainCall; + + /// Proxy a call with a `real` account without a forced proxy type. + /// params: + /// - real: The real account. + /// - call: The call to be executed. + fn proxy_call(real: Self::AccountId, call: Self::RelayChainCall) -> Self::RelayChainCall; } /// Dispatchable tasks diff --git a/modules/support/src/liquid_crowdloan.rs b/modules/support/src/liquid_crowdloan.rs new file mode 100644 index 0000000000..79d7b3df46 --- /dev/null +++ b/modules/support/src/liquid_crowdloan.rs @@ -0,0 +1,29 @@ +// 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 sp_runtime::DispatchResult; + +/// Crowdloan vault XCM operations. +pub trait CrowdloanVaultXcm { + /// Cross-chain transfer DOT from crowdloan vault to liquid crowdloan module account. + fn transfer_to_liquid_crowdloan_module_account( + vault: AccountId, + recipient: AccountId, + amount: Balance, + ) -> DispatchResult; +} diff --git a/modules/xcm-interface/src/lib.rs b/modules/xcm-interface/src/lib.rs index 1f8815fc6e..d5322d0a02 100644 --- a/modules/xcm-interface/src/lib.rs +++ b/modules/xcm-interface/src/lib.rs @@ -27,9 +27,9 @@ #![cfg_attr(not(feature = "std"), no_std)] #![allow(clippy::unused_unit)] -use frame_support::{log, pallet_prelude::*, transactional}; +use frame_support::{log, pallet_prelude::*, traits::Get, transactional}; use frame_system::pallet_prelude::*; -use module_support::{CallBuilder, HomaSubAccountXcm}; +use module_support::{CallBuilder, CrowdloanVaultXcm, HomaSubAccountXcm}; use orml_traits::XcmTransfer; use primitives::{Balance, CurrencyId, EraIndex}; use scale_info::TypeInfo; @@ -57,6 +57,8 @@ pub mod module { HomaUnbond, // Parachain fee with location info ParachainFee(Box), + // `XcmPallet::reserve_transfer_assets` call via proxy account + ProxyReserveTransferAssets, } #[pallet::config] @@ -87,6 +89,13 @@ pub mod module { /// The interface to Cross-chain transfer. type XcmTransfer: XcmTransfer; + + /// Self parachain location. + #[pallet::constant] + type SelfLocation: Get; + + /// Convert AccountId to MultiLocation to build XCM message. + type AccountIdToMultiLocation: Convert; } #[pallet::error] @@ -277,4 +286,39 @@ pub mod module { Self::xcm_dest_weight_and_fee(XcmInterfaceOperation::ParachainFee(Box::new(location))).1 } } + + impl CrowdloanVaultXcm for Pallet { + fn transfer_to_liquid_crowdloan_module_account( + vault: T::AccountId, + recipient: T::AccountId, + amount: Balance, + ) -> DispatchResult { + let (xcm_dest_weight, xcm_fee) = + Self::xcm_dest_weight_and_fee(XcmInterfaceOperation::ProxyReserveTransferAssets); + + let proxy_call = T::RelayChainCallBuilder::proxy_call( + vault.clone(), + T::RelayChainCallBuilder::xcm_pallet_reserve_transfer_assets( + T::SelfLocation::get(), + T::AccountIdToMultiLocation::convert(recipient.clone()), + // Note this message is executed in the relay chain context. + vec![(Concrete(Here.into()), amount).into()].into(), + 0, + ), + ); + let xcm_message = + T::RelayChainCallBuilder::finalize_call_into_xcm_message(proxy_call, xcm_fee, xcm_dest_weight); + + let result = pallet_xcm::Pallet::::send_xcm(Here, Parent, xcm_message); + log::debug!( + target: "xcm-interface", + "Send {:?} planck DOT from crowdloan vault {:?} to {:?}, result: {:?}", + amount, vault, recipient, result, + ); + + ensure!(result.is_ok(), Error::::XcmFailed); + + Ok(()) + } + } } diff --git a/modules/xcm-interface/src/mock.rs b/modules/xcm-interface/src/mock.rs index f94bd2859a..03142a2fd0 100644 --- a/modules/xcm-interface/src/mock.rs +++ b/modules/xcm-interface/src/mock.rs @@ -131,6 +131,7 @@ parameter_types! { pub const GetStakingCurrencyId: CurrencyId = DOT; pub const ParachainAccount: AccountId = AccountId32::new([0u8; 32]); pub const ParachainId: module_relaychain::ParaId = module_relaychain::ParaId::new(2000); + pub SelfLocation: MultiLocation = MultiLocation::new(1, X1(Parachain(ParachainId::get().into()))); } pub struct SubAccountIndexMultiLocationConvertor; @@ -205,6 +206,17 @@ impl XcmTransfer for MockXcmTransfer { } } +pub struct AccountIdToMultiLocation; +impl Convert for AccountIdToMultiLocation { + fn convert(account: AccountId) -> MultiLocation { + X1(Junction::AccountId32 { + network: None, + id: account.into(), + }) + .into() + } +} + impl Config for Runtime { type RuntimeEvent = RuntimeEvent; type UpdateOrigin = EnsureSignedBy; @@ -212,8 +224,10 @@ impl Config for Runtime { type ParachainAccount = ParachainAccount; type RelayChainUnbondingSlashingSpans = ConstU32<28>; type SovereignSubAccountLocationConvert = SubAccountIndexMultiLocationConvertor; - type RelayChainCallBuilder = module_relaychain::RelayChainCallBuilder; + type RelayChainCallBuilder = module_relaychain::RelayChainCallBuilder; type XcmTransfer = MockXcmTransfer; + type SelfLocation = SelfLocation; + type AccountIdToMultiLocation = AccountIdToMultiLocation; } type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; diff --git a/runtime/acala/Cargo.toml b/runtime/acala/Cargo.toml index 0b126b493b..1b281fdd35 100644 --- a/runtime/acala/Cargo.toml +++ b/runtime/acala/Cargo.toml @@ -117,6 +117,7 @@ module-session-manager = { path = "../../modules/session-manager", default-featu module-relaychain = { path = "../../modules/relaychain", default-features = false, features = ["polkadot"] } module-idle-scheduler = { path = "../../modules/idle-scheduler", default-features = false } module-aggregated-dex = { path = "../../modules/aggregated-dex", default-features = false } +module-liquid-crowdloan = { path = "../../modules/liquid-crowdloan", default-features = false } primitives = { package = "acala-primitives", path = "../../primitives", default-features = false } runtime-common = { path = "../common", default-features = false } @@ -248,6 +249,7 @@ std = [ "module-transaction-pause/std", "module-transaction-payment/std", "module-xcm-interface/std", + "module-liquid-crowdloan/std", "primitives/std", "runtime-common/std", @@ -382,6 +384,7 @@ try-runtime = [ "module-transaction-pause/try-runtime", "module-transaction-payment/try-runtime", "module-xcm-interface/try-runtime", + "module-liquid-crowdloan/try-runtime", "primitives/try-runtime", diff --git a/runtime/acala/src/benchmarking/mod.rs b/runtime/acala/src/benchmarking/mod.rs index ffe02f5481..fe436e52a8 100644 --- a/runtime/acala/src/benchmarking/mod.rs +++ b/runtime/acala/src/benchmarking/mod.rs @@ -83,6 +83,9 @@ pub mod transaction_payment { pub mod session_manager { include!("../../../mandala/src/benchmarking/session_manager.rs"); } +pub mod liquid_crowdloan { + include!("../../../mandala/src/benchmarking/liquid_crowdloan.rs"); +} pub mod nutsfinance_stable_asset { include!("../../../mandala/src/benchmarking/nutsfinance_stable_asset.rs"); diff --git a/runtime/acala/src/lib.rs b/runtime/acala/src/lib.rs index 1f9e250a5f..6bba48b2ad 100644 --- a/runtime/acala/src/lib.rs +++ b/runtime/acala/src/lib.rs @@ -170,6 +170,7 @@ parameter_types! { // This Pallet is only used to payment fee pool, it's not added to whitelist by design. // because transaction payment pallet will ensure the accounts always have enough ED. pub const TransactionPaymentPalletId: PalletId = PalletId(*b"aca/fees"); + pub const LiquidCrowdloanPalletId: PalletId = PalletId(*b"aca/lqcl"); pub const StableAssetPalletId: PalletId = PalletId(*b"nuts/sta"); } @@ -1535,8 +1536,10 @@ impl module_xcm_interface::Config for Runtime { type ParachainAccount = ParachainAccount; type RelayChainUnbondingSlashingSpans = ConstU32<5>; type SovereignSubAccountLocationConvert = SubAccountIndexMultiLocationConvertor; - type RelayChainCallBuilder = RelayChainCallBuilder; + type RelayChainCallBuilder = RelayChainCallBuilder; type XcmTransfer = XTokens; + type SelfLocation = xcm_config::SelfLocation; + type AccountIdToMultiLocation = xcm_config::AccountIdToMultiLocation; } impl orml_unknown_tokens::Config for Runtime { @@ -1649,6 +1652,24 @@ impl nutsfinance_stable_asset::Config for Runtime { type EnsurePoolAssetId = EnsurePoolAssetId; } +parameter_types!( + // Crowdloan vault address: `132zsjMwGjNaUXF5XjUCDs2cDEq9Qao51TsL9RSUTGZbinVK` + pub CrowdloanVault: AccountId = AccountId::from(hex_literal::hex!("59fe89295c2e57d7b4d4d8be9e00a3802e513703ab4b5b424ed0a646e899d3c9")); + pub const LiquidCrowdloanCurrencyId: CurrencyId = LCDOT; +); + +impl module_liquid_crowdloan::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = Currencies; + type LiquidCrowdloanCurrencyId = LiquidCrowdloanCurrencyId; + type RelayChainCurrencyId = GetStakingCurrencyId; + type PalletId = LiquidCrowdloanPalletId; + type GovernanceOrigin = EnsureRootOrHalfGeneralCouncil; + type CrowdloanVault = CrowdloanVault; + type XcmTransfer = XcmInterface; + type WeightInfo = weights::module_liquid_crowdloan::WeightInfo; +} + construct_runtime!( pub enum Runtime where Block = Block, @@ -1744,6 +1765,7 @@ construct_runtime!( Incentives: module_incentives = 120, NFT: module_nft = 121, AssetRegistry: module_asset_registry = 122, + LiquidCrowdloan: module_liquid_crowdloan = 123, // Smart contracts EVM: module_evm = 130, @@ -1850,6 +1872,7 @@ mod benches { [nutsfinance_stable_asset, benchmarking::nutsfinance_stable_asset] [module_idle_scheduler, benchmarking::idle_scheduler] [module_aggregated_dex, benchmarking::aggregated_dex] + [module_liquid_crowdloan, benchmarking::liquid_crowdloan] ); } diff --git a/runtime/acala/src/weights/mod.rs b/runtime/acala/src/weights/mod.rs index 21e9d526f3..34f40f4322 100644 --- a/runtime/acala/src/weights/mod.rs +++ b/runtime/acala/src/weights/mod.rs @@ -34,6 +34,7 @@ pub mod module_evm_accounts; pub mod module_homa; pub mod module_honzon; pub mod module_incentives; +pub mod module_liquid_crowdloan; pub mod module_nft; pub mod module_prices; pub mod module_session_manager; diff --git a/runtime/acala/src/weights/module_liquid_crowdloan.rs b/runtime/acala/src/weights/module_liquid_crowdloan.rs new file mode 100644 index 0000000000..6d97f9ea05 --- /dev/null +++ b/runtime/acala/src/weights/module_liquid_crowdloan.rs @@ -0,0 +1,91 @@ +// 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 . + +//! Autogenerated weights for module_liquid_crowdloan +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-06-02, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! HOSTNAME: `ip-172-31-34-64`, CPU: `Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("acala-dev"), DB CACHE: 1024 + +// Executed Command: +// target/production/acala +// benchmark +// pallet +// --chain=acala-dev +// --steps=50 +// --repeat=20 +// --pallet=module_liquid_crowdloan +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --template=./templates/runtime-weight-template.hbs +// --output=./runtime/acala/src/weights/ + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::Weight}; +use sp_std::marker::PhantomData; + +/// Weight functions for module_liquid_crowdloan. +pub struct WeightInfo(PhantomData); +impl module_liquid_crowdloan::WeightInfo for WeightInfo { + // Storage: Tokens Accounts (r:3 w:3) + // Proof: Tokens Accounts (max_values: None, max_size: Some(147), added: 2622, mode: MaxEncodedLen) + // Storage: Tokens TotalIssuance (r:1 w:1) + // Proof: Tokens TotalIssuance (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + // Storage: EvmAccounts EvmAddresses (r:2 w:0) + // Proof: EvmAccounts EvmAddresses (max_values: None, max_size: Some(60), added: 2535, mode: MaxEncodedLen) + // Storage: System Account (r:1 w:1) + // Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + fn redeem() -> Weight { + // Proof Size summary in bytes: + // Measured: `1883` + // Estimated: `22041` + // Minimum execution time: 84_404 nanoseconds. + Weight::from_parts(86_781_000, 22041) + .saturating_add(T::DbWeight::get().reads(7)) + .saturating_add(T::DbWeight::get().writes(5)) + } + // Storage: XcmInterface XcmDestWeightAndFee (r:1 w:0) + // Proof Skipped: XcmInterface XcmDestWeightAndFee (max_values: None, max_size: None, mode: Measured) + // Storage: ParachainInfo ParachainId (r:1 w:0) + // Proof: ParachainInfo ParachainId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + // Storage: PolkadotXcm SupportedVersion (r:1 w:0) + // Proof Skipped: PolkadotXcm SupportedVersion (max_values: None, max_size: None, mode: Measured) + // Storage: PolkadotXcm VersionDiscoveryQueue (r:1 w:1) + // Proof Skipped: PolkadotXcm VersionDiscoveryQueue (max_values: Some(1), max_size: None, mode: Measured) + // Storage: PolkadotXcm SafeXcmVersion (r:1 w:0) + // Proof Skipped: PolkadotXcm SafeXcmVersion (max_values: Some(1), max_size: None, mode: Measured) + // Storage: ParachainSystem HostConfiguration (r:1 w:0) + // Proof Skipped: ParachainSystem HostConfiguration (max_values: Some(1), max_size: None, mode: Measured) + // Storage: ParachainSystem PendingUpwardMessages (r:1 w:1) + // Proof Skipped: ParachainSystem PendingUpwardMessages (max_values: Some(1), max_size: None, mode: Measured) + fn transfer_from_crowdloan_vault() -> Weight { + // Proof Size summary in bytes: + // Measured: `1392` + // Estimated: `22711` + // Minimum execution time: 56_316 nanoseconds. + Weight::from_parts(57_458_000, 22711) + .saturating_add(T::DbWeight::get().reads(7)) + .saturating_add(T::DbWeight::get().writes(2)) + } +} diff --git a/runtime/integration-tests/src/relaychain/kusama_cross_chain_transfer.rs b/runtime/integration-tests/src/relaychain/kusama_cross_chain_transfer.rs index df4b295473..a82e92e610 100644 --- a/runtime/integration-tests/src/relaychain/kusama_cross_chain_transfer.rs +++ b/runtime/integration-tests/src/relaychain/kusama_cross_chain_transfer.rs @@ -856,11 +856,9 @@ fn unspent_xcm_fee_is_returned_correctly() { Karura::execute_with(|| { // Construct a transfer XCM call with returning the deposit - let transfer_call = RelayChainCallBuilder::::balances_transfer_keep_alive( - AccountId::from(BOB), - dollar_n, - ); - let batch_call = RelayChainCallBuilder::::utility_as_derivative_call(transfer_call, 0); + let transfer_call = + RelayChainCallBuilder::::balances_transfer_keep_alive(AccountId::from(BOB), dollar_n); + let batch_call = RelayChainCallBuilder::::utility_as_derivative_call(transfer_call, 0); let weight = XcmWeight::from_parts(10_000_000_000, 0); // Fee to transfer into the hold register let asset = MultiAsset { @@ -899,12 +897,10 @@ fn unspent_xcm_fee_is_returned_correctly() { Karura::execute_with(|| { // Construct a transfer using the RelaychainCallBuilder - let transfer_call = RelayChainCallBuilder::::balances_transfer_keep_alive( - AccountId::from(BOB), - dollar_n, - ); - let batch_call = RelayChainCallBuilder::::utility_as_derivative_call(transfer_call, 0); - let finalized_call = RelayChainCallBuilder::::finalize_call_into_xcm_message( + let transfer_call = + RelayChainCallBuilder::::balances_transfer_keep_alive(AccountId::from(BOB), dollar_n); + let batch_call = RelayChainCallBuilder::::utility_as_derivative_call(transfer_call, 0); + let finalized_call = RelayChainCallBuilder::::finalize_call_into_xcm_message( batch_call, dollar_n, XcmWeight::from_parts(10_000_000_000, 0), @@ -938,7 +934,7 @@ fn trapped_asset() -> MultiAsset { }; Karura::execute_with(|| { - let transfer_call = RelayChainCallBuilder::::balances_transfer_keep_alive( + let transfer_call = RelayChainCallBuilder::::balances_transfer_keep_alive( AccountId::from(BOB), dollar(NATIVE_CURRENCY), ); diff --git a/runtime/integration-tests/src/relaychain/liquid_crowdloan.rs b/runtime/integration-tests/src/relaychain/liquid_crowdloan.rs new file mode 100644 index 0000000000..7639f8f578 --- /dev/null +++ b/runtime/integration-tests/src/relaychain/liquid_crowdloan.rs @@ -0,0 +1,98 @@ +// 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 crate::relaychain::polkadot_test_net::*; +use crate::setup::*; + +use frame_support::assert_ok; +use module_xcm_interface::XcmInterfaceOperation; +use sp_runtime::traits::StaticLookup; +use xcm_emulator::TestExt; + +const ACALA_PARA_ID: u32 = 2000; + +//TODO: Enable after Polkadot runtime allows XCM proxy calls: +// https://github.com/paritytech/polkadot/blob/5c554b95e223b507a9b7e420e2cdee06e0982ab0/runtime/polkadot/src/xcm_config.rs#L167 +#[ignore = "polkadot runtime does not allow XCM proxy calls"] +#[test] +fn transfer_from_crowdloan_vault_works() { + TestNet::reset(); + + let vault = acala_runtime::CrowdloanVault::get(); + let module_liquid_crowdloan_account = acala_runtime::LiquidCrowdloan::account_id(); + let acala_sovereign_account: AccountId = ParaId::from(ACALA_PARA_ID).into_account_truncating(); + + PolkadotNet::execute_with(|| { + use polkadot_runtime::{Balances, Proxy, ProxyType, Runtime, RuntimeOrigin}; + + let _ = Balances::deposit_creating(&vault, dollar(DOT) * 100); + + assert_ok!(Proxy::add_proxy( + RuntimeOrigin::signed(vault.clone()), + ::Lookup::unlookup(acala_sovereign_account.clone()), + ProxyType::Any, + 0 + )); + + // NOTE: the following code is to help debugging via duplicating the XCM transact in + // Polkadot runtime. Feel free to delete it after Polkadot runtime allows XCM proxy calls + // and the test can be enabled. + + // let call = RuntimeCall::XcmPallet(pallet_xcm::Call::reserve_transfer_assets { + // dest: Box::new(Parachain(2000).into_versioned()), + // beneficiary: Box::new( + // Junction::AccountId32 { + // id: module_liquid_crowdloan_account.clone().into(), + // network: None + // } + // .into_versioned() + // ), + // assets: Box::new((Here, dollar(DOT)).into()), + // fee_asset_item: 0, + // }); + // assert_ok!(Proxy::proxy( + // RuntimeOrigin::signed(acala_sovereign_account.clone()), + // ::Lookup::unlookup(vault.clone()), + // None, + // Box::new(call), + // )); + }); + + Acala::execute_with(|| { + use acala_runtime::{LiquidCrowdloan, RuntimeOrigin, XcmInterface}; + + assert_ok!(XcmInterface::update_xcm_dest_weight_and_fee( + RuntimeOrigin::root(), + vec![( + XcmInterfaceOperation::ProxyReserveTransferAssets, + Some(XcmWeight::from_parts(20_000_000_000, 0)), + Some(100_000_000_000) + )] + )); + + assert_ok!(LiquidCrowdloan::transfer_from_crowdloan_vault( + RuntimeOrigin::root(), + dollar(DOT), + )); + + assert_eq!( + Tokens::free_balance(DOT, &module_liquid_crowdloan_account), + 9_998_397_440, + ); + }); +} diff --git a/runtime/integration-tests/src/relaychain/mod.rs b/runtime/integration-tests/src/relaychain/mod.rs index c000d561d4..c5d2f4b3e6 100644 --- a/runtime/integration-tests/src/relaychain/mod.rs +++ b/runtime/integration-tests/src/relaychain/mod.rs @@ -27,6 +27,8 @@ pub mod kusama_test_net; #[cfg(feature = "with-karura-runtime")] mod statemine; +#[cfg(feature = "with-acala-runtime")] +mod liquid_crowdloan; #[cfg(feature = "with-acala-runtime")] mod polkadot_cross_chain_transfer; #[cfg(feature = "with-acala-runtime")] diff --git a/runtime/integration-tests/src/relaychain/relay_chain.rs b/runtime/integration-tests/src/relaychain/relay_chain.rs index be3067513d..4967c7f539 100644 --- a/runtime/integration-tests/src/relaychain/relay_chain.rs +++ b/runtime/integration-tests/src/relaychain/relay_chain.rs @@ -32,7 +32,7 @@ mod karura_tests { use pallet_staking::StakingLedger; use xcm_emulator::TestExt; - type KusamaCallBuilder = RelayChainCallBuilder; + type KusamaCallBuilder = RelayChainCallBuilder; #[test] /// Tests the staking_withdraw_unbonded call. diff --git a/runtime/karura/src/lib.rs b/runtime/karura/src/lib.rs index 9a59f23bb1..33c2a03961 100644 --- a/runtime/karura/src/lib.rs +++ b/runtime/karura/src/lib.rs @@ -1549,8 +1549,10 @@ impl module_xcm_interface::Config for Runtime { type ParachainAccount = ParachainAccount; type RelayChainUnbondingSlashingSpans = ConstU32<5>; type SovereignSubAccountLocationConvert = SubAccountIndexMultiLocationConvertor; - type RelayChainCallBuilder = RelayChainCallBuilder; + type RelayChainCallBuilder = RelayChainCallBuilder; type XcmTransfer = XTokens; + type SelfLocation = xcm_config::SelfLocation; + type AccountIdToMultiLocation = xcm_config::AccountIdToMultiLocation; } impl orml_unknown_tokens::Config for Runtime { diff --git a/runtime/mandala/Cargo.toml b/runtime/mandala/Cargo.toml index 35decd564a..308fe4bd00 100644 --- a/runtime/mandala/Cargo.toml +++ b/runtime/mandala/Cargo.toml @@ -125,6 +125,7 @@ module-session-manager = { path = "../../modules/session-manager", default-featu module-relaychain = { path = "../../modules/relaychain", default-features = false, features = ["polkadot"]} module-idle-scheduler = { path = "../../modules/idle-scheduler", default-features = false } module-aggregated-dex = { path = "../../modules/aggregated-dex", default-features = false } +module-liquid-crowdloan = { path = "../../modules/liquid-crowdloan", default-features = false } primitives = { package = "acala-primitives", path = "../../primitives", default-features = false } runtime-common = { path = "../common", default-features = false } @@ -270,6 +271,7 @@ std = [ "module-transaction-pause/std", "module-transaction-payment/std", "module-xcm-interface/std", + "module-liquid-crowdloan/std", "primitives/std", "runtime-common/std", @@ -418,6 +420,7 @@ try-runtime = [ "module-transaction-pause/try-runtime", "module-transaction-payment/try-runtime", "module-xcm-interface/try-runtime", + "module-liquid-crowdloan/try-runtime", "primitives/try-runtime", diff --git a/runtime/mandala/src/benchmarking/liquid_crowdloan.rs b/runtime/mandala/src/benchmarking/liquid_crowdloan.rs new file mode 100644 index 0000000000..7b7017dee3 --- /dev/null +++ b/runtime/mandala/src/benchmarking/liquid_crowdloan.rs @@ -0,0 +1,56 @@ +// 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 crate::{AccountId, LiquidCrowdloan, LiquidCrowdloanCurrencyId, PolkadotXcm, Runtime, RuntimeOrigin, System}; + +use super::utils::{set_balance, STAKING}; +use frame_benchmarking::whitelisted_caller; +use frame_system::RawOrigin; +use orml_benchmarking::runtime_benchmarks; +use sp_std::prelude::*; + +runtime_benchmarks! { + { Runtime, module_liquid_crowdloan } + + redeem { + let caller: AccountId = whitelisted_caller(); + let amount = 100_000_000_000_000; + set_balance(LiquidCrowdloanCurrencyId::get(), &caller, amount); + set_balance(STAKING, &LiquidCrowdloan::account_id(), amount); + }: _(RawOrigin::Signed(caller), amount) + verify { + System::assert_last_event(module_liquid_crowdloan::Event::Redeemed { amount }.into()); + } + + transfer_from_crowdloan_vault { + PolkadotXcm::force_default_xcm_version(RuntimeOrigin::root(), Some(2)).unwrap(); + let amount = 1_000; + }: _(RawOrigin::Root, amount) + verify { + System::assert_last_event(module_liquid_crowdloan::Event::TransferFromCrowdloanVaultRequested { amount }.into()); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::benchmarking::utils::tests::new_test_ext; + use orml_benchmarking::impl_benchmark_test_suite; + + impl_benchmark_test_suite!(new_test_ext(),); +} diff --git a/runtime/mandala/src/benchmarking/mod.rs b/runtime/mandala/src/benchmarking/mod.rs index 4a02a3db02..35a42d33e7 100644 --- a/runtime/mandala/src/benchmarking/mod.rs +++ b/runtime/mandala/src/benchmarking/mod.rs @@ -43,6 +43,7 @@ pub mod homa; pub mod honzon; pub mod idle_scheduler; pub mod incentives; +pub mod liquid_crowdloan; pub mod nominees_election; pub mod nutsfinance_stable_asset; pub mod prices; diff --git a/runtime/mandala/src/lib.rs b/runtime/mandala/src/lib.rs index 94af33d604..d628b4ed42 100644 --- a/runtime/mandala/src/lib.rs +++ b/runtime/mandala/src/lib.rs @@ -113,7 +113,7 @@ pub use runtime_common::{ GeneralCouncilInstance, GeneralCouncilMembershipInstance, HomaCouncilInstance, HomaCouncilMembershipInstance, MaxTipsOfPriority, OffchainSolutionWeightLimit, OperationalFeeMultiplier, OperatorMembershipInstanceAcala, Price, ProxyType, Rate, Ratio, RuntimeBlockLength, RuntimeBlockWeights, SystemContractsFilter, TechnicalCommitteeInstance, - TechnicalCommitteeMembershipInstance, TimeStampedPrice, TipPerWeightStep, ACA, AUSD, DOT, KSM, LDOT, + TechnicalCommitteeMembershipInstance, TimeStampedPrice, TipPerWeightStep, ACA, AUSD, DOT, KSM, LCDOT, LDOT, }; pub use xcm::{prelude::*, v3::Weight as XcmWeight}; @@ -180,6 +180,7 @@ parameter_types! { // This Pallet is only used to payment fee pool, it's not added to whitelist by design. // because transaction payment pallet will ensure the accounts always have enough ED. pub const TransactionPaymentPalletId: PalletId = PalletId(*b"aca/fees"); + pub const LiquidCrowdloanPalletId: PalletId = PalletId(*b"aca/lqcl"); // Ecosystem modules pub const StableAssetPalletId: PalletId = PalletId(*b"nuts/sta"); // lock identifier for earning module @@ -1401,8 +1402,10 @@ impl module_xcm_interface::Config for Runtime { type ParachainAccount = ParachainAccount; type RelayChainUnbondingSlashingSpans = ConstU32<5>; type SovereignSubAccountLocationConvert = SubAccountIndexMultiLocationConvertor; - type RelayChainCallBuilder = RelayChainCallBuilder; + type RelayChainCallBuilder = RelayChainCallBuilder; type XcmTransfer = XTokens; + type SelfLocation = xcm_config::SelfLocation; + type AccountIdToMultiLocation = xcm_config::AccountIdToMultiLocation; } parameter_types! { @@ -1767,6 +1770,24 @@ impl module_idle_scheduler::Config for Runtime { impl cumulus_pallet_aura_ext::Config for Runtime {} +parameter_types!( + // Crowdloan vault address: `132zsjMwGjNaUXF5XjUCDs2cDEq9Qao51TsL9RSUTGZbinVK` + pub CrowdloanVault: AccountId = AccountId::from(hex_literal::hex!("59fe89295c2e57d7b4d4d8be9e00a3802e513703ab4b5b424ed0a646e899d3c9")); + pub const LiquidCrowdloanCurrencyId: CurrencyId = LCDOT; +); + +impl module_liquid_crowdloan::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = Currencies; + type LiquidCrowdloanCurrencyId = LiquidCrowdloanCurrencyId; + type RelayChainCurrencyId = GetStakingCurrencyId; + type PalletId = LiquidCrowdloanPalletId; + type GovernanceOrigin = EnsureRootOrHalfGeneralCouncil; + type CrowdloanVault = CrowdloanVault; + type XcmTransfer = XcmInterface; + type WeightInfo = weights::module_liquid_crowdloan::WeightInfo; +} + #[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug)] pub struct ConvertEthereumTx; @@ -2039,6 +2060,7 @@ construct_runtime!( Incentives: module_incentives = 140, NFT: module_nft = 141, AssetRegistry: module_asset_registry = 142, + LiquidCrowdloan: module_liquid_crowdloan = 143, // Parachain ParachainInfo: parachain_info exclude_parts { Call } = 161, @@ -2107,6 +2129,7 @@ mod benches { [module_evm_accounts, benchmarking::evm_accounts] [module_currencies, benchmarking::currencies] [module_session_manager, benchmarking::session_manager] + [module_liquid_crowdloan, benchmarking::liquid_crowdloan] [orml_tokens, benchmarking::tokens] [orml_vesting, benchmarking::vesting] [orml_auction, benchmarking::auction] diff --git a/runtime/mandala/src/weights/mod.rs b/runtime/mandala/src/weights/mod.rs index ad441a23fe..204071158c 100644 --- a/runtime/mandala/src/weights/mod.rs +++ b/runtime/mandala/src/weights/mod.rs @@ -36,6 +36,7 @@ pub mod module_homa; pub mod module_honzon; pub mod module_idle_scheduler; pub mod module_incentives; +pub mod module_liquid_crowdloan; pub mod module_nft; pub mod module_nominees_election; pub mod module_prices; diff --git a/runtime/mandala/src/weights/module_liquid_crowdloan.rs b/runtime/mandala/src/weights/module_liquid_crowdloan.rs new file mode 100644 index 0000000000..c5d5cc3491 --- /dev/null +++ b/runtime/mandala/src/weights/module_liquid_crowdloan.rs @@ -0,0 +1,85 @@ +// 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 . + +//! Autogenerated weights for module_liquid_crowdloan +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-06-02, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! HOSTNAME: `ip-172-31-35-142`, CPU: `Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// target/production/acala +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=module_liquid_crowdloan +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --template=./templates/runtime-weight-template.hbs +// --output=./runtime/mandala/src/weights/ + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::Weight}; +use sp_std::marker::PhantomData; + +/// Weight functions for module_liquid_crowdloan. +pub struct WeightInfo(PhantomData); +impl module_liquid_crowdloan::WeightInfo for WeightInfo { + // Storage: Tokens Accounts (r:3 w:3) + // Proof: Tokens Accounts (max_values: None, max_size: Some(147), added: 2622, mode: MaxEncodedLen) + // Storage: Tokens TotalIssuance (r:1 w:1) + // Proof: Tokens TotalIssuance (max_values: None, max_size: Some(67), added: 2542, mode: MaxEncodedLen) + // Storage: EvmAccounts EvmAddresses (r:2 w:0) + // Proof: EvmAccounts EvmAddresses (max_values: None, max_size: Some(60), added: 2535, mode: MaxEncodedLen) + // Storage: System Account (r:1 w:1) + // Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + fn redeem() -> Weight { + // Proof Size summary in bytes: + // Measured: `2927` + // Estimated: `22041` + // Minimum execution time: 86_706 nanoseconds. + Weight::from_parts(88_388_000, 22041) + .saturating_add(T::DbWeight::get().reads(7)) + .saturating_add(T::DbWeight::get().writes(5)) + } + // Storage: XcmInterface XcmDestWeightAndFee (r:1 w:0) + // Proof Skipped: XcmInterface XcmDestWeightAndFee (max_values: None, max_size: None, mode: Measured) + // Storage: ParachainInfo ParachainId (r:1 w:0) + // Proof: ParachainInfo ParachainId (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + // Storage: ParachainSystem HostConfiguration (r:1 w:0) + // Proof Skipped: ParachainSystem HostConfiguration (max_values: Some(1), max_size: None, mode: Measured) + // Storage: ParachainSystem PendingUpwardMessages (r:1 w:1) + // Proof Skipped: ParachainSystem PendingUpwardMessages (max_values: Some(1), max_size: None, mode: Measured) + fn transfer_from_crowdloan_vault() -> Weight { + // Proof Size summary in bytes: + // Measured: `1255` + // Estimated: `11689` + // Minimum execution time: 32_468 nanoseconds. + Weight::from_parts(33_256_000, 11689) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(1)) + } +}