A branch is a process that occurs within ranges where the collateral price decreases and liquidations become necessary. When a branch is active, the liquidation of positions "belonging" to this branch can be performed. Branches may be merged with others; for example, Branch 2 is merged into Branch 1 when its base level is reached. This process is better described in the whitepaper, while our goal is to review the implementation. Therefore, let's examine the
branchData struct. The most critical elements are the
minimaTick - the minimal tick for this branch, making this branch obsolete or merged when reached - and the branch
debt. Branches facilitate the monotonic and sequential elimination of the debt from unhealthy ticks.
For all operations the Vault protocol uses a single entry point: the
operate() function. Although it shares its name with the
operate() function at the liquidity layer, it operates differently by interacting with an NFT that represents a user's
position. This operation is "universal" for deposit/withdraw and borrow/payback, so, all the changes during the operations are tracked in the special operation structure
OperateMemoryVars, which accumulates information about the previous and next ticks of the user's position. As liquidations in Vault don't access individual users' positions data directly, this data must be updated whenever the position is accessed. For instance, if a position's tick
becomes subject to liquidation, it needs to be updated to the 'nearest available' tick at the
end of the operation.
The supply and borrowing operations in the
operate() function write new
colRaw and
debtRaw fields into the operation variables (
here and
here), calculating the raw values of the position debt/collateral using
borrowExPrice and
supplyExPrice. The next step, which demonstrates work with ticks, is the
part where the debt is being added to some tick, using the
_addDebtToTickWrite() function. The conversion from the ratio to the tick number requires the calculation of a logarithmic function. This calculation is done using precalculated values in
tickMath.sol, similarly to Uniswap V3.
The mechanics of assigning different ratios to different ticks using "not-exact" amounts of tokens lead to the occurrence of dust in the protocol, so, in Fluid, a part of logic requires handling these "dust" values. For example, the same
_addDebtToTickWrite() function, described above, also returns a
rawDust_ value that must be later
saved in the user's position to keep all values in sync. Finally, the dust is absorbed by the protocol
here and then can be processed using the admin function
absorbDustDebt().
But, of course, the most interesting part of the Vault protocol review is the
liquidate() function, allowing to liquidate positions of multiple users at once. First, the liquidation
updates borrow/supply prices, and this represents another important connection with the base liquidity layer, providing supply/borrowing exchange prices for multiple protocols (
here).
After some oracle checks (which will be described below), the Vault
absorbs the debt in the ticks above the tick being liquidated. Additionally, the
liquidate() function can liquidate the bad debt from the protocol if the
absorb_ flag is set.
The core of liquidation is the
loop over the ticks, which
accumulates debt and collateral on each iteration. Each iteration "belongs" to a current liquidation branch, "tracking" the ticks being liquidated. A
refTick is determined
here, which is later used to move the liquidation tick forward (
here and
here with consideration for some edge cases.
The calculation of the resulting debt/collateral in a single tick with the already known
ratio is outlined
here. After that the current tick is advanced (as shown above), and then the information for the current branch is updated (
here and
here).
There was no access to any of the users' individual positions during the liquidation process! The code operates solely with ticks and branches. This is a key feature of the Vault protocol: the protocol rebalances the collateral/debt ratio seamlessly, for all users' positions simultaneously, without "touching" them.
From algorithmic perspective, this mechanism acts similarly to the Uniswap V3 fees mechanics, where the liquidity provision and fees accumulation are managed using only ticks' internal data and global accumulators (described in the
article), without direct interaction with the liquidity providers' positions. However, it's important to distinguish between the protocols. In Fluid, ticks manage collateral/debt ratios, not prices, and they exclude global fee accumulators. The mechanics of the ticks also differ.
It seems like the idea of splitting the ranges of prices, ratios, amounts (and we don't know what else yet) into discrete ticks makes sense in DeFi, allowing to avoid "per position" interactions with DeFi protocols.
This design is exceptionally gas-efficient for all users: borrowers/suppliers and traders/liquidators. Liquidations work seamlessly, with additional restrictions on price and volumes movement, "enveloping" the full spectrum of users' positions by liquidation "waves". True to its name, the protocol is indeed "Fluid" :)