diff --git a/pallets/liquidity-pools/src/defensive_weights.rs b/pallets/liquidity-pools/src/defensive_weights.rs index 6c49292d45..b48f17a496 100644 --- a/pallets/liquidity-pools/src/defensive_weights.rs +++ b/pallets/liquidity-pools/src/defensive_weights.rs @@ -27,6 +27,7 @@ pub trait WeightInfo { fn update_tranche_token_metadata() -> Weight; fn freeze_investor() -> Weight; fn unfreeze_investor() -> Weight; + fn update_tranche_hook() -> Weight; } // NOTE: We use temporary weights here. `execute_epoch` is by far our heaviest @@ -109,4 +110,10 @@ impl WeightInfo for () { // Writes: MessageNonceStore, MessageQueue RocksDbWeight::get().reads_writes(2, 2) } + + fn update_tranche_hook() -> Weight { + // Reads: Pool, Tranche, Permissions + // Writes: MessageNonceStore, MessageQueue + RocksDbWeight::get().reads_writes(3, 2) + } } diff --git a/pallets/liquidity-pools/src/lib.rs b/pallets/liquidity-pools/src/lib.rs index 07bc8dd632..ad4cba72fe 100644 --- a/pallets/liquidity-pools/src/lib.rs +++ b/pallets/liquidity-pools/src/lib.rs @@ -961,6 +961,58 @@ pub mod pallet { Ok(()) } + + /// Notify the specified destination domain about a tranche hook address + /// update. + /// + /// Origin: Pool admin + #[pallet::call_index(16)] + #[pallet::weight(T::WeightInfo::update_tranche_hook())] + pub fn update_tranche_hook( + origin: OriginFor, + pool_id: T::PoolId, + tranche_id: T::TrancheId, + domain: Domain, + hook: [u8; 20], + ) -> DispatchResult { + let who = ensure_signed(origin.clone())?; + + ensure!( + T::PoolInspect::pool_exists(pool_id), + Error::::PoolNotFound + ); + ensure!( + T::PoolInspect::tranche_exists(pool_id, tranche_id), + Error::::TrancheNotFound + ); + ensure!( + T::Permission::has( + PermissionScope::Pool(pool_id), + who.clone(), + Role::PoolRole(PoolRole::PoolAdmin) + ), + Error::::NotPoolAdmin + ); + + let evm_chain_id = match domain { + Domain::EVM(id) => Ok(id), + _ => Err(Error::::InvalidDomain), + }?; + let hook_32 = + T::DomainAddressToAccountId::convert(DomainAddress::EVM(evm_chain_id, hook)).into(); + + T::OutboundMessageHandler::handle( + who, + domain, + Message::UpdateTrancheHook { + pool_id: pool_id.into(), + tranche_id: tranche_id.into(), + hook: hook_32, + }, + )?; + + Ok(()) + } } impl Pallet { diff --git a/pallets/liquidity-pools/src/mock.rs b/pallets/liquidity-pools/src/mock.rs index f76c5e26ab..4945190a4f 100644 --- a/pallets/liquidity-pools/src/mock.rs +++ b/pallets/liquidity-pools/src/mock.rs @@ -40,8 +40,10 @@ pub const ALICE_EVM_LOCAL_ACCOUNT: AccountId = { pub const CENTRIFUGE_DOMAIN_ADDRESS: DomainAddress = DomainAddress::Centrifuge(ALICE_32); pub const CONTRACT_ACCOUNT: [u8; 20] = [1; 20]; pub const CONTRACT_ACCOUNT_ID: AccountId = AccountId::new([1; 32]); -pub const DOMAIN_HOOK_ADDRESS: [u8; 20] = [10u8; 20]; +pub const DOMAIN_HOOK_ADDRESS_20: [u8; 20] = [10u8; 20]; +pub const DOMAIN_HOOK_ADDRESS_32: [u8; 32] = [10u8; 32]; pub const EVM_DOMAIN_ADDRESS: DomainAddress = DomainAddress::EVM(CHAIN_ID, CONTRACT_ACCOUNT); +pub const EVM_DOMAIN: Domain = Domain::EVM(CHAIN_ID); pub const AMOUNT: Balance = 100; pub const CURRENCY_ID: CurrencyId = CurrencyId::ForeignAsset(1); pub const POOL_CURRENCY_ID: CurrencyId = CurrencyId::LocalAsset(LocalAssetId(1)); diff --git a/pallets/liquidity-pools/src/tests.rs b/pallets/liquidity-pools/src/tests.rs index 39d7d3c2ae..4bb1353f83 100644 --- a/pallets/liquidity-pools/src/tests.rs +++ b/pallets/liquidity-pools/src/tests.rs @@ -472,7 +472,7 @@ mod add_tranche { fn config_mocks() { let mut hook = [0; 32]; - hook[0..20].copy_from_slice(&DOMAIN_HOOK_ADDRESS); + hook[0..20].copy_from_slice(&DOMAIN_HOOK_ADDRESS_20); hook[20..28].copy_from_slice(&1u64.to_be_bytes()); hook[28..31].copy_from_slice(b"EVM"); @@ -487,10 +487,10 @@ mod add_tranche { AssetRegistry::mock_metadata(|_| Some(util::default_metadata())); Gateway::mock_get(move |domain| { assert_eq!(domain, &EVM_DOMAIN_ADDRESS.domain()); - Some(DOMAIN_HOOK_ADDRESS) + Some(DOMAIN_HOOK_ADDRESS_20) }); DomainAddressToAccountId::mock_convert(move |domain| { - assert_eq!(domain, DomainAddress::EVM(CHAIN_ID, DOMAIN_HOOK_ADDRESS)); + assert_eq!(domain, DomainAddress::EVM(CHAIN_ID, DOMAIN_HOOK_ADDRESS_20)); hook.clone().into() }); Gateway::mock_handle(move |sender, destination, msg| { @@ -1848,3 +1848,163 @@ mod unfreeze { } } } + +mod update_tranche_hook { + use super::*; + + fn config_mocks() { + DomainAddressToAccountId::mock_convert(move |_| DOMAIN_HOOK_ADDRESS_32.into()); + Permissions::mock_has(move |scope, who, role| { + assert!(matches!(scope, PermissionScope::Pool(POOL_ID))); + match role { + Role::PoolRole(PoolRole::PoolAdmin) => { + assert_eq!(who, ALICE); + true + } + _ => false, + } + }); + Pools::mock_pool_exists(|_| true); + Pools::mock_tranche_exists(|_, _| true); + Gateway::mock_handle(|sender, destination, msg| { + assert_eq!(sender, ALICE); + assert_eq!(destination, EVM_DOMAIN); + assert_eq!( + msg, + Message::UpdateTrancheHook { + pool_id: POOL_ID, + tranche_id: TRANCHE_ID, + hook: DOMAIN_HOOK_ADDRESS_32 + } + ); + Ok(()) + }); + } + + #[test] + fn success() { + System::externalities().execute_with(|| { + config_mocks(); + + assert_ok!(LiquidityPools::update_tranche_hook( + RuntimeOrigin::signed(ALICE), + POOL_ID, + TRANCHE_ID, + EVM_DOMAIN, + DOMAIN_HOOK_ADDRESS_20 + )); + }); + } + + mod erroring_out { + use cfg_types::domain_address::Domain; + + use super::*; + + #[test] + fn with_bad_origin_unsigned_none() { + System::externalities().execute_with(|| { + assert_noop!( + LiquidityPools::update_tranche_hook( + RuntimeOrigin::none(), + POOL_ID, + TRANCHE_ID, + EVM_DOMAIN, + DOMAIN_HOOK_ADDRESS_20 + ), + DispatchError::BadOrigin + ); + }); + } + #[test] + fn with_bad_origin_unsigned_root() { + System::externalities().execute_with(|| { + assert_noop!( + LiquidityPools::update_tranche_hook( + RuntimeOrigin::root(), + POOL_ID, + TRANCHE_ID, + EVM_DOMAIN, + DOMAIN_HOOK_ADDRESS_20 + ), + DispatchError::BadOrigin + ); + }); + } + + #[test] + fn with_pool_dne() { + System::externalities().execute_with(|| { + config_mocks(); + Pools::mock_pool_exists(|_| false); + + assert_noop!( + LiquidityPools::update_tranche_hook( + RuntimeOrigin::signed(ALICE), + POOL_ID, + TRANCHE_ID, + EVM_DOMAIN, + DOMAIN_HOOK_ADDRESS_20 + ), + Error::::PoolNotFound + ); + }); + } + + #[test] + fn with_tranche_dne() { + System::externalities().execute_with(|| { + config_mocks(); + Pools::mock_tranche_exists(|_, _| false); + + assert_noop!( + LiquidityPools::update_tranche_hook( + RuntimeOrigin::signed(ALICE), + POOL_ID, + TRANCHE_ID, + EVM_DOMAIN, + DOMAIN_HOOK_ADDRESS_20 + ), + Error::::TrancheNotFound + ); + }); + } + + #[test] + fn with_origin_not_admin() { + System::externalities().execute_with(|| { + config_mocks(); + Permissions::mock_has(|_, _, _| false); + + assert_noop!( + LiquidityPools::update_tranche_hook( + RuntimeOrigin::signed(ALICE), + POOL_ID, + TRANCHE_ID, + EVM_DOMAIN, + DOMAIN_HOOK_ADDRESS_20 + ), + Error::::NotPoolAdmin + ); + }); + } + + #[test] + fn with_invalid_domain() { + System::externalities().execute_with(|| { + config_mocks(); + + assert_noop!( + LiquidityPools::update_tranche_hook( + RuntimeOrigin::signed(ALICE), + POOL_ID, + TRANCHE_ID, + Domain::Centrifuge, + DOMAIN_HOOK_ADDRESS_20 + ), + Error::::InvalidDomain + ); + }); + } + } +} diff --git a/runtime/integration-tests/src/cases/lp/pool_management.rs b/runtime/integration-tests/src/cases/lp/pool_management.rs index 40f005d45a..b3de30810f 100644 --- a/runtime/integration-tests/src/cases/lp/pool_management.rs +++ b/runtime/integration-tests/src/cases/lp/pool_management.rs @@ -26,9 +26,12 @@ use sp_runtime::FixedPointNumber; use crate::{ cases::lp::{ - names, utils, + names, + names::POOL_A_T_1, + utils, utils::{pool_a_tranche_1_id, Decoder}, - LocalUSDC, EVM_DOMAIN_CHAIN_ID, LOCAL_RESTRICTION_MANAGER_ADDRESS, POOL_A, USDC, + LocalUSDC, EVM_DOMAIN, EVM_DOMAIN_CHAIN_ID, LOCAL_RESTRICTION_MANAGER_ADDRESS, POOL_A, + USDC, }, config::Runtime, env::{EnvEvmExtension, EvmEnv}, @@ -764,3 +767,51 @@ fn unfreeze_member() { )); }); } + +#[test_runtimes([centrifuge, development])] +fn update_tranche_hook() { + let new_hook: [u8; 20] = [1u8; 20]; + let mut env = super::setup::(|evm| { + super::setup_currencies(evm); + super::setup_pools(evm); + super::setup_tranches(evm); + super::setup_investment_currencies(evm); + super::setup_deploy_lps(evm); + }); + + env.state(|evm| { + let solidity = evm.deployed(names::RESTRICTION_MANAGER).address(); + let rust = LOCAL_RESTRICTION_MANAGER_ADDRESS.into(); + assert_eq!( + solidity, rust, + "Hook address changed, please change our stored value (right) to the new address (left)" + ); + let hook_address = Decoder::::decode( + &evm.view(Keyring::Alice, POOL_A_T_1, "hook", None) + .unwrap() + .value, + ); + assert_eq!(hook_address, solidity); + }); + + env.state_mut(|_| { + assert_ok!(pallet_liquidity_pools::Pallet::::update_tranche_hook( + Keyring::Admin.as_origin(), + POOL_A, + pool_a_tranche_1_id::(), + EVM_DOMAIN, + new_hook + )); + + utils::process_gateway_message::(utils::verify_gateway_message_success::); + }); + + env.state(|evm| { + let hook_address = Decoder::::decode( + &evm.view(Keyring::Alice, POOL_A_T_1, "hook", None) + .unwrap() + .value, + ); + assert_eq!(hook_address, H160::from(new_hook)); + }); +}