diff --git a/pallets/liquidity-pools/src/lib.rs b/pallets/liquidity-pools/src/lib.rs index b6057cd7d0..b0f76a0a19 100644 --- a/pallets/liquidity-pools/src/lib.rs +++ b/pallets/liquidity-pools/src/lib.rs @@ -739,33 +739,8 @@ pub mod pallet { Error::::NotPoolAdmin ); - // Ensure currency is allowed as payment and payout currency for pool - let invest_id = Self::derive_invest_id(pool_id, tranche_id)?; - // Required for increasing and collecting investments - ensure!( - T::ForeignInvestment::accepted_payment_currency(invest_id.clone(), currency_id), - Error::::InvalidPaymentCurrency - ); - // Required for decreasing investments as well as increasing, decreasing and - // collecting redemptions - ensure!( - T::ForeignInvestment::accepted_payout_currency(invest_id, currency_id), - Error::::InvalidPayoutCurrency - ); - - // Ensure the currency is enabled as pool_currency - let metadata = - T::AssetRegistry::metadata(¤cy_id).ok_or(Error::::AssetNotFound)?; - ensure!( - metadata.additional.pool_currency, - Error::::AssetMetadataNotPoolCurrency - ); - - // Derive GeneralIndex for currency - let currency = Self::try_get_general_index(currency_id)?; - - let LiquidityPoolsWrappedToken::EVM { chain_id, .. } = - Self::try_get_wrapped_token(¤cy_id)?; + let (currency, chain_id) = + Self::validate_investment_currency(pool_id, tranche_id, currency_id)?; T::OutboundQueue::submit( who, @@ -847,6 +822,39 @@ pub mod pallet { }, ) } + + /// Disallow a currency to be used as a pool currency and to invest in a + /// pool on the domain derived from the given currency. + #[pallet::call_index(13)] + #[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())] + pub fn disallow_investment_currency( + origin: OriginFor, + pool_id: T::PoolId, + tranche_id: T::TrancheId, + currency_id: CurrencyIdOf, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + ensure!( + T::Permission::has( + PermissionScope::Pool(pool_id), + who.clone(), + Role::PoolRole(PoolRole::PoolAdmin) + ), + Error::::NotPoolAdmin + ); + + let (currency, chain_id) = + Self::validate_investment_currency(pool_id, tranche_id, currency_id)?; + + T::OutboundQueue::submit( + who, + Domain::EVM(chain_id), + Message::DisallowInvestmentCurrency { pool_id, currency }, + )?; + + Ok(()) + } } impl Pallet { @@ -972,6 +980,43 @@ pub mod pallet { Ok(currency) } + + /// Performs multiple checks for the provided currency and returns its + /// general index and the EVM chain ID associated with it. + pub fn validate_investment_currency( + pool_id: T::PoolId, + tranche_id: T::TrancheId, + currency_id: CurrencyIdOf, + ) -> Result<(u128, EVMChainId), DispatchError> { + // Ensure currency is allowed as payment and payout currency for pool + let invest_id = Self::derive_invest_id(pool_id, tranche_id)?; + // Required for increasing and collecting investments + ensure!( + T::ForeignInvestment::accepted_payment_currency(invest_id.clone(), currency_id), + Error::::InvalidPaymentCurrency + ); + // Required for decreasing investments as well as increasing, decreasing and + // collecting redemptions + ensure!( + T::ForeignInvestment::accepted_payout_currency(invest_id, currency_id), + Error::::InvalidPayoutCurrency + ); + + // Ensure the currency is enabled as pool_currency + let metadata = + T::AssetRegistry::metadata(¤cy_id).ok_or(Error::::AssetNotFound)?; + ensure!( + metadata.additional.pool_currency, + Error::::AssetMetadataNotPoolCurrency + ); + + let currency = Self::try_get_general_index(currency_id)?; + + let LiquidityPoolsWrappedToken::EVM { chain_id, .. } = + Self::try_get_wrapped_token(¤cy_id)?; + + Ok((currency, chain_id)) + } } impl InboundQueue for Pallet diff --git a/pallets/liquidity-pools/src/message.rs b/pallets/liquidity-pools/src/message.rs index 324e344378..3e6a84de39 100644 --- a/pallets/liquidity-pools/src/message.rs +++ b/pallets/liquidity-pools/src/message.rs @@ -362,6 +362,14 @@ where token_name: [u8; TOKEN_NAME_SIZE], token_symbol: [u8; TOKEN_SYMBOL_SIZE], }, + /// Disallow a currency to be used as a pool currency and to invest in a + /// pool. + /// + /// Directionality: Centrifuge -> EVM Domain. + DisallowInvestmentCurrency { + pool_id: PoolId, + currency: u128, + }, } impl< @@ -404,6 +412,7 @@ impl< Self::ScheduleUpgrade { .. } => 21, Self::CancelUpgrade { .. } => 22, Self::UpdateTrancheTokenMetadata { .. } => 23, + Self::DisallowInvestmentCurrency { .. } => 24, } } } @@ -729,6 +738,10 @@ impl< token_symbol.encode(), ], ), + Message::DisallowInvestmentCurrency { pool_id, currency } => encoded_message( + self.call_type(), + vec![encode_be(pool_id), encode_be(currency)], + ), } } @@ -881,6 +894,10 @@ impl< token_name: decode::(input)?, token_symbol: decode::(input)?, }), + 24 => Ok(Self::DisallowInvestmentCurrency { + pool_id: decode_be_bytes::<8, _, _>(input)?, + currency: decode_be_bytes::<16, _, _>(input)?, + }), _ => Err(parity_scale_codec::Error::from( "Unsupported decoding for this Message variant", )), @@ -1319,6 +1336,28 @@ mod tests { ) } + #[test] + fn disallow_investment_currency() { + test_encode_decode_identity( + LiquidityPoolsMessage::DisallowInvestmentCurrency { + pool_id: POOL_ID, + currency: TOKEN_ID, + }, + "180000000000bce1a40000000000000000000000000eb5ec7b", + ) + } + + #[test] + fn disallow_investment_currency_zero() { + test_encode_decode_identity( + LiquidityPoolsMessage::DisallowInvestmentCurrency { + pool_id: 0, + currency: 0, + }, + "18000000000000000000000000000000000000000000000000", + ) + } + /// Verify the identity property of decode . encode on a Message value and /// that it in fact encodes to and can be decoded from a given hex string. fn test_encode_decode_identity( diff --git a/runtime/integration-tests/src/generic/cases/liquidity_pools.rs b/runtime/integration-tests/src/generic/cases/liquidity_pools.rs index bfea310603..88927cc3ca 100644 --- a/runtime/integration-tests/src/generic/cases/liquidity_pools.rs +++ b/runtime/integration-tests/src/generic/cases/liquidity_pools.rs @@ -1435,7 +1435,7 @@ mod development { }); } - fn allow_pool_should_fail() { + fn allow_investment_currency_should_fail() { let mut env = FudgeEnv::::from_parachain_storage( Genesis::default() .add(genesis::balances::(cfg(1_000))) @@ -1600,14 +1600,112 @@ mod development { ), pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsWrappedToken ); + }); + } + + fn disallow_investment_currency() { + let mut env = FudgeEnv::::from_parachain_storage( + Genesis::default() + .add(genesis::balances::(cfg(1_000))) + .add(genesis::tokens::(vec![( + GLMR_CURRENCY_ID, + DEFAULT_BALANCE_GLMR, + )])) + .storage(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let currency_id = AUSD_CURRENCY_ID; + let pool_id = DEFAULT_POOL_ID; + let evm_chain_id: u64 = MOONBEAM_EVM_CHAIN_ID; + let evm_address = [1u8; 20]; + + // Create an AUSD pool + create_ausd_pool::(pool_id); + + enable_liquidity_pool_transferability::(currency_id); + + // Enable LiquidityPools transferability + assert_ok!(orml_asset_registry::Pallet::::update_asset( + ::RuntimeOrigin::root(), + currency_id, + None, + None, + None, + None, + // Changed: Add location which can be converted to LiquidityPoolsWrappedToken + Some(Some(liquidity_pools_transferable_multilocation::( + evm_chain_id, + evm_address, + ))), + Some(CustomMetadata { + // Changed: Allow liquidity_pools transferability + transferability: CrossChainTransferability::LiquidityPools, + mintable: Default::default(), + permissioned: Default::default(), + pool_currency: true, + }) + )); - // Create new pool for non foreign asset - // NOTE: Can be removed after merging https://github.com/centrifuge/centrifuge-chain/pull/1343 + assert_ok!( + pallet_liquidity_pools::Pallet::::disallow_investment_currency( + RawOrigin::Signed(Keyring::Bob.into()).into(), + pool_id, + default_tranche_id::(pool_id), + currency_id, + ) + ); + + assert_noop!( + pallet_liquidity_pools::Pallet::::disallow_investment_currency( + RawOrigin::Signed(Keyring::Charlie.into()).into(), + pool_id, + default_tranche_id::(pool_id), + currency_id, + ), + pallet_liquidity_pools::Error::::NotPoolAdmin + ); + }); + } + + fn disallow_investment_currency_should_fail() { + let mut env = FudgeEnv::::from_parachain_storage( + Genesis::default() + .add(genesis::balances::(cfg(1_000))) + .add(genesis::tokens::(vec![( + GLMR_CURRENCY_ID, + DEFAULT_BALANCE_GLMR, + )])) + .storage(), + ); + + setup_test(&mut env); + + env.parachain_state_mut(|| { + let pool_id = DEFAULT_POOL_ID; + let currency_id = CurrencyId::ForeignAsset(42); + let ausd_currency_id = AUSD_CURRENCY_ID; + + // Should fail if pool does not exist + assert_noop!( + pallet_liquidity_pools::Pallet::::disallow_investment_currency( + RawOrigin::Signed(Keyring::Bob.into()).into(), + pool_id, + // Tranche id is arbitrary in this case as pool does not exist + [0u8; 16], + currency_id, + ), + pallet_liquidity_pools::Error::::NotPoolAdmin + ); + + // Register currency_id with pool_currency set to true assert_ok!(orml_asset_registry::Pallet::::register_asset( ::RuntimeOrigin::root(), AssetMetadata { - name: "Acala Dollar".into(), - symbol: "AUSD".into(), + name: "Test".into(), + symbol: "TEST".into(), decimals: 12, location: None, existential_deposit: 1_000_000, @@ -1618,22 +1716,124 @@ mod development { pool_currency: true, }, }, - Some(CurrencyId::AUSD) + Some(currency_id) )); - create_currency_pool::(pool_id + 1, CurrencyId::AUSD, 10_000 * dollar(12)); + // Create pool + create_currency_pool::(pool_id, currency_id, 10_000 * dollar(12)); - // Should fail if currency is not foreign asset + // Should fail if asset is not payment currency assert_noop!( - pallet_liquidity_pools::Pallet::::allow_investment_currency( + pallet_liquidity_pools::Pallet::::disallow_investment_currency( RawOrigin::Signed(Keyring::Bob.into()).into(), - pool_id + 1, - // Tranche id is arbitrary in this case, so we don't need to check for the - // exact pool_id - default_tranche_id::(pool_id + 1), - CurrencyId::AUSD, + pool_id, + default_tranche_id::(pool_id), + ausd_currency_id, + ), + pallet_liquidity_pools::Error::::InvalidPaymentCurrency + ); + + // Allow as payment but not payout currency + assert_ok!(pallet_order_book::Pallet::::add_trading_pair( + ::RuntimeOrigin::root(), + currency_id, + ausd_currency_id, + Default::default() + )); + + // Should fail if asset is not payout currency + enable_liquidity_pool_transferability::(ausd_currency_id); + + assert_noop!( + pallet_liquidity_pools::Pallet::::disallow_investment_currency( + RawOrigin::Signed(Keyring::Bob.into()).into(), + pool_id, + default_tranche_id::(pool_id), + ausd_currency_id, + ), + pallet_liquidity_pools::Error::::InvalidPayoutCurrency + ); + + // Should fail if currency is not liquidityPools transferable + assert_ok!(orml_asset_registry::Pallet::::update_asset( + ::RuntimeOrigin::root(), + currency_id, + None, + None, + None, + None, + None, + Some(CustomMetadata { + // Disallow any cross chain transferability + transferability: CrossChainTransferability::None, + mintable: Default::default(), + permissioned: Default::default(), + // Changed: Allow to be usable as pool currency + pool_currency: true, + }), + )); + assert_noop!( + pallet_liquidity_pools::Pallet::::disallow_investment_currency( + RawOrigin::Signed(Keyring::Bob.into()).into(), + pool_id, + default_tranche_id::(pool_id), + currency_id, + ), + pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsTransferable + ); + + // Should fail if currency does not have any MultiLocation in metadata + assert_ok!(orml_asset_registry::Pallet::::update_asset( + ::RuntimeOrigin::root(), + currency_id, + None, + None, + None, + None, + None, + Some(CustomMetadata { + // Changed: Allow liquidityPools transferability + transferability: CrossChainTransferability::LiquidityPools, + mintable: Default::default(), + permissioned: Default::default(), + // Still allow to be pool currency + pool_currency: true, + }), + )); + assert_noop!( + pallet_liquidity_pools::Pallet::::disallow_investment_currency( + RawOrigin::Signed(Keyring::Bob.into()).into(), + pool_id, + default_tranche_id::(pool_id), + currency_id, ), - DispatchError::Token(sp_runtime::TokenError::Unsupported) + pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsWrappedToken + ); + + // Should fail if currency does not have LiquidityPoolsWrappedToken location in + // metadata + assert_ok!(orml_asset_registry::Pallet::::update_asset( + ::RuntimeOrigin::root(), + currency_id, + None, + None, + None, + None, + // Changed: Add some location which cannot be converted to + // LiquidityPoolsWrappedToken + Some(Some(VersionedMultiLocation::V3(Default::default()))), + // No change for transferability required as it is already allowed for + // LiquidityPools + None, + )); + assert_noop!( + pallet_liquidity_pools::Pallet::::disallow_investment_currency( + RawOrigin::Signed(Keyring::Bob.into()).into(), + pool_id, + default_tranche_id::(pool_id), + currency_id, + ), + pallet_liquidity_pools::Error::::AssetNotLiquidityPoolsWrappedToken ); }); } @@ -1771,7 +1971,9 @@ mod development { crate::test_for_runtimes!([development], add_currency); crate::test_for_runtimes!([development], add_currency_should_fail); crate::test_for_runtimes!([development], allow_investment_currency); - crate::test_for_runtimes!([development], allow_pool_should_fail); + crate::test_for_runtimes!([development], allow_investment_currency_should_fail); + crate::test_for_runtimes!([development], disallow_investment_currency); + crate::test_for_runtimes!([development], disallow_investment_currency_should_fail); crate::test_for_runtimes!([development], schedule_upgrade); crate::test_for_runtimes!([development], cancel_upgrade); crate::test_for_runtimes!([development], update_tranche_token_metadata);