owned this note
owned this note
Published
Linked with GitHub
# Liquidity Pools V1 - Domain Specific Details - Centrifuge Chain
###### tags: `connector` `liquidity-pools` `specs`
[TOC]
## Architecture
The follwoing diagramm gives a high level overview of what connectors looks like after v2 is implemented, where the arrows define the path of Connectors messages (incoming and outgoing).
```plantuml
@startuml Architecture
component Centrifuge-Chain {
node "Pallet-Connectors" {
component [Extrinsics] as C1
component [Message Handler] as C8
port InboundQueue
}
node "Pallet-Connectors-Gateway" {
component [Router Domain A] as C3
component [Router Domain B] as C5
component [Router Domain ..] as C6
component [Process Messages] as C7
port OutboundQueue
}
node "Transport-Layer" {
component XCM
component "Axelar-Gateway"
}
}
cloud "Bridge/Relay-Chain" as C9 {
}
"Transport-Layer" ..> C9 : sends
C9 ..> "Transport-Layer" : receives
C3 --> "Transport-Layer": submits
C5 --> "Transport-Layer": submits
C6 --> "Transport-Layer": submits
"Transport-Layer" --> C7: forwards
C7 --> "InboundQueue": submits
"InboundQueue" ..> C8: uses
C1 --> "OutboundQueue": submits
"OutboundQueue" ..> C3: chooses
"OutboundQueue" ..> C5: chooses
"OutboundQueue" ..> C6: chooses
@enduml
```
## Types
### Account Conversion
Every account conversion MUST follow this [spec](https://centrifuge.hackmd.io/nEwvthS8TLOPDAMZ191-rA).
### Domain Address
:::info
:bulb: Already existing. Just noted her for completness.
:::
:::danger
:warning: **SHOULD** be moved to `libs::types` and the implementation of `TypeId` **MUST** be moved to `libs::ids`.
:::
* Used to uniquely identify accounts accross different domains
* Used to derive a unique, native `AccountId32` in Centrifuge chain
```rust=
#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo)]
#[cfg_attr(feature = "std", derive(Debug))]
pub enum DomainAddress {
/// A Centrifuge-Chain based account address, 32-bytes long
Centrifuge([u8; 32]),
/// An EVM chain address, 20-bytes long
EVM(EVMChainId, [u8; 20]),
}
impl TypeId for DomainAddress {
const TYPE_ID: [u8; 4] = *b"dadr";
}
```
### Domain Locator
:::info
:bulb: Already existing. Just noted here for completness.
:::
:::danger
:warning: **SHOULD** be moved to `libs::types` and the implementation of `TypeId` **MUST** be moved to `libs::ids`.
:::
* Used to derive a unique, native `AccountId32` in Centrifuge chain for a domain.
→ Used for accounting purposes. How many tokens left and where received from a specific domain.
```rust=
#[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo)]
pub struct DomainLocator<Domain> {
pub domain: Domain,
}
impl<Domain> TypeId for DomainLocator<Domain> {
const TYPE_ID: [u8; 4] = *b"domn";
}
```
### Domain
:::info
:bulb: Already existing. Just noted her for completness.
:::
* Used to identify a Domain on Centrifuge chain
```rust=
#[derive(Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)]
#[cfg_attr(feature = "std", derive(Debug))]
pub enum Domain {
/// Referring to the Centrifuge Parachain. Will be used for handling incoming messages.
/// NOTE: Connectors messages CAN NOT be sent directly from the Centrifuge chain to the
/// Centrifuge chain itself.
Centrifuge,
/// An EVM domain, identified by its EVM Chain Id
EVM(EVMChainId),
}
```
### Currency
:::warning
:question: Need to adapt the `enum CurrencyId` :question:
:::
**What we do need**
* Methods for mapping `uint128`
* Methods for retreiving reserve/issuer domain of an asset
* Sync between XCM and Connectors asset -- not having custom structures for them
**Current Problems**
The given enum (see below)
```rust=
#[derive(
Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen,
)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub enum CurrencyId {
// The Native token, representing AIR in Altair and CFG in Centrifuge.
Native,
// A Tranche token
Tranche(PoolId, TrancheId),
/// Karura KSM
KSM,
/// Acala Dollar
/// In Altair, it represents AUSD in Kusama;
/// In Centrifuge, it represents AUSD in Polkadot;
AUSD,
/// A foreign asset
ForeignAsset(ForeignAssetId),
}
```
mixes
* Fixed variants for foreign assets -- e.g. KSM
* ForeignAssets(ForeignAssetId)
both of the above are foreign assets but they are identified differently.
* One doesn't have a registry entry
* One has a registry entry
:::danger
:warning: Existing struct. **Check** possible migrations :warning:
:::
:::success
:page_facing_up: Proposal for...
*using ONLY fixed variant values for local stuff and ALL OTHERS in `ForeignAssets`*
```rust=
pub enum CurrencyId {
// The Native token, representing AIR in Altair and CFG in Centrifuge.
Native,
// A Tranche token
Tranche(PoolId, TrancheId),
/// A foreign asset
/// NOTE: Move KSM or AUSD into this id
ForeignAsset(ForeignAssetId),
}
/// NOTE: u128 as we can map from general index
pub type ForeignAssetId = u128
```
:::
:::success
:page_facing_up: Proposal for...
*using ONLY fixed variant values.*
* NEEDS runtime-target specifc configurations to distingish correctly
* NEEDS some macro or so to derive methods
```rust=
pub enum CurrencyId {
// The Native token, representing AIR in Altair and CFG in Centrifuge.
#[currency::reserve(Domain::Centrifuge)]
#[currency::decimals(18)]
#[currency::name("Centrifuge")]
#[currency::symbol("CFG")]
Native,
// A Tranche token
#[currency::reserve(Domain::Centrifuge)]
#[currency::decimals(fn tranche_decimals)]
#[currency::name(fn tranche_name))]
#[currency::symbol(fn tranche_symbol)]
Tranche(PoolId, TrancheId),
// Possible future variants
#[currency::reserve(Domain::EVM(ACALA_ID))]
#[currency::decimals(12)]
#[currency::name("Acala USD")]
AUSD,
#[currency::reserve(Domain::EVM(MAINNET_ID))]
#[currency::decimals(16)]
#[currency::name("Wrapped Ethereum DAI")]
EthDai
#[currency::reserve(Domain::EVM(MAINNET_ID))]
#[currency::decimals(16)]
#[currency::name("Wrapped Ethereum USDC")]
EthUSDC
#[currency::reserve(Domain::Statemint)]
#[currency::decimals(16)]
#[currency::name("Statemint native USDC")]
#[currency::general_index(1000)]
USDC
#[cfg_attr("runtime-altair")]
#[currency::reserve(Domain::Kusama)]
#[currency::decimals(16)]
#[currency::name("Kusama")]
#[currency::general_index(1001)]
KSM
}
```
:::
## Traits
* The outbound queue for sending connector messages
*This is implemented by the `pallet-connectors-gateway`.*
:::info
:bulb: Although, the implementation of this by the `pallet-connectors-gateway` will NOT be a real queue for the time being, we probably need some queue like behaviour in the future. E.g., if the process after queue is just to heavy.
Futhermore, the queue would be a good case to handle error resolution.
:::
```rust=
trait OutboundQueue {
// The Sender type of the Outgoing message
type Sender;
/// The connector message enum.
type Message;
/// The destination this message should go to
type Destination
/// "Submitting" a message to the outbound queue.
fn submit(destination: Self::Destination, sender: Self::Sender, msg: Self::Message) -> DispatchResult;
}
```
* The inbound queue for receiving connector messages
*This is implemented by the `pallet-conectors`.*
:::info
:bulb: Although, the implementation of this by the `pallet-connectors` will NOT be a real queue for the time being, we probably need some queue like behaviour in the future. E.g., if the process after queue is just to heavy.
Futhermore, the queue would be a good case to handle error resolution.
:::
```rust=
trait InboundQueue {
// The origin domain of the message
type Sender;
/// The connector message enum.
type Message;
/// "Submitting" a message to the inbound queue.
fn submit(sender: Self::Sender, msg: Self::Message) -> DispatchResult;
}
```
## Pallets
### `pallet-connectors-gateway`.
:::info
:bulb: The pallet is used to forward connector messages in the right way for each domain. This pallet will actually hold the routers and be adaptable in cases we wanna switch the way a message gets routed to its destination domain.
:::
:::info
:bulb: The pallet is used to receive connectors messages from various sources, checks if they are from valid sources and forwards them to the connectors pallet.
:::
* MUST implement `trait OutboundQueue`
```rust=
impl<T: Config> OutboundQueue for Pallet<T> {
type Sender = T::AccountId,
type Destination = Domain;
type Message = connectors::Message;
}
```
* Logical Requirements
* MUST ensure messages with `destination = Domain::Centrifuge` get rejected
* MUST ensure the router "sees" the correct sender
:::info
:bulb: The reason here is that we can not trust any sender on other domains to submit valid Connector messages. Hence, we need to generate a trusted sender here, that we can add as a valid sender on the other domains.
:::
* MUST ensure that only local, runtime-generated origins can trigger the processing of connector messages
```rust=
/// The origin that needs to be provided to
/// process messages
#[pallet::origin]
pub enum Origin = DomainAddress;
impl EnsureOrigin<T::RuntimeOrigin> for Origin
{
type Success = Self;
...
}
```
* Config
```rust=
#[pallet::config]
pub trait Config: frame_system::Config {
/// The logic actually processing messages further down the stack
type Connectors: InboundQueue<Sender = DomainAddress, Message = ConnectorMessage>;
...
}
```
#### Extrinsics
##### `fn set_domain_router`
:::info
:bulb: Routers are a way of telling this pallet which transport -- XCM, Axelar, ... -- to use for a given destination when sending Connectors messages.
:::
* Logical Requirements
* ONE router per domain
* ONLY callable by `AdminOrigin`
* Triggers setting new router event
* Method Parameters
```rust
origin: OriginFor<T>,
````
```rust
domain: Domain,
````
```rust
router: Router<CurrencyIdOf<T>>,
````
* Implementation Proposal
```rust=
/// Set a Domain's router
#[pallet::weight(< T as Config >::WeightInfo::set_domain_router())]
pub fn set_domain_router(
origin: OriginFor<T>,
domain: Domain,
router: Router<CurrencyIdOf<T>>,
) -> DispatchResult {
T::AdminOrigin::ensure_origin(origin.clone())?;
<DomainRouter<T>>::insert(domain.clone(), router.clone());
Self::deposit_event(Event::SetDomainRouter { domain, router });
Ok(())
}
```
---
##### `fn process_msg`
:::info
:bulb: An extrinsics that is used by the the runtime or others to forward messages to (expected to be connector messages). Possible sources of theses messages can be XCM or Axelar.
:::
* Logical Requirements
* MUST ensure ONLY local origins can call it
* Decodes bytes into on-chain connectors message type
* MUST ensure that origin is valid -- e.g. that the right contract on the Ethereum side submitted the message
* Setting this valid origin MUST be done via the `fn add_submitter` extrinsic
* Method Parameters
```rust
origin: OriginFor<T>,
````
```rust
msg: BoundedVec<u8, T::MaxMsgSize>,
````
* Implementation proposal
```rust=
#[pallet::weight(0)]
fn process_msg(origin: OriginFor<T>, msg: BoundedVec<u8, T::MaxEthMsg>) -> DispatchResult {
// Ensure that the origin is generated on-chain and is the local
// one.
let local_origin = Origin::ensure_origin(origin)?;
let (sender, msg) = match local_origin {
Origin::EVM { chain_id, address } = Self::decode_and_verify_ethereum_message(address, msg)?
Origin::Centirfuge {..} => Error::<T>::LocalMessagePassed
}
T::Connectors::submit(sender, msg)?
}
```
---
##### `fn add_submitter`
:::info
:bulb: An extrinsics that is used to store a `DomainAddress` as a valid source of connectors messages. These addresses will be used during processing of messages, to ensure only valid sources can submit messages to our system.
:::
* Logical Requirements
* MULTIPLE valid submitters per Domain must be possible
* ONLY callable by `AdminOrigin`
* Method Parameters
```rust
origin: OriginFor<T>,
````
```rust
submitter: DomainAddress
````
---
##### `fn rm_submitter`
* Logical Requirements
* ONLY callable by `AdminOrigin`
* Removes previously added submitter if possible
* Method Parameters
```rust
origin: OriginFor<T>,
````
```rust
submitter: DomainAddress
````
### `pallet-connectors`
:::success
:page_facing_up: Proposal for...
using a new `trait PoolMetadata: PoolInspect {..}` that allows retrieving the necessary data and the registry implements it. Usable for
* Getting tranche metadata -- Adding tranche, updating tranche metadata
* Getting pool metadate -- Adding pool, updating pool metdata
This allows to have the adding and updating extrinsics of this pallet to accept anybody to trigger them, as the data is not provided by the calley but rather by the chain state.
:::
* MUST implement `trait InboundQueue`
```rust=
trait InboundQueue {
type Sender = Domain;
type Message = connectors::Message;
}
```
:::info
:bulb: The `Sender` of the message is only the domain, as we do not need to verify anything based on the "real" sender -- e.g. a smart contract allowlisted in the `pallet-connectors-gateway`. BUT we need to know from which domain we are receiving this message.
:::
* MUST ONLY handle messages of type `incoming`/`bidirectional`
* `relaying` is currently NOT supported
* REQUIREMENTS for handling of accepted messages
:::info
:bulb: Checks for `Role::TrancheInvestor` are done in the investments pallet and MUST NOT be done again in the connectors pallet.
:::
##### `Transfer`
* MUST ensure received `uint128` maps to an accepted `CurrencyId` variant
* MUST mint `amount` tokens into receiving address
* MUST ensure receiving domain is not local
:::warning
:question: Is it okay to simply map the recveiver bytes of `[u8; 32]` into our local `AccountId32` :question:
:::
* MUST trigger message processed event
##### `TransferTrancheTokens`
* MUST ensure pool exists
* MUST ensure tranche exists
* MUST ensure receiver has `TrancheInvestor` role
* MUST transfer tokens from `DomainLocator` account of origination domain of this message into holdings of receiving account
:::warning
:question: Is it okay to simply map the recveiver bytes of `[u8; 32]` into our local `AccountId32` :question:
:::
* MUST trigger message processed event
##### `IncreaseInvestOrder`
* MUST ensure received `uint128` maps to an accepted `CurrencyId` variant
* MUST ensure mapped `CurrencyId` variant equals pool currency
* MUST mint `amount` into receiving address
* MUST ensure pool exists
* MUST ensure tranche exists
* Generate the right order amount → `order = order.current + amount`
* Update the order in the `pallet-investments`
:::success
:page_facing_up: Proposal for...
*using `T::Investments: Investments`* a trait that pallet-investments implements
:::
##### `DecreaseInvestOrder`
* MUST ensure received `uint128` maps to an accepted `CurrencyId` variant
* MUST ensure mapped `CurrencyId` variant equals pool currency
* MUST ensure pool exists
* MUST ensure tranche exists
* Generate the right order amount → `order = order.current - amount`
* Update the order in the `pallet-investments`
:::success
:page_facing_up: Proposal for...
*using `T::Investments: Investments`* a trait that pallet-investments implements
:::
* MUST burn `amount` from investor account
* MUST trigger `Message::Transfer` of `amount` back to the sending domain, to refund the user.
* MUST use the same currency as the received one
##### `IncreaseRedeemOrder`
* MUST ensure pool exists
* MUST ensure tranche exists
* MUST transfer tranche tokens from `DomainLocator` account of origination domain of this message into holdings of investor account
* Generate the right order amount → `order = order.current + amount`
* Update the order in the `pallet-investments`
:::success
:page_facing_up: Proposal for...
*using `T::Investments: Investments`* a trait that pallet-investments implements
:::
##### `DecreaseRedeemOrder`
* MUST ensure pool exists
* MUST ensure tranche exists
* MUST transfer tranche tokens from investor account into holdings of `DomainLocator` account of origination domain of this message
* Generate the right order amount → `order = order.current - amount`
* Update the order in the `pallet-investments`
:::success
:page_facing_up: Proposal for...
*using `T::Investments: Investments`* a trait that pallet-investments implements
:::
* MUST trigger `Message::TransferTrancheTokens` of `amount` back to the sending domain, to refund the user.
##### `CollectInvest`
* MUST call to collect
* MUST trigger `Message::Transfer` of collected amount back to the sending domain to move collected tokens to user.
##### `CollectRedeem`
* MUST call to collect
* MUST trigger `Message::TransferTrancheTokens` of collected amount back to the sending domain to move collected tokens to user.
:::success
:page_facing_up: Proposal for...
*extending `trait Investments`* to actually also expose `collect`-methods or create a new trait that is implemented by the pallet.
:::
#### Extrinsics
:::info
:bulb: All of the extrinsics of this pallet will in the end trigger the sending of an outgoing Connectors message to another domain using the provided `T::OutboundQueue`. But the amount of work each logic must do upfront differs. E.g. bookeeping for transfered balances, sanity checks.
:::
:::info
:bulb: The sender of the connectors message MUST be origin that submitted the extrinsics. The `T::OutboundQueue` although MUST and WILL take care of generating a safe origin that the receiving side will accept. But the original sender is important for fees and (probably) recovery reasons.
:::
##### `fn add_pool`
* Logical Requirements
* Callable by any signed origin
* MUST check whether pool exist
* MUST trigger an message sent event
* MUST submit `Message::AddPool` to outbound queue
* Method Parameters
```rust
origin: OriginFor<T>,
````
```rust
domain: Domain,
````
```rust
pool: PoolIdOf<T>,
````
---
##### `fn add_pool_currency`
:::info
:bulb: If a currency is allowlisted for a given pool in this way, then the domain assumes that Centrifuge Chain provides means of using this domain to invest into a pool.
:::
:::info
:bulb: MUST also be called if a pool itself is denominated in a foreign currency -- i.e. the native pool currency is a foreign currency. Hence, `AddPool` + `AddTranche` themselves are not sufficient to allow investors to invest into a pool from other domains. It must rather be `AddPool` + `AddTranche` + `AddPoolCurrency`
:::
:::warning
:construction: Requirements are not 100% defined yet. :construction:
Best would be:
* Some location on our chain provides a trait that allows to query whether one can invest with a given currency. If this is positive we can forward the message to the correct domain
For Now:
* Only allow to be called by `T::AdminOrigin`
:::
:::success
:page_facing_up: Proposal for...
Extending the `trait Investment` or create a new `trait ForeignInvestments` that allows to query whether a given currency can be used for investing into a pool. If yes, then we can submit this currency for the given pool.
:::
* Logical Requirements
* Callable by :question: `NOT YET DEFINED` :question:
* MUST derive domain from provided currency
* MUST check whether we can use this currency to invest into the given pool.
* MUST submit `Message::AddPoolCurrency` to outbound queue
* Method Parameters
```rust
origin: OriginFor<T>,
````
```rust
pool: PoolIdOf<T>,
````
```rust
currency: CurrencyIdOf<T>,
````
---
##### `fn add_tranche`
* Logical Requirements
* Callable by any signed origin
* MUST check whether tranche exist on pool
* MUST trigger an message sent event
* MUST create/collect the metadata of the tranche from the `pallet-pool-registry`.
* MUST get the price of the tranche
* MUST submit `Message::AddTranche` to outbound queue
* Method Parameters
```rust
origin: OriginFor<T>,
````
```rust
domain: Domain,
````
```rust
pool: PoolIdOf<T>,
````
```rust
tranche: TrancheIdOf<T>,
````
---
##### `fn add_currency`
* Logical Requirements
* Callable by any signed origin
* MUST check in the `orml-asset-registry` if the given is a foreign asset that has an Connectors "remote" address.
* MUST derive both the domain to send to as also the EVM address of that currency from the `orml-asset-registry`
* MUST submit `Message::AddCurrency` to outbound queue
* Method Parameters
```rust
origin: OriginFor<T>,
````
```rust
currency: CurrencyIdOf<T>,
````
---
##### `fn update_member`
:::info
:bulb: This methods assumes that the `MemberListAdmin` of the pool has already allowlisted the `DomainAddress.into_account_truncating()` via the `pallet_permissions::Pallet::add(..)` extrinsic.
:::
:::info
:bulb: We also check for the existence of the pool as we have no way of ensuring all storage related to a pool has been wiped once we allow to close pools.
:::
:::warning
:question: Is it a risk to allow any signed origin to submit this :question:
Argument for NO:
* The address must before that be whitelisted from the `MemberlistAdmin`
* The address can NEVER be associated with a correct `pubKey` of an encryption scheme
:::
* Logical Requirements
* Callable by any signed origin
* MUST check whether the `address` has permissions as `TrancheInvestors`
* MUST check whether passed `valid_until` is actually a valid value
:::success
:page_facing_up: Proposal for...
*how to check permissions.*
* Change `ensure` to:
```rust=
ensure!(
T::Permission::has(
PermissionScope::Pool(pool_id),
address.into_account_truncating(),
Role::PoolRole(PoolRole::TrancheInvestor(tranche_id, valid_until))
),
)
```
* Change `libs::types::permissions`
* Take into account passed time if value not `const UNION: u64 = 0.`
→ Need to change `fn contains()` of `struct TrancheInvestors`
:::
* MUST trigger an message sent event
* MUST check existance of pool
* MUST check existance of tranche
* MUST submit `Message::UpdateMember` to outbound queue
* Method Parameters
```rust
origin: OriginFor<T>,
```
```rust
pool: PoolIdFor<T>,
```
```rust
tranche: TrancheIdFor<T>,
````
```rust
address: DomainAddress,
```
```rust
valid_until: Moment
```
---
##### `fn update_tranche_token_price`
:::success
*changing the `trait PoolInspect`*.
Currently the `trait PoolInspect` also contains the `get_tranche_token_price`. A few arguments for refactoring
* Naming is misleading. Actually, it is computing the price
* The trait should not be bloated. It is not inspecting here.
* Implement something like `trait TranchePricer` or so
* Maybe even store calculate prices, BUT NOT really sure if this is useful
:::
* Logical Requirements
* Callable by any signed origin
* MUST get the token price from the `pallet-pools`
* MUST trigger an message sent event
* MUST get the price of the tranche
* MUST submit `Message::UpdateTrancheTokenPrice` to outbound queue
* Method Parameters
```rust
origin: OriginFor<T>,
```
```rust
pool: PoolIdFor<T>,
```
```rust
tranche: TrancheIdFor<T>,
````
```rust
domain: Domain,
```
---
##### `fn transfer`
* Logical Requirements
* Callable by any signed origin
* MUST take balance from signed origin
* MUST ensure the given `currency` is actually a currency where the receiving domain is the reserve/issuer of these tokens
:::danger
:warning: (All of the following does NOT relate to `TrancheToken`)
* We only want to move tokens between `Centrifuge <> OtherDomain`.
* We MUST ensure that tokens from `OtherDomain-A` never get transferred to `OtherDomain-B`
* We NEVER want to send tokens where Centrifuge is the reserve/issuer to be moved to other domains
:::warning
:question: Maybe with exception of CFG:question:
:::
:::warning
:question: Does this mean, Connectors becomes our only "Bridge-Logic" and we only accept transferring tokens to/from other-domains via Connectors contract logic/messages:question:
:::
:::warning
:question:What about existing bridge logic and future other:question:
:::
See `Message::Transfer` notes also.
:::
* MUST ensure `amount` is greater zero
:::info
:bulb: Issuance of "incoming" tokens are implicitly tracked via the `TotalIssuance` of the given currency. This is also why it is so important that we never allow receiving `CurrencyId::TokenA` from `OtherDomain-A`, and sending `CurrencyId::TokenA` to `OtherDomain-B`. The accounting on the receiving chains is broken otherwise.
:::
* MUST ensure `receiver` is not local domain, e.g. `Centrifuge`
* MUST burn the `amount` from the users balance of currency
* MUST submit `Message::Transfer` to outbound queue
* Method Parameters
```rust
origin: OriginFor<T>,
````
```rust
currency: CurrencyIdFor<T>,
```
```rust
receiver: DomainAddress,
```
```rust
amount: BalanceFor<T>,
````
---
##### `fn transfer_tranche_tokens`
* Logical Requirements
:::info
:bulb: Checking for pool existance needed, as we don't want to allow moving tokens out, of the pool is closed or the tranche is removed. Although, the internal pools logic MUST ensure this is never possible to happen
:::
:::warning
:Question: Is checking for tranche investor role of receiver actually needed:question:
→ Maybe only needed if the receiving chain also has restrictions in the token
→ Maybe legal reasons
:::
* Callable by any signed origin
* MUST take balance from signed origin
* MUST ensure the receiving address has `TrancheInvester` permissions for the given tranche
* MUST ensure the pool exists
* MUST ensure the tranche exists
* MUST ensure `amount` is greater zero
* MUST ensure `DomainLocator` balance is INCREASED by the sended amount. Address must be derived from `DomainLocator::into_account_truncating()`.
:::info
:bulb: Transfers of this kind **increases** the amount of tranche tokens held by the `DomainLocator`-Account. The reason being is, that Centrifuge chain is the reserve/issuer of these tokens.
Vice versa, incoming messages of kind `TranferTrancheTokens` **reduce** the holdings of the `DomainLocator`-Account.
:::
* MUST submit `Message::TransferTrancheTokens` to outbound queue
* Method Parameters
```rust
origin: OriginFor<T>,
````
```rust
pool: PoolIdFor<T>,
```
```rust
tranche: TrancheIdFor<T>,
````
```rust
receiver: DomainAddress,
```
```rust
amount: BalanceFor<T>,
```