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