owned this note
owned this note
Published
Linked with GitHub
# Liquidity Pools v2 Centrifuge Chain Cantina Contest
## Introduction
Founded in 2017, Centrifuge is the institutional platform for credit onchain. Centrifuge was the first protocol where MakerDAO minted DAI against a real-world asset, the first onchain securitization, and Centrifuge launched the RWA Market with Aave. Centrifuge’s multi-chain strategy allows investors to access native RWA yields on the network of their choice.
Centrifuge works based on a hub-and-spoke model. RWA pools are managed by borrowers on Centrifuge Chain, an application-specific blockchain built purposely for managing real world assets. Liquidity Pools are deployed on any other L1 or L2 where there is demand for RWA, and each Liquidity Pool deployment communicates directly with Centrifuge Chain using messaging layers.
## High level overview
The architecture of Centrifuge Chain pallets is centered around `pallet-pool-system` which contains the core logic for managing investment pools such as bundling loans, slicing pools into tranchers and controlling investment epochs. In this particular document, we want to focus on the upper left corner which can be categorized as Liquidity Pools.

## Liquidity Pools
The objective of Liquidity Pools is to facilitate investment activities on EVM chains using Solidity contracts ([repository](https://github.com/centrifuge/liquidity-pools)). Prior to the implementation of these pallets, investments could only be conducted directly on the Centrifuge Chain, either through local transactions or via XCM for any Polkadot chain with a bidirectional channel (UMP for relay chains, HRMP for parachains). Given the Polkadot ecosystem's liquidity constraints, this feature allows Centrifuge Chain to leverage the strengths of both ecosystems: Polkadot's efficiency, flexibility, and security, along with the liquidity available on EVM chains.
The LP messages ([spec](https://centrifuge.hackmd.io/Soy4JiFbRSyovj53KW-a4A?view)) are exchanged between the EVM and Centrifuge Chains through the LP Router (or *Adapter* in Solidity terminoligy). As of now, our implementation exclusively utilizes Axelar EVM. Nevertheless, the architecture is designed to be adaptable, accommodating the integration of multiple gateways in the future. The Centrifuge Chain acts as a single source of truth. Thus, any necessary information from the Centrifuge Chain must be communicated to the corresponding EVM chain following the deployment of the Solidity contracts. For instance, when a new pool `P` is introduced on an EVM chain `E`, a series of LP messages must be dispatched from the Centrifuge Chain to `E` to establish the initial state before investments in `P` can commence on `E`.
Another crucial principle is that assets invested on EVM chains are not directly transferred. Instead, they are locked within the contract and represented on the Centrifuge Chain as a local entity `CurrencyId::ForeignAsset(u64)`.
## Routing
Here we define all entities required for sending/receiving message
```plantuml
@startuml
skinparam roundcorner 10
cloud "legend" {
component "I'm a pallet" as a
rectangle "I'm an interface" as b
rectangle #Application "I'm a type configured\n at runtime level" as c
a -[hidden]down- b
b -[hidden]down- c
}
skinparam component {
BackgroundColor #Motivation
}
skinparam rectangle {
BackgroundColor #Strategy
}
[pallet-liquidity-pools-gateway] as gateway
[pallet-liquidity-pools] as lp
[pallet-liquidity-pools-queue] as events
[pallet-liquidity-pools-forwarder] as forwarder
frame " routers "{
port "n" as l
[pallet-axelar-router] as axelar_evm
[pallet-snowbridge-router \n <<unimplemented>>] as snowbridge
}
rectangle RouterDispatcher as dispatcher #Application
rectangle MessageSerializer as serializer #Application
rectangle MessageQueue
rectangle MessageProcessor
rectangle OutboundMessageHandler
rectangle InboundMessageHandler
rectangle MessageSender
rectangle MessageReceiver
lp .down.|> InboundMessageHandler : implements
lp -down-> OutboundMessageHandler : uses
gateway .up..|> OutboundMessageHandler : implements
gateway -up--> InboundMessageHandler : uses
gateway .right.|> MessageProcessor : implements
gateway --> MessageQueue : uses
events ..|> MessageQueue : implements
events -up-> MessageProcessor : uses
gateway .left.|> MessageReceiver : implements
gateway --> MessageSender : uses
forwarder .up.> MessageSender : implements & uses
forwarder .up.> MessageReceiver : implements & uses
dispatcher .up.|> MessageSender : implements
dispatcher ---> axelar_evm : uses
dispatcher ---> snowbridge : uses
serializer .down.> MessageSender : implements & uses
serializer .left.> MessageReceiver : implements & uses
l .up..|> MessageSender : implements
l -up--> MessageReceiver : uses
@enduml
```
### Entities
* `pallet-liquidity-pools` is the primary communication point between Centrifuge Chain logic and the EVM through:
* Calling extrinsics that trigger remote EVM contract functions (outbound)
* Internal chain hooks that update the EVM state (outbound)
* Processing actions from EVM domains that alter the chain (inbound)
This pallet communicates with the EVM via the `pallet-liquidity-pools-gateway` by sending messages that contain information about the action. Essentially, `pallet-liquidity-pools` converts actions into messages and messages into actions.
* `pallet-liquidity-pools-gateway` is the central component that links all processes together. It enqueues/dequeues messages for later processing and sends/receives these messages to/from routers that manage communication with EVM domains. The gateway is responsible for:
* Batching messages
* Verifying message proofs before marking a message as *valid* and then processing it
* Sending proofs for a message to different routers
* `pallet-liquidity-pools-gateway-queue` is responsible for storing messages to be processed later when the block has capacity, as determined by the `on_idle()` pallet method, a default Polkadot SDK pallet hook ([documentation](https://paritytech.github.io/polkadot-sdk/master/frame_support/traits/trait.Hooks.html#method.on_idle)). Any message that needs to be dispatched or has been received is first queued. When the block has enough capacity to execute messages from the queue, they are processed one by one until the maximum block weight (configured in the runtime) is reached. Depending on the message's direction, it is either redirected to its designated EVM domain (outbound) or handled internally if Centrifuge Chain is the destination domain (inbound).
* `pallet-liquidity-pools-forwarder` handles forwarded messages. It:
* Checks if an incoming message from domain `B` was forwarded from source domain `A`, and if so, unwraps the `Forwarded` message to extract the original payload and the source domain `A`.
* Checks if an outgoing message to destination domain `A` needs to be forwarded via intermediary domain `B`, and if so, wraps the payload into a `Forwarded` message, including source domain `Centrifuge` and the forwarding contract address on intermediary domain `B`.
* `MessageSerializer` is responsible for serializing outgoing messages and deserializing incoming ones. It is a runtime type that serves as middleware between `routers` and `pallet-liquidity-pools-forwarder`, so neither needs to handle (de-)serialization.
* `RouterDispatcher` is responsible for selecting the correct router based on a `router_id`. The gateway uses this entity to send messages and determine the appropriate router for a router ID. This entity is a runtime type that provides the same interface as a router for sending messages. From the gateway's perspective, it functions as a router, but in practice, it mainly forwards outbound messages. `RouterDispatcher` is used only to send messages to actual routers. In the opposite direction, when receiving messages, these routers submit messages directly to the gateway.
* `routers` are pallets that can receive messages from other domains and send messages back to them. Examples of these routers include:
* `pallet-axelar-router`
* `pallet-snowbridge-router` _(not yet in the codebase)_
When routers receive messages from outside, they submit them to the `pallet-liquidity-pools-gateway`, identifying the message by its unique ID per pallet instance. Similarly, when the gateway sends messages to routers via the `RouterDispatcher`, routers are responsible for delivering them to the message's destination domain.
#### Traits
To know more about the trait methods, check: [`traits/src/liquidity_pools.rs`](https://github.com/centrifuge/centrifuge-chain/blob/main/libs/traits/src/liquidity_pools.rs)
#### Basic types
Types that requires spetial attention to understand Liquidity Pools
* `Domain`: Represents another chain. Currently just supported our own chain and Evm chains.
* `DomainAddress`: A junction of a `Domain` along with the address in that domain. This type also allow coversions.
* i.e. I can create a `DomainAddress` from an ethereum address (as `H160`) and from a chain as:
```rust
let address = DomainAddress::Evm(chain, eth_address)
```
later I can obtain its local representation of that account in our chain as `address.as_local()`, of type `AccountId`
* `RouterId`. Represents a router, which is a *way* of sending/receiving a message to certain chain. It has the same information as a `Domain`, but also contains the information about the *medium* or the *how* the message is sent and receive to that domain.
### Actions
Two main actors:
* The **Centrifuge Operator** who calls extrinsics in Centrifuge Chain with the intention of perform a side effect in the EVM side.
- The **Liquidity Pools Contact** on EVM, who will send messages through EVM to Centrifuge Chain to perform some action on-chain.
The Queue pallet can also be seen as an "actor" that dequeing messages previously enqueued to be processed. This is only performed if the chain has enough capacity to do it after processing the block pending extrinsics (it means, if the chain is "idle").
```plantuml
@startuml
skinparam sequenceArrowThickness 2
skinparam roundcorner 20
skinparam sequence {
LifeLineBackgroundColor #Business
}
actor "Liquidity\nPools\nContact\non\nEVM" as EVM
collections routers << (P, motivation) >>
participant "Router \n Dispatcher" as RouterDispatcher << (T, motivation) >>
participant "Message\nSerializer" as MessageSerializer << (T, motivation) >>
participant Forwarder << (P,motivation) >>
participant Gateway << (P,motivation) >>
participant Queue << (P,motivation) >>
participant "Liquidity \n Pools" as LP << (P,motivation) >>
database "Centrifuge\nChain" as CentrifugeChain
actor "Centrifuge\nOperator" as CentrifugeOperator
== receive message ==
EVM -[#blue]> routers ++ : execute()
routers -> MessageSerializer ++ : receive()
note over MessageSerializer : deserialize
MessageSerializer -> Forwarder ++ : receive()
note over Forwarder : unwrap msg\nif forwarded
Forwarder -> Gateway ++ : receive()
Gateway -> Queue ++ : submit()
note over of Queue : queue msg
Gateway <-- Queue --
Forwarder <-- Gateway --
MessageSerializer <-- Forwarder --
routers <-- MessageSerializer --
deactivate
== execute inbound message ==
...during on_idle()...
note over of Queue : unqueue msg
Queue -> Gateway ++ : process()
loop for any submsg in the msg
Gateway -> LP ++ : handle()
LP -> CentrifugeChain ++ : do_action()
LP <-- CentrifugeChain --
Gateway <-- LP --
end
Queue <-- Gateway --
...
...
...
== execute outbound message ==
CentrifugeOperator -> LP ++ : call_extrinsic()
LP -> Gateway ++ : handle()
alt if batching is enabled
Gateway -> Queue ++ : submit()
note over of Queue : queue msg
Gateway <-- Queue --
else
Gateway -> Gateway : add_into_batch
end
LP <-- Gateway --
CentrifugeOperator <-- LP --
== end batching ==
CentrifugeOperator -> Gateway ++ : end_batch_messages()
Gateway -> Queue ++ : submit()
note over of Queue : queue msg
Gateway <-- Queue --
CentrifugeOperator <-- Gateway --
== send message ==
...during on_idle()...
note over of Queue : unqueue msg
Queue -> Gateway ++ : process()
Gateway -> Forwarder ++ : send()
note over Forwarder : wrap msg\nif forwarding
Forwarder -> MessageSerializer ++ : send()
note over MessageSerializer : serialize
MessageSerializer -> RouterDispatcher ++ : send()
note over RouterDispatcher : choose the correct\nrouter instance
RouterDispatcher -> routers ++ : send()
routers -[#blue]> EVM : call()
RouterDispatcher <-- routers --
MessageSerializer <-- RouterDispatcher --
Forwarder <-- MessageSerializer --
Gateway <-- Forwarder --
Queue <-- Gateway --
@enduml
```
## Foreign Investments
```plantuml
@startuml
skinparam roundcorner 10
cloud "legend" {
component "I'm a pallet" as a
rectangle "I'm an interface" as b
a -[hidden]down- b
}
skinparam component {
BackgroundColor<<utility>> #Lavender
BackgroundColor #Motivation
}
skinparam rectangle {
BackgroundColor #Strategy
}
[pallet-foreign-investments] <<utility>> as fi
[pallet-liquidity-pools] as lp
[pallet-investments] as investments
[pallet-order-book] as orders
[pallet-token-mux] <<utility>> as tokenmux
rectangle Investments
rectangle ForeignInvestments
rectangle ForeignInvestmentsHooks
rectangle TokenSwaps
rectangle "NotificationStatusHook\n(collected)" as collected_hook
rectangle "NotificationStatusHook\n(fulfilled)" as fulfilled_hook
lp .down.|> ForeignInvestmentsHooks : implements
lp -down-> ForeignInvestments : uses
fi .up.|> ForeignInvestments : implements
fi -up-> ForeignInvestmentsHooks : uses
fi .down.|> fulfilled_hook : implements
fi .down.|> collected_hook : implements
fi -down-> Investments : uses
fi -down-> TokenSwaps : uses
orders .up.|> TokenSwaps : implements
orders -up-> fulfilled_hook : uses
tokenmux -up-> TokenSwaps : uses
investments .up.|> Investments : implements
investments -up-> collected_hook : uses
@enduml
```
### Entities
* The `pallet-foreign-investments` is a module without extrinsics that functions as a connector, linking investments and orders to liquidity pools through various traits:
* For incoming investment deposit requests (`DepositRequest` messages), it initiates the token swap via the `TokenSwaps` trait, converting the foreign asset to the pool currency by creating or updating orders in the `pallet-orderbook`.
* Upon partial fulfillment of those orders, the swapped amount is invested via `pallet-investments`.
* Whenever a pending (partial) investment is processed during [pool epoch execution](https://docs.centrifuge.io/user/centrifuge-pools/epoch/), a `FulfilledDepositRequest` message is sent back to the source EVM domain to report the invested amount.
* For incoming investment cancellation requests (`CancelDepositRequest` messages), it cancels any pending investment that has not been processed during the pool epoch execution and initiates a token swap from the pool currency back to the foreign asset originally used for the investment.
* When the entire pending investment amount has been swapped back to the foreign asset, `pallet-foreign-investments` dispatches a `FulfilledCancelDepositRequest` message to the EVM, returning the investment.
* For incoming redemption requests (`RedeemRequest` messages), it starts the redemption process by forwarding the request to `pallet-investments`. This process progresses once the corresponding pool closes and executes an epoch, resulting in the dispatch of a `FulfilledRedeemRequest`.
* For incoming redemption cancellation requests (`CancelRedeemRequest` messages), it cancels any pending redemption process and immediately dispatches a `FulfilledCancelRedeemRequest`, providing information about the amount that is still redeemed.
* `pallet-orderbook` is responsible for swapping two tokens, A and B. In practice, these token pairs represent foreign assets used for investments and pool currencies. It handles placing, updating, canceling, and fulfilling orders.
* Whenever an order related to a foreign investment is partially fulfilled, a notification hook informs `pallet-foreign-investments` of the order details.
* `pallet-token-mux` is a utility module for proxying variants of the same foreign asset (e.g., USDC) to a local asset representation. This allows pools to use a specific local asset representation as the pool currency and facilitates the deposit of foreign investments.
* `pallet-investments` is responsible for managing investments in the corresponding pool's asset and handling redemptions. It creates orders for assets and allows users to collect these orders.
### Actions
The following diagrams shows the sequence from the `pallet-foreign-investments` point of view and which LP messages are sent/received.
#### Investments
```plantuml
@startuml
skinparam sequenceArrowThickness 2
skinparam roundcorner 20
skinparam sequence {
LifeLineBackgroundColor #Business
}
actor "Liquidity Pools\nContact on EVM" as Solidity
participant LiquidityPools as LP << (P, motivation) >>
participant ForeignInvestments as FI << (P, motivation) >>
participant Investments << (P, motivation) >>
participant OrderBook << (P, motivation) >>
== increase ==
Solidity -[#Green]> LP : DepositRequest
activate LP
LP -> FI ++ : increase_foreign_investment()
FI -> FI : increase()
activate FI #Strategy
FI -> OrderBook ++ : create_or_increase_swap()
FI <-- OrderBook --
deactivate FI
alt "if same currencies"
FI -> FI : post_increase_swap()
activate FI #Strategy
FI -> Investments ++ : update_investment()
FI <-- Investments --
deactivate FI
end
LP <-- FI --
deactivate LP
== cancel ==
Solidity -[#Green]> LP : CancelDepositRequest
activate LP
LP -> FI ++ : cancel_foreign_investment()
FI -> FI : cancel()
activate FI #Strategy
FI -> Investments ++ : update_investment()
FI <-- Investments --
FI -> OrderBook ++ : create_swap()
FI <-- OrderBook --
deactivate FI
alt "if no pool to foreign swap or if same currencies"
FI -> FI : post_cancel_swap()
activate FI #Strategy
LP <- FI ++ #Strategy : fulfill_cancel_investment()
Solidity <[#Blue]- LP : FulfilledCancelDepositRequest
LP --> FI --
deactivate FI
end
deactivate LP
LP <-- FI --
== fulfill a foreign to pool swap ==
hnote over OrderBook : Order partially fulfilled
FI <- OrderBook ++ : fulfill()
FI -> FI : post_increase_swap()
activate FI #Strategy
FI -> Investments ++ : update_investment()
FI <-- Investments --
deactivate FI
FI --> OrderBook --
== fulfill a pool to foreign swap ==
hnote over OrderBook : Order partially fulfilled
FI <- OrderBook ++ : fulfill()
FI -> FI : post_cancel_swap()
activate FI #Strategy
note right of LP : Called only when the\nswap is fully fulfilled
LP <- FI ++ #Strategy : fulfill_cancel_investment()
Solidity <[#Blue]- LP : FulfilledCancelDepositRequest
LP --> FI --
deactivate FI
FI --> OrderBook --
== collect ==
hnote over Investments : Epoch close.\nInvestment partially\ncollected
FI <- Investments ++ : collect()
FI -> FI : post_collect()
activate FI #Strategy
LP <- FI ++ #Strategy : fulfill_collect_investment()
Solidity <[#Blue]- LP : FulfilledDepositRequest
LP --> FI --
deactivate FI
FI --> Investments --
@enduml
```
#### Redemptions
```plantuml
@startuml
skinparam sequenceArrowThickness 2
skinparam roundcorner 20
skinparam sequence {
LifeLineBackgroundColor #Business
}
actor "Liquidity Pools\nContact on EVM" as Solidity
participant LiquidityPools as LP << (P, motivation) >>
participant ForeignInvestments as FI << (P, motivation) >>
participant Investments << (P, motivation) >>
participant OrderBook << (P, motivation) >>
== increase ==
Solidity -[#Green]> LP : RedeemRequest
activate LP
LP -> FI ++ : increase_foreign_redemption()
FI -> FI : increase()
activate FI #Strategy
FI -> Investments ++ : increase_redemption()
Investments -> Investments : update_redemption()
FI <-- Investments --
deactivate FI
LP <-- FI --
deactivate LP
== cancel ==
Solidity -[#Green]> LP : CancelRedeemRequest
activate LP
LP -> FI ++ : cancel_foreign_redemption()
FI -> FI : cancel()
activate FI #Strategy
FI -> Investments ++ : cancel_redeemption()
Investments -> Investments : update_redemption()
FI <-- Investments --
deactivate FI
LP <-- FI --
Solidity <[#Blue]- LP : FulfilledCancelRedeemRequest
deactivate LP
== collect ==
hnote over Investments : Epoch close.\nRedemption partially\ncollected
FI <- Investments ++ : collect()
FI -> FI : post_collect_and_swap()
activate FI #Strategy
FI -> OrderBook ++ : create_or_increase_swap()
FI <-- OrderBook --
deactivate FI
alt "if same currencies"
FI -> FI : post_swap()
activate FI #Strategy
LP <- FI ++ #Strategy : fulfill_collect_redemption()
Solidity <[#Blue]- LP : FulfilledRedeemRequest
LP --> FI --
deactivate FI
end
FI --> Investments --
== fulfill a pool to foreign swap ==
hnote over OrderBook : Order partially fulfilled
FI <- OrderBook ++ : fulfill()
FI -> FI : post_swap()
activate FI #Strategy
note right of LP : Called only when the\nswap is fully fulfilled
LP <- FI ++ #Strategy : fulfill_collect_redemption()
Solidity <[#Blue]- LP : FulfilledRedeemRequest
LP --> FI --
deactivate FI
FI --> OrderBook --
@enduml
```
## Scope
| File | SLOC |
|-------------------------------------------------------------|-------|
| pallets/liquidity-pools-gateway-queue/src/lib.rs | 170 |
| pallets/liquidity-pools-gateway/src/message.rs | 14 |
| pallets/liquidity-pools-gateway/src/lib.rs | 440 |
| pallets/liquidity-pools-gateway/src/message_processing.rs | 362 |
| pallets/liquidity-pools/src/message.rs | 826 |
| pallets/liquidity-pools/src/lib.rs | 934 |
| pallets/liquidity-pools/src/hooks.rs | 96 |
| pallets/liquidity-pools/src/gmpf/error.rs | 35 |
| pallets/liquidity-pools/src/gmpf/ser.rs | 244 |
| pallets/liquidity-pools/src/gmpf/de.rs | 193 |
| pallets/liquidity-pools/src/inbound.rs | 140 |
| pallets/foreign-investments/src/lib.rs | 180 |
| pallets/foreign-investments/src/impls.rs | 168 |
| pallets/foreign-investments/src/entities.rs | 301 |
| pallets/foreign-investments/src/swaps.rs | 115 |
| pallets/order-book/src/lib.rs | 591 |
| pallets/token-mux/src/lib.rs | 267 |
| pallets/investments/src/lib.rs | 1045 |
| runtime/common/src/routing.rs | 96 |
| libs/types/src/domain_address.rs | 89 |
| libs/types/src/tokens.rs | 467 |
| libs/types/src/investments.rs | 125 |
| libs/traits/src/investments.rs | 153 |
| libs/traits/src/liquidity_pools.rs | 82 |
| libs/traits/src/swaps.rs | 64 |
| **Total** | **7197** |
### Out of scope issues
* Centrifuge Chain Runtime configuration of pallets
* However, findings on possible misconfiguration of pallets in scope will be considered
* Any issue that requires governance or admin actions
* Adding assets is controlled by CFG governance, and governance will only add standard tokens (limited to reasonable decimals i.e. <= 18), no rebasing tokens, fee-on-transfer tokens, tokens with callbacks, etc.
* Any issues from Solidity LP audits that also apply to logic of Rust code
* Any sections of the files in scope under `#[cfg(test)]` or feature-gated by `runtime-benchmark`
* Overestimated weights
* Forwarding not implemented on Solidity side
* Tranche tokens can be stuck if a cross-chain transfer is performed to a destination that is not a member, or an invalid domain, or an invalid address
* Gas for routing is not paid automatically
* Batches can be submitted that are overweight on the destination domain
* Liquidity can be locked if a pool admin disallows all assets
* Investments are only fulfilled if the `collect_investments/redemptions_for` method is triggered by a user or bot on Centrifuge Chain
## How to build
The Liquidity Pools protocol involves interaction between Solidity contracts on the EVM side and the Centrifuge Chain. Setting up both environments locally is beyond the scope of this audit. However, if you are interested in experimenting with the setup, we recommend reviewing [our LP integration tests](https://github.com/centrifuge/centrifuge-chain/blob/main/runtime/integration-tests/src/cases/lp/investments.rs#L412), which cover the complete LP setup, including the EVM contracts. For isolated unit tests, you can take a look at the `tests.rs` files of the pallets in scope. However, note that tests and mocks are not in scope.
### Using Chopsticks
If you still wish to run the Centrifuge Chain locally, we suggest using [Chopsticks](https://github.com/AcalaNetwork/chopsticks?tab=readme-ov-file#quick-start) for that purpose. We have recently updated our Centrifuge config. Unfortunately, this has not been released (current version [v0.13.3](https://github.com/AcalaNetwork/chopsticks/releases)) such that you need to download that config locally from the Chopsticks repository [here](https://github.com/AcalaNetwork/chopsticks/blob/master/configs/centrifuge.yml).
```bash
npx @acala-network/chopsticks@latest -c configs/centrifuge.yml
```
The config funds the well-known `Alice` key with 1M CFG.
```bash
Secret Key URI `//Alice` is account:
Network ID: substrate
Secret seed: 0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a
Public key (hex): 0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d
Account ID: 0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d
Public key (SS58): 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
SS58 Address: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
```
### How to run integration tests
:::info
:bulb: LP integration tests require Foundry ([official documentation](https://book.getfoundry.sh/getting-started/installation#installation)) to be installed on your system because otherwise the LP contracts from the git submodule `liquidity-pools` cannot be built leading to the error message from the `LP_SOL_SOURCES` constant:
> Build script failed to populate environment variable LP_SOL_SOURCES pointing to missing solidity source files in the 'target/*/build/integration-tests*/out' directory required for EVM integration tests.\n\nPlease check if you have pulled the 'liquidity-pools' submodule via `git pull --recurse-submodules` and if you have installed the forge cli, e.g. check `forge -V`.");
Moreover, you need to initialize the submodule via
```
git submodule update --init --recursive
git pull --recurse-submodules
```
:::
Please note that we do not recommend running all integration tests since that will take a while but rather focus on a single one to save time:
```bash
# Run all LP integration tests
cargo test -p runtime-integration-tests cases::lp::
# Run an isolated LP integration tests
cargo test -p runtime-integration-tests [<test-name>]
```
### How to run unit tests
```bash
# Run all pallet tests
cargo test -p <pallet-name>
# Run an isolated UT
cargo test -p <pallet-name> [<test-name>]
# Run all unit tests
cargo test --workspace --release --features try-runtime --exclude runtime-integration-tests
```