The core functions of Fluid DEX are the
_swapIn() in
main.sol and
_swapOut() in
perfectOperationsAndSwapOut.sol, which are the most complex functions in the protocol. Let's start from the
_swapIn().
Fluid DEX restricts operations that change the swap price by more than 5% (another "fluid" part), performing the
_priceDiffCheck() in the last
part of the swap operation (in the
_updateOracle() function, which we will discuss later).
The price model of Fluid DEX is also "fluid," as all price movements(even governance changes of ranges and thresholds) are calculated with respect to previous price changes and performed gradually. Price values and ranges begin a continuous movement towards new value when range limits are reached. This requires tracking previous prices, shifting time, status of price shift, and other variables, controlling "dynamic" behaviour of pools. The state of the dex, containing this data, is stored in two main variables:
- dexVariables: Here, the focus is on two previous swap prices of the pool and the time difference between them, the center price (to be described below), and the time of the last interaction.
- dexVariables2: Contains flags (smart collateral/debt enabled), information about fees, and a pack of percentages, determining the range "around" the center price (similar to lower-upper price ranges for the current tick in a concentrated liquidity setting). Another important parameter is the shifting time, determining the rate of price movement (to be discussed below). The next pack of parameters are related to the restrictions on the center price (can be fetched externally, has max/min restrictions, restricted by utilization ratio, etc.). Another notable flag is the pause flag, which disables all "imbalanced" operations; only "perfect" operations can be performed (those that don't change the relative distribution of tokens in the pool).
The swap process parameters are
stored temporarily in the in-memory SwapInMemory struct.
The first stop in the swap process is the
pex_ struct, which holds exchange prices for the protocol. These are calculated by the
_getPricesAndExchangePrices() function, the real "core" of Fluid DEX's swap (to be discussed below). Currently, it is important to note that there are
separate prices for the supply/borrow of token1/token2.
As previously mentioned, operations in Fluid DEX involve
two swaps: in "collateral" and "debt" pools. Therefore, the next step is preparing reserves for both parts of the swap. The
first part is for collateral, and the
second is for debt (the "imaginary" reserves also will be discussed below).
The next
section deals with limiting operation amounts in the pool. We check if the "in" amount being swapped will not significantly affect pool reserves and price. There will be a
_priceDiffCheck() at the end of the swap, but Fluid prefers to perform this check beforehand.
Then begins the section calculating "in" token amounts and their distribution between pools (in one pool, we perform a "deposit", in another - "payback"). We
calculate swap amounts in the
_swapRoutingIn() function, accepting the "in" amount
t along with initial and in/out "imaginary" reserves for both collateral and debt. The result of this function provides a solution for the system of equations, while the result
a determines which part of the swap goes to collateral, debt, or both pools. This solution results in these
three branches, indicating which amounts go to "deposit" and which to "payback".
Next, we need to calculate "out" amounts for "withdraw" in one pool and "borrow" in another. Additionally, we need to check if the required amounts are available. This is done in the "
_amountOutCol" and "
_amountOutDebt" sections.
Next, we decide which pool will be used to determine the swap price:
collateral or
debt. We choose the one having a larger swap amount as the price source, but it is important to remember that, at the end of the swap, the final price of the given asset in both pools will be the same.
After
converting token balances to normal amounts and
setting the callback data (if transferring tokens is made via callback), there are two important operations with the base liquidity layer. The liquidity layer of Fluid (common for all protocols on Fluid) uses a single
operate() function for all actions: supply/withdraw/borrow/payback, determined by two signed parameters:
supplyAmount_ and
borrowAmount_ (negative values of these parameters result in reverse operations). The first operation is:
LIQUIDITY.operate(..., +supply_amount, -payback_amount, ...);while the second is:
LIQUIDITY.operate(..., -withdraw_amount, +borrowAmount, ...);These operations direct all "in" tokens to supply collateral and payback debt, while the "out" tokens allow withdrawing collateral and borrowing debt on the protocol's liquidity ("lending") layer.
Next is a call to an additional hook that can stop the swap if collateral or debt pools require liquidation. We have a lending position for the Fluid DEX on the lending side, and this position can become "unhealthy," requiring liquidation. In such cases, any swaps should be halted before liquidation occurs. The "health" status is checked by this hook, whose interface and description can be found
here. However, the combination of a DEX, careful selection of smart debt/collateral token pairs, and rebalancing of assets in these pools according to external market prices makes this situation unlikely.
The next part of the swap includes
checking the utilization (total_debt/total_collateral ratio) in the liquidity layer. The function
_utilizationVerify() checks if final utilization is
not above the threshold set in the liquidity layer (an example of utilization usage in the liquidity layer's
operate() function is
here).
The final part of the swap involves calling
_updateOracle().
The
swapOut() function in the
perfectOperationsAndSwapOut.sol module performs the same operations as
swapIn(). The code is nearly identical, except for the calculation of out tokens with the
_swapRoutingOut() function and the different order of withdraw/deposit/borrow/payback amounts. The same restrictions, utilization checks, and the
_updateOracle() call at the end of the swap are applied.