diff --git a/pallets/liquidity-pools/src/lib.rs b/pallets/liquidity-pools/src/lib.rs index ad4cba72fe..31fde198d3 100644 --- a/pallets/liquidity-pools/src/lib.rs +++ b/pallets/liquidity-pools/src/lib.rs @@ -121,6 +121,7 @@ pub mod pallet { }; use frame_system::pallet_prelude::*; use parity_scale_codec::HasCompact; + use sp_core::U256; use sp_runtime::{traits::Zero, DispatchError}; use super::*; @@ -1013,6 +1014,44 @@ pub mod pallet { Ok(()) } + + /// Initiate the recovery of assets which were sent to an incorrect + /// contract by the account represented by `domain_address`. + /// + /// NOTE: Asset and contract addresses in 32 bytes in order to support + /// future non-EVM chains. + /// + /// Origin: Root. + #[pallet::call_index(17)] + #[pallet::weight(T::WeightInfo::update_tranche_hook())] + pub fn recover_assets( + origin: OriginFor, + domain_address: DomainAddress, + incorrect_contract: [u8; 32], + asset: [u8; 32], + // NOTE: Solidity balance is `U256` per default + amount: U256, + ) -> DispatchResult { + ensure_root(origin)?; + + ensure!( + matches!(domain_address.domain(), Domain::EVM(_)), + Error::::InvalidDomain + ); + + T::OutboundMessageHandler::handle( + T::TreasuryAccount::get(), + domain_address.domain(), + Message::RecoverAssets { + contract: incorrect_contract, + asset, + recipient: T::DomainAddressToAccountId::convert(domain_address).into(), + amount: amount.into(), + }, + )?; + + Ok(()) + } } impl Pallet { diff --git a/pallets/liquidity-pools/src/message.rs b/pallets/liquidity-pools/src/message.rs index d981abfb16..076c8f6250 100644 --- a/pallets/liquidity-pools/src/message.rs +++ b/pallets/liquidity-pools/src/message.rs @@ -15,7 +15,6 @@ use serde::{ ser::{Error as _, SerializeTuple}, Deserialize, Serialize, Serializer, }; -use sp_core::U256; use sp_runtime::{traits::ConstU32, DispatchError, DispatchResult}; use sp_std::{vec, vec::Vec}; @@ -267,10 +266,10 @@ pub enum Message { asset: Address, /// The user address which receives the recovered tokens recipient: Address, - /// The amount of tokens to recover + /// The amount of tokens to recover. /// - /// NOTE: Use `u256` as EVM balances are `u256`. - amount: U256, + /// NOTE: Represents `sp_core::U256` because EVM balances are `u256`. + amount: [u8; 32], }, // --- Gas service --- /// Updates the gas price which should cover transaction fees on Centrifuge @@ -903,6 +902,26 @@ mod tests { ) } + #[test] + fn recover_assets() { + let msg = Message::RecoverAssets { + contract: [2u8; 32], + asset: [1u8; 32], + recipient: [3u8; 32], + amount: (sp_core::U256::MAX - 1).into(), + }; + test_encode_decode_identity( + msg, + concat!( + "07", + "0202020202020202020202020202020202020202020202020202020202020202", + "0101010101010101010101010101010101010101010101010101010101010101", + "0303030303030303030303030303030303030303030303030303030303030303", + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe", + ), + ); + } + #[test] fn update_tranche_token_metadata() { test_encode_decode_identity( diff --git a/pallets/liquidity-pools/src/mock.rs b/pallets/liquidity-pools/src/mock.rs index 4945190a4f..d676dd04ea 100644 --- a/pallets/liquidity-pools/src/mock.rs +++ b/pallets/liquidity-pools/src/mock.rs @@ -26,7 +26,7 @@ pub const CHAIN_ID: u64 = 1; pub const ALICE_32: [u8; 32] = [2; 32]; pub const ALICE: AccountId = AccountId::new(ALICE_32); pub const ALICE_ETH: [u8; 20] = [2; 20]; -pub const ALICE_EVM_DOMAIN_ADDRESS: DomainAddress = DomainAddress::EVM(42, ALICE_ETH); +pub const ALICE_EVM_DOMAIN_ADDRESS: DomainAddress = DomainAddress::EVM(CHAIN_ID, ALICE_ETH); // TODO(future): Can be removed after domain conversion refactor pub const ALICE_EVM_LOCAL_ACCOUNT: AccountId = { let mut arr = [0u8; 32]; diff --git a/pallets/liquidity-pools/src/tests.rs b/pallets/liquidity-pools/src/tests.rs index 4bb1353f83..8654f41926 100644 --- a/pallets/liquidity-pools/src/tests.rs +++ b/pallets/liquidity-pools/src/tests.rs @@ -2008,3 +2008,102 @@ mod update_tranche_hook { } } } + +mod recover_assets { + use super::*; + + const CONTRACT: [u8; 32] = [42; 32]; + const ASSET: [u8; 32] = [43; 32]; + + fn config_mocks() { + DomainAddressToAccountId::mock_convert(move |_| ALICE_EVM_LOCAL_ACCOUNT); + Permissions::mock_has(|_, _, _| false); + Gateway::mock_handle(|sender, destination, msg| { + assert_eq!(sender, TreasuryAccount::get()); + assert_eq!(destination, EVM_DOMAIN); + assert_eq!( + msg, + Message::RecoverAssets { + contract: CONTRACT, + asset: ASSET, + recipient: ALICE_EVM_LOCAL_ACCOUNT.into(), + amount: sp_core::U256::from(AMOUNT).into(), + } + ); + Ok(()) + }); + } + + #[test] + fn success() { + System::externalities().execute_with(|| { + config_mocks(); + + assert_ok!(LiquidityPools::recover_assets( + RuntimeOrigin::root(), + ALICE_EVM_DOMAIN_ADDRESS, + CONTRACT, + ASSET, + AMOUNT.into(), + )); + }); + } + + mod erroring_out { + use super::*; + + #[test] + fn with_wrong_origin_none() { + System::externalities().execute_with(|| { + config_mocks(); + + assert_noop!( + LiquidityPools::recover_assets( + RuntimeOrigin::none(), + ALICE_EVM_DOMAIN_ADDRESS, + CONTRACT, + ASSET, + AMOUNT.into(), + ), + DispatchError::BadOrigin + ); + }); + } + + #[test] + fn with_wrong_origin_signed() { + System::externalities().execute_with(|| { + config_mocks(); + + assert_noop!( + LiquidityPools::recover_assets( + RuntimeOrigin::signed(ALICE.into()), + ALICE_EVM_DOMAIN_ADDRESS, + CONTRACT, + ASSET, + AMOUNT.into(), + ), + DispatchError::BadOrigin + ); + }); + } + + #[test] + fn with_wrong_domain() { + System::externalities().execute_with(|| { + config_mocks(); + + assert_noop!( + LiquidityPools::recover_assets( + RuntimeOrigin::root(), + DomainAddress::Centrifuge(ALICE.into()), + CONTRACT, + ASSET, + AMOUNT.into(), + ), + Error::::InvalidDomain + ); + }); + } + } +} diff --git a/pallets/liquidity-pools/src/tests/inbound.rs b/pallets/liquidity-pools/src/tests/inbound.rs index bc0dd61cbd..d77ee70f14 100644 --- a/pallets/liquidity-pools/src/tests/inbound.rs +++ b/pallets/liquidity-pools/src/tests/inbound.rs @@ -85,6 +85,8 @@ mod handle_transfer { } mod handle_tranche_tokens_transfer { + use cfg_types::domain_address::Domain; + use super::*; fn config_mocks(receiver: DomainAddress) { @@ -143,48 +145,48 @@ mod handle_tranche_tokens_transfer { #[test] fn success_with_evm_domain() { + const OTHER_CHAIN_ID: u64 = CHAIN_ID + 1; + const OTHER_DOMAIN: Domain = Domain::EVM(OTHER_CHAIN_ID); + const OTHER_DOMAIN_ADDRESS_ALICE: DomainAddress = + DomainAddress::EVM(OTHER_CHAIN_ID, ALICE_ETH); + System::externalities().execute_with(|| { - config_mocks(ALICE_EVM_DOMAIN_ADDRESS); + config_mocks(OTHER_DOMAIN_ADDRESS_ALICE); TransferFilter::mock_check(|_| Ok(())); Gateway::mock_handle(|sender, destination, msg| { assert_eq!(sender, ALICE); - assert_eq!(destination, ALICE_EVM_DOMAIN_ADDRESS.domain()); + assert_eq!(destination, OTHER_DOMAIN); assert_eq!( msg, Message::TransferTrancheTokens { pool_id: POOL_ID, tranche_id: TRANCHE_ID, - domain: ALICE_EVM_DOMAIN_ADDRESS.domain().into(), - receiver: ALICE_EVM_DOMAIN_ADDRESS.address().into(), + domain: OTHER_DOMAIN.into(), + receiver: OTHER_DOMAIN_ADDRESS_ALICE.address().into(), amount: AMOUNT } ); Ok(()) }); - Tokens::mint_into( - TRANCHE_CURRENCY, - &EVM_DOMAIN_ADDRESS.domain().into_account(), - AMOUNT, - ) - .unwrap(); + let origin = EVM_DOMAIN.into_account(); + Tokens::mint_into(TRANCHE_CURRENCY, &origin, AMOUNT).unwrap(); assert_ok!(LiquidityPools::handle( EVM_DOMAIN_ADDRESS, Message::TransferTrancheTokens { pool_id: POOL_ID, tranche_id: TRANCHE_ID, - domain: ALICE_EVM_DOMAIN_ADDRESS.domain().into(), + domain: OTHER_DOMAIN.into(), receiver: ALICE.into(), amount: AMOUNT } )); - let origin = EVM_DOMAIN_ADDRESS.domain().into_account(); + let destination = OTHER_DOMAIN.into_account(); + assert_ne!(destination, origin); assert_eq!(Tokens::balance(TRANCHE_CURRENCY, &origin), 0); - - let destination = ALICE_EVM_DOMAIN_ADDRESS.domain().into_account(); assert_eq!(Tokens::balance(TRANCHE_CURRENCY, &destination), AMOUNT); }); } diff --git a/runtime/integration-tests/src/cases/lp/pool_management.rs b/runtime/integration-tests/src/cases/lp/pool_management.rs index 48ada53f6f..7e0636ab25 100644 --- a/runtime/integration-tests/src/cases/lp/pool_management.rs +++ b/runtime/integration-tests/src/cases/lp/pool_management.rs @@ -30,8 +30,8 @@ use crate::{ names::POOL_A_T_1, utils, utils::{pool_a_tranche_1_id, Decoder}, - LocalUSDC, EVM_DOMAIN, EVM_DOMAIN_CHAIN_ID, LOCAL_RESTRICTION_MANAGER_ADDRESS, POOL_A, - USDC, + LocalUSDC, DECIMALS_6, DEFAULT_BALANCE, EVM_DOMAIN, EVM_DOMAIN_CHAIN_ID, + LOCAL_RESTRICTION_MANAGER_ADDRESS, POOL_A, USDC, }, config::Runtime, env::{EnvEvmExtension, EvmEnv}, @@ -791,3 +791,128 @@ fn update_tranche_hook() { assert_eq!(hook_address, H160::from(new_hook)); }); } + +#[test] +fn tmp() { + recover_assets::() +} + +#[test_runtimes([development])] +fn recover_assets() { + let mut env = super::setup::(|evm| { + super::setup_currencies(evm); + }); + let investor = Keyring::Custom("WrongTransfer"); + let amount = DEFAULT_BALANCE * DECIMALS_6; + + // Transfer assets into wrong contract + let (token, wrong_contract) = env.state_mut(|evm| { + let wrong_contract = evm.deployed(names::POOL_MANAGER).address(); + let token = evm.deployed(names::USDC).address(); + + // Need to mint here instead of executing `transferAssets` because this would + // transfer to escrow instead of pool manager + evm.call( + Keyring::Admin, + Default::default(), + names::USDC, + "mint", + Some(&[ + Token::Address(wrong_contract.into()), + Token::Uint(sp_core::U256::from(amount)), + ]), + ) + .unwrap(); + + assert_eq!( + Decoder::::decode(&evm.view( + Keyring::Alice, + names::USDC, + "balanceOf", + Some(&[Token::Address(wrong_contract.into())]), + )), + amount + ); + assert_eq!( + Decoder::::decode(&evm.view( + Keyring::Alice, + names::USDC, + "balanceOf", + Some(&[Token::Address(investor.into())]), + )), + 0 + ); + + (token, wrong_contract) + }); + + env.state_mut(|_| { + assert_ok!(pallet_liquidity_pools::Pallet::::recover_assets( + ::RuntimeOrigin::root(), + DomainAddress::EVM(EVM_DOMAIN_CHAIN_ID, investor.into()), + utils::to_fixed_array(wrong_contract.as_bytes()), + utils::to_fixed_array(token.as_bytes()), + sp_core::U256::from(amount), + )); + + utils::process_gateway_message::(utils::verify_gateway_message_success::); + }); + + env.state(|evm| { + assert_eq!( + Decoder::::decode(&evm.view( + Keyring::Alice, + names::USDC, + "balanceOf", + Some(&[Token::Address(wrong_contract)]), + )), + 0 + ); + assert_eq!( + Decoder::::decode(&evm.view( + Keyring::Alice, + names::USDC, + "balanceOf", + Some(&[Token::Address(investor.into())]), + )), + amount + ); + }); +} + +#[test_runtimes([development])] +fn schedule_upgrade() { + let mut env = super::setup_full::(); + env.state_mut(|evm| { + assert_ok!(pallet_liquidity_pools::Pallet::::schedule_upgrade( + ::RuntimeOrigin::root(), + EVM_DOMAIN_CHAIN_ID, + evm.deployed(names::POOL_MANAGER).address().into() + )); + + utils::process_gateway_message::(utils::verify_gateway_message_success::); + }); +} + +#[test_runtimes([development])] +fn cancel_upgrade() { + let mut env = super::setup_full::(); + env.state_mut(|evm| { + assert_ok!(pallet_liquidity_pools::Pallet::::schedule_upgrade( + ::RuntimeOrigin::root(), + EVM_DOMAIN_CHAIN_ID, + evm.deployed(names::POOL_MANAGER).address().into() + )); + + utils::process_gateway_message::(utils::verify_gateway_message_success::); + }); + env.state_mut(|evm| { + assert_ok!(pallet_liquidity_pools::Pallet::::cancel_upgrade( + ::RuntimeOrigin::root(), + EVM_DOMAIN_CHAIN_ID, + evm.deployed(names::POOL_MANAGER).address().into() + )); + + utils::process_gateway_message::(utils::verify_gateway_message_success::); + }); +} diff --git a/runtime/integration-tests/submodules/liquidity-pools b/runtime/integration-tests/submodules/liquidity-pools index 4301885b9a..6e8f1a29df 160000 --- a/runtime/integration-tests/submodules/liquidity-pools +++ b/runtime/integration-tests/submodules/liquidity-pools @@ -1 +1 @@ -Subproject commit 4301885b9a3b8ec36f3bda4b789daa5b115c006a +Subproject commit 6e8f1a29dff0d7cf5ff74285cfffadae8a8b303f