Understanding Fluid: Key Concepts and Practices

Financial Quantities

  • Raw Amounts: These are preliminary figures that require multiplication by an exchange price to yield the actual token amounts (e.g. with interest mode amounts)
  • Normal Amounts: These figures represent the final token values directly, requiring no further adjustments to reflect accurate balances (e.g. interest free mode amounts).

User Definition

  • In the context of the Liquidity contract, a User typically refers to a protocol layer constructed atop it, such as an fToken or a Vault. This designation underscores the intermediary role these constructs play between the Liquidity contract and the end-users.

Optimization and Addressing

  • Gas Efficiency: A cornerstone of Fluid's design philosophy is the prioritization of gas optimization. This focus influences various aspects of the codebase, enhancing performance and reducing transaction costs.
  • Native Token Representation: The placeholder 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE is utilized to denote the native blockchain currency (e.g., ETH, MATIC) within transactions, serving as a universal identifier.

Error Handling in Fluid

Our approach to error management is systematic and uniform across Fluid's codebase, facilitating efficient debugging and code maintenance. Each error within the system is associated with a unique code, allowing developers to quickly pinpoint the root cause of issues. For a detailed understanding of our error structuring, please refer to the Error Handling Guide.

Enhancing Efficiency through Gas Optimizations

Memory Management

To optimize for gas consumption, we strategically utilize the temp_ prefix for variables within our code. This practice involves reusing the same memory slot for multiple purposes, significantly reducing the need for additional memory allocations. We strike a balance between gas efficiency and code clarity, supplementing our code with comprehensive comments to maintain readability despite the complex memory management techniques.

Storage Variable Packing

In an effort to further minimize gas costs, Fluid's codebase employs a technique known as storage variable packing. This method is meticulously applied, especially within critical contracts like Liquidity & Vault, to compact data at the bit level:

  • Writing to Storage: To update stacked uint values in storage, we first clear the necessary bits using a bitwise AND (&) operation with a mask. Subsequently, values are combined using a bitwise OR (|), with each value positioned correctly through bit shifting (<<).
  • Reading from Storage: The retrieval of stacked values involves shifting the data to align with the start position (>>) and then applying a mask (&) to extract the precise value.

These optimization strategies are essential to Fluid's design philosophy, reducing transaction costs and enhancing the protocol's performance without sacrificing code integrity.

Leveraging BigMath for Efficient Storage

Fluid employs the BigMath library to optimize numeric value storage, achieving significant savings on storage requirements while accepting minimal and predictable precision loss. This method involves distilling large numbers into a manageable format of coefficients and exponents, ensuring efficient use of blockchain storage without necessitating overflow checks. Here’s a closer look at how BigMath enhances Fluid’s functionality:

Precision and Rounding

The utilization of BigMath introduces a slight precision loss, a compromise that remains favorable for users due to the protocol's strategic handling of rounding to safeguard liquidity and prevent transaction reverts:

  • Supply Amounts: Rounded down to minimize overestimation of assets.
  • Borrow Amounts: User and total borrow amounts are rounded up, ensuring that obligations are not understated.
  • Limits: Implemented limits are rounded down for conservative operation, except for withdrawal limits, where rounding down instead of up avoids minor discrepancies leading to transaction reverts, without compromising the protocol's security.

Operational Insights

  • Precision Threshold: BigMath's precision is set at 7.2057594e16 for a coefficient size of 56 bits, above which numbers experience precision loss. This design choice ensures that most operations within Fluid are unaffected by rounding errors.
  • Impact of Rounding: The rounding strategy may result in the sum of user borrow amounts surpassing the total borrowed amount. In such cases, corrective measures are applied — for example, setting the total borrow to zero if repayments exceed the outstanding amount. This approach also necessitates seeding new protocols with a nominal initial balance to prevent reverts from negligible imprecision.

BigMath in Action

Discover how to manage BigMath data effectively in the context of Fluid’s operations: Understanding BigMath with LogOperate Event.

Configuration and Token Listing Workflow

Fluid has implemented comprehensive checks and balances to ensure seamless operation and integration of tokens into the Liquidity protocol. These measures are designed to maintain the system's integrity and prevent potential issues related to token values and operational flows.

Safeguards Against High Token Amounts

The Liquidity operate() function is engineered with safeguards that prevent transactions involving exceedingly large token amounts, which could otherwise cause unexpected overflows. Specifically, it rejects values exceeding the maximum integer size of 128 bits (max int 128). Practically, token amounts up to approximately 1e70 are deemed safe, a threshold well beyond the scope of any legitimate token's requirements.

Token Listing Process

Listing a new token within the Liquidity protocol involves a precise sequence of steps to ensure accurate configuration and integration:

  1. Rate Configuration: Establish the rate configuration for the token.
  2. Token Configuration: Proceed to set the token's configuration.
  3. User Access: Finally, enable access for users (protocols) to interact with the newly listed token.

Adhering to this order is crucial; deviations will result in errors, underscoring the importance of following the prescribed workflow for successful token listing.

Lending fToken Creation

The creation of a new Lending fToken, a cornerstone of Fluid's lending protocol, also follows a specific procedural framework:

  1. Asset Configuration: Initiate by setting up the configuration for the underlying asset within the Liquidity protocol.
  2. fToken Deployment: Utilize the LendingFactory’s createToken() method to deploy the new fToken.
  3. Supply Configuration: Finalize by configuring the user supply settings for the fToken within Liquidity.

Native Token Addressing

For operations involving the native underlying fToken, it is imperative to use the official wrapped token address (e.g., WETH for Ethereum, WMATIC for Polygon), rather than the generic placeholder for native blockchain currencies (0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE).

Deployment Scripts

The integration of new fTokens and vaults into the Fluid ecosystem is facilitated by dedicated deployment scripts.

Managing Liquidity Limits

Fluid's Liquidity protocol employs dynamic limits for borrowing and withdrawals to maintain stability and flexibility within the ecosystem. These limits adjust based on user activity and market conditions, ensuring a balanced approach to liquidity management.

Dynamic Borrowing Limits

The borrowing capacity within Fluid is not static; it evolves through a series of predetermined steps:

  • Base to Maximum: Starting at the baseDebtCeiling, the borrowing limit can expand by a set percentage expandPercentage over a defined duration expandDuration until it reaches the maxDebtCeiling.
  • Synonyms: "Debt ceiling" and "borrow limit" are interchangeable terms within this context.

Withdrawal Limits

Withdrawal capacities are similarly adaptive, designed to safeguard the protocol's liquidity:

  • Base Limit: Withdrawals up to the baseWithdrawalLimit are unrestricted.
  • Expansion Mechanism: Beyond the base limit, the withdrawal capacity can increase by a certain percentage of the user's total supply over time, allowing gradual access to funds above the base threshold.
  • Expansion Settings: Deposits help reach the fully expanded withdrawal limit, if the deposit is big enough it can make this happen instantly. Withdrawals trigger further expansion of the withdrawal limit, starting from the lastWithdrawal limit to the fully expanded amount.

Example with configs:

  • Expand percent = 20%
  • Expand duration = 200 sec
  • Base withdrawal limit is 5.

Some scenarios:

  • Multiple withdrawals scenario: Starting with user's deposit is 15 and withdrawal limit before operation is 12 (fully expanded).
  • New withdrawal of 2 -> down to 13. withdrawal limit will expand from previous limit (12) down to full expansion of 13 * 0.8 -> 10.4.
  • Instant withdrawal right after this operation of an amount > 1 would revert.
  • Assuming 100 sec passed (half expand duration). Withdrawal limit would be 12 - (13 * 0.1) -> 10.7.
  • Assuming 150 sec passed. Withdrawal limit would be 12 - (13 * 0.15) -> 10.05, which is below maximum expansion of 10.4 so the actual limit is 10.4.
  • Further withdrawals will trigger a new expansion process.
  • Other scenarios: If user's supply is below 5 then limit will be 0 (meaning user can withdraw fully).
  • New deposit of 5.5: If user supply is 5.5 then withdrawal limit is 5.5 * 0.8 = 4.4 (instantly expanded 20% because of new deposit).
  • New withdrawal of 0.6 down to 4.9: If someone withdraws below base limit then the new limit set at the end of the operation will instantly be 0 and user's can withdraw down to 0. So if the current user supply minus the withdrawable amount is below the base withdrawal limit, this has the effect that essentially the full user supply amount is withdrawable. It just happens in two steps: first a withdrawal to below base limit -> triggers new limit becoming 0 -> full rest amount is withdrawable. This is not super elegant but it allows for a easy implementation that serves the desired purpose good enough.
  • New scenario: user's deposit is 6 and withdrawal limit before operation is 5.5, if someone supplies 0.5 (total supply = 6.5) -> the limit will remain as 5.5 and expand from there to the full 20% expansion -> 5.2.
  • If user's deposit is 6 and withdrawal limit is 5.5 and someone supplies 1 (total supply = 7) -> now the new limit will become 5.6 (20% of total deposits) instantly.

A special case is the first time that the user supply amount comes above the base withdrawal limit: the withdrawal limit immediately becomes the fully expanded withdrawal limit. So this acts like a big deposit that would immediately "fill" the full expanded withdrawal limit. This is a known and acceptable behavior for the withdrawal limit as it does not negatively affect the desired goal of the limit whilst keeping the implementation logic simple.

Borrow Limits

Paybacks help reach the fully expanded borrow limit, if the payback is big enough it can make this happen instantly. Borrows trigger further expansion of the borrow limit, starting from the last borrow limit to the fully expanded amount. Borrow limit has a hard max limit, above which expansion is never possible.

Example with configs:

  • Expand percent = 20%
  • Expand duration = 200 sec
  • Base borrow limit is 5.
  • Max borrow limit is 7.

Some scenarios:

  • User can always borrow up until base borrow limit of 5.
  • New borrow of 4.5 (to total borrow of 4.5): would trigger expansion to above base limit (at fully expanded after 200 seconds 4.5 * 1.2 = 5.4).
  • Assuming full expand duration passed, new borrow of 0.5 to total borrow of 5 -> triggers expansion from 5.4 to 6 (full expansion: 5 * 1.2 = 6).
  • After half duration passed, borrow limit would be lastBorrowLimit + halfExpandedLimit = 5.4 + 0.5 -> 5.9.
  • Assuming full expand duration passed, new borrow of 1 to total borrow of 6 -> triggers expansion from 5.9 to 7.2 (full expansion: 6 * 1.2 = 7.2). Note that this would be above max limit.
  • Even after full expansion, the limit will be hard capped at max limit of 7. User borrow of 1.01 to 7.01 would revert.
  • Assuming full expand duration passed, new borrow of 1 to max limit 7 total borrow. No higher borrow amount is ever possible (unless max limit config is changed).
  • Payback of 1.5 down to 5.5 total borrow. Shrinking to fully expanded borrow limit of 5.5 * 1.2 = 6.6 is immediately active.
  • Assuming no time passed -> borrow of 1.11 would revert.
  • Assuming no time passed -> Borrow of 0.1 to total borrow of 5.6 triggers expansion from 6.6 to 6.72 (full expansion: 5.6 * 1.2 = 6.72).
  • After half duration, borrow limit would be 6.6 + 0.56 -> 7.16 which is above fully expanded so limit will be 6.72.
  • Payback of 5.6 down to 0. Shrinking of new borrow limit to base limit of 5 happens instantly.

Vault protocol

See Whitepaper: https://fluid.guides.instadapp.io/vault-protocol-whitepaper