Modern Stablecoins, How They're Made:
F(x) Protocol 2.0

Author(s): Sergey Boogerwooger, Artem Petrov
Security researcher(s) at MixBytes
Intro
We continue our series on stablecoins, and today's focus is the f(x) protocol, version 2.0. This is an upgrade to the previous 1.0 version. The stablecoin is named fxUSD and includes many improvements in its design and peg-maintenance mechanism.

The f(x) protocol uses a stabilization scheme based on price bands, liquidations, redemptions, reserve pools, and various additional stability measures. Certain elements of this scheme resemble mechanisms seen in the Liquity protocol (such as redemptions targeting the most "risky" positions and the Stability Pool). Others are similar to the concentrated liquidity concept observed in Uniswap V3, while some evoke rebalancing operations in Fluid DEX. Certain aspects are particularly innovative, including the use of flashloans to leverage positions.

Moreover, f(x) integrates extensively with external protocols such as Aave, Curve, and Morpho. Users of f(x) protocol earn sustainable yield and benefit from a unique position leverage mechanism.

These mechanics are highly interesting to analyze, so let’s dive in.
Higher-level review
The f(x) protocol uses multiple pools, each operating with a single collateral asset like USDC/USDT or ETH/stETH/wstETH, and interacts with the fxUSD stablecoin. Users create collateral-debt positions (xPOSITIONs) and mint fxUSD. One key term used in the protocol documentation is NAV (net asset value)— the "price" of fxUSD tokens and xPOSITION shares in terms of collateral. This ratio is fundamental for estimating the value of owned fxUSD, xPOSITIONs, and for the stabilization mechanism, which adjusts these NAVs to maintain the fxUSD peg.

The primary invariant used in the stabilization of fxUSD is:

Maintaining this invariant ensures that fluctuations in the collateral price s (on the left-hand side) are offset by adjustments on the right-hand side through changes to the NAV of fxUSD(nf) and xPOSITIONs(nx). Here, xPOSITIONs act as "absorbers" of volatility, minimizing its impact on the fxUSD price. This concept originates from f(x) protocol 1.0. The 2.0 version introduces "leveraged" positions, enabling users to manage collateral-debt positions with leverage ranging up to 10x depending on the asset, making the protocol a powerful tool for trading. Also, these leveraged positions have a greater impact on the stabilization actions of market makers.

To understand this further, we need to define "xPOSITION leverage," as there are two types of leverage ratios in the f(x) protocol:

  • Real-Time xPOSITION Leverage: This is the actual ratio between minted fxUSD and the collateral asset, based on the current collateral price. This leverage functions similarly to a "collateral ratio" in other protocols. Changes in this leverage ratio trigger rebalancing, redemption, or liquidation procedures to maintain the fxUSD peg.
  • Target (User-Defined) xPOSITION Leverage: When users open an xPOSITION in the f(x) protocol, they select a leverage ratio (up to 10x). This determines how much fxUSD they can mint relative to their collateral. For example, if a user opens a $200 xPOSITION at 10x leverage, they provide $200 in collateral and can mint up to $1,800 fxUSD ($2,000 = 10 × $200 = $1,800 + $200). This is achieved using an external flashloan, which adds more collateral to the position and is instantly repaid with minted fxUSD. This approach enables users to trade collateral assets with higher exposure and less upfront capital. We will explore this intriguing feature in greater detail after reviewing the core contracts and stabilization mechanics.

When the real-time leverage ratio reaches the rebalance threshold, a rebalance operation is triggered to adjust it back to the threshold. If the real-time leverage ratio surpasses and reaches the liquidation threshold, the system initiates liquidation, redeeming all fxUSD tied to the affected position. These thresholds are calibrated for each collateral asset to account for its volatility and associated risks. If the total collateralization ratio drops below 100%, the f(x) protocol halts the creation of new xPOSITIONs but still allows existing ones to be closed.

In the f(x) protocol, xPOSITIONs are organized into "leverage ticks" (similar to price ticks in Uniswap V3). Collateral price movement triggers rebalancing operations within these ticks.

Another key stabilization mechanism is redemption, which allows users to exchange fxUSD for collateral at a $1 price. This option becomes appealing when fxUSD’s price falls below $1. Redemptions prioritize the highest-leverage xPOSITIONs first, eliminating riskier positions and stabilizing the fxUSD price simultaneously.

The stabilization mechanism also includes the Stability Pool, which holds fxUSD and USDC balances. Its critical function in stabilization is supplying fxUSD and USDC tokens for xPOSITION rebalancing and liquidations. These operations occur when the stablecoin price exceeds $1 (when collateral becomes cheaper), incentivizing actions that drive the stablecoin price down. However, such operations, especially liquidation (or its "lighter" counterpart, rebalancing), require sourcing stablecoins — often through external markets — driving their price up instead. To counter this, the f(x) protocol first uses fxUSD tokens from the Stability Pool (avoiding reliance on external markets) and only resorts to external sources when the Stability Pool is exhausted.

Additionally, the f(x) protocol includes a Reserve Fund, funded by a portion of fees and protocol revenue. This reserve is intended as a safeguard during adverse scenarios, shoring up protocol stability in the face of potential issues. If bad debt arises and cannot be covered by funds in the Reserve Fund, losses are "socialized" (proportionally redistributed across all xPOSITIONs).

F(x) V2 also introduces a market for fTokens and xTokens — derivatives of fxUSD and xPOSITIONs where fTokens offer additional yield for users.

The protocol integrates with several external DeFi projects: Aave (for additional fxUSD interest), Morpho (for zero-fee flashloans), and Curve (for fxUSD/USDC price oracles and rebalancing swaps).

Now, let’s examine how all this works in the code.
Core
PoolManager
The entry point to the protocol's pools is the PoolManager.sol contract, which manages multiple pools and routes the processes of opening, closing, adjusting, and liquidating positions to the relevant pools. Each pool (example) has its own specific implementation and operates with its own collateral token, price oracle, fees, and other parameters.

The core operational structure is the PoolStruct, which tracks the current and maximum (capacity) amounts of collateral and debt tokens. The collateral amount is composed of two parts: "raw" collateral and collateral adjusted with the token rate (deposited part). Every function that interacts with xPOSITIONs and modifies debt or collateral updates the PoolStruct. This struct is crucial for calculating the pool's collateralization ratio. The main functions of PoolManager - operate(), redeem(), rebalance() and liquidate() - are responsible for adjusting global pool amounts, transferring tokens into or out of the protocol, and applying fees. The "in-pool" mechanics of these operations are implemented in the specific instance of each pool.

Opening and adjusting xPOSITIONs are performed through the operate() function. Internally, this function calls the operate() function of the relevant pool, updates the pool's debt and collateral values, collects protocol fees, and mints or burns fxUSD tokens. During position creation, after all calculations and checks of debt and collateral are completed, the operation "settles" the position into a specific "tick." Ticks represent a particular debt-to-collateral ratio. We will explore ticks further later.

Other key functions — redeem(), rebalance(), and liquidate() — operate in a similar manner: they delegate execution to the specific instance of a pool, update global pool parameters, and perform the necessary transfers, mints, or burns. For instance, transfers and burns occur after a redeem() call, or hooks are executed after rebalancing or liquidation. These hooks will be discussed in more detail later.

Another important function is harvest(), accessible only by addresses with the HARVESTER_ROLE. This function collects three types of fees from the protocol: performanceFee, harvestBounty, and pendingRewards. These fees are then distributed: the performanceFee goes to the treasury, the harvestBounty is sent to the harvester address, and pendingRewards are allocated for the incentivization of external fxUSD AMM pools.

Governance controls the registration of new pools, sets their maximum capacities, updates the reward splitter, configures rate providers for tokens like stETH, and defines liquidation thresholds.

Now it's good to review the fxUSD token contract, which is widely used by all f(x) protocol contracts.
FxUSD token (FxUSDRegeneracy)
The FxUSDRegeneracy contract implements the FxUSD token. fxUSD is a fractional token, structured across multiple markets. Each market is controlled by an FxMarketStruct, where users hold fToken shares in that market. The contract also tracks stablecoin reserves (e.g., USDC) via the StableReserveStruct.

An important part of stabilization mechanism is the _checkMarketMintable() function. This check blocks the minting of shares (and thus the creation of new fxUSD) if the collateral_ratio is below the stability_ratio. Specifically, if "collateral is insufficient relative to fxUSD," minting new fxUSD is denied. Conversely, when "fxUSD supply is low and collateral is sufficient," minting new fxUSD is allowed.
BasePool
The code of BasePool is the core component of the f(x) protocol, implementing mechanisms for opening and adjusting xPOSITIONs, as well as handling redemptions and liquidations. BasePool is then inherited by specialized pools, such as AaveFundingPool. This design allows the pool to work with any asset that has a reliable price oracle for calculating collateral-debt ratios. Pools must override and implement the _updateCollAndDebtIndex() and _deductProtocolFees() functions to manage collateral, debt, and fees independently. BasePool itself is responsible for minting and burning fxUSD, operating with collateral-debt positions using a "concentrated liquidity" pattern (with price ticks) and rebalancing the fxUSD/collateral reserves.

The main functions of BasePool, detailed below, are accessed through PoolManager, where all token transfers, minting/burning operations (in FxUSDRegeneracy), and updates to global "per-pool" parameters occur in pre- and post-operation hooks. Meanwhile, BasePool operates on its internal positions, ticks, amounts, and shares.

One of the key functions is operate(), which creates or adjusts xPOSITIONs. By providing newRawColl and newRawDebt parameters, this function can add or remove collateral and debt assets. The first step involves receiving the oracle price of a given collateral asset. Each xPOSITION is associated with a specific price tick, and changes in collateral, debt, or oracle price may move the position to a different tick. So, at the start, a position is first removed from the current tick.

Each xPOSITION is "measured" in terms of collateral and debt shares rather than raw amounts. That's why all operations with raw amounts of collateral and debt require a conversion from shares to amounts (like here). This "shares-based" approach simplifies operations on groups of positions, handles rebaseable assets like stETH, and allows fees or operation surpluses to be distributed proportionally across the total supply of pool tokens, effectively increasing the holdings of token owners.

After verifying the liquidation threshold (which prevents borrowing or withdrawals beyond limits), the protocol executes collateral operations, applies fees, updates collateral shares, and then performs debt operations, which adjust debt shares.

Finally, the protocol checks whether the debt ratio of the position falls within valid undercollateralization levels. The updated position may have a new collateral-debt ratio, associating it with a different price tick, where it is added. The position's details are updated, and the pool's global collateral and debt amounts are refreshed.

To maintain the health of the pool, the f(x) protocol employs two mechanisms to lower the debt ratio of positions and ticks. The "lighter" mechanism is rebalance(), used when a tick (which groups multiple positions) or an individual position has a debt ratio greater than rebalanceDebtRatio but less than liquidateDebtRatio. This mechanism prevents critical states by recalibrating debt ratios early. In cases where collateral price changes are gradual, rebalancing can entirely avoid liquidations by gradually adjusting positions and ensuring the protocol remains overcollateralized.

A single position encompasses a share of the "total tick debt" and "total tick collateral," meaning the logic for single positions and ticks is quite similar. The "tick"-level version operates on total tick amounts and shares, while the "position"-level version operates on individual position amounts and shares. In both cases, shares are converted to raw amounts (here or here), and, after verifying debt ratios, the necessary amount of debt to be converted into collateral is calculated using _getRawDebtToRebalance(). This function addresses the question: "How much debt should be removed to achieve the target debt ratio?" Once the target debt and collateral amounts have been converted back to shares, the function behavior diverges slightly:

  • The "position-level" version, after being removed from its original tick, is added to a tick aligned with the new debt ratio.
  • The "tick-level" version uses _liquidateTick() to adjust and redistribute rebalanced share amounts across ticks (moved to the next tick).

This "shares based" logic enables rebalancing of multiple positions in a single tick simultaneously, without directly modifying individual positions. The debt and collateral shares of all user positions within a tick remain unchanged, but the "totalSupply" of collateral and debt shares in the new tick differs, adjusting each user’s debt-collateral ratio accordingly.

The next step involves the liquidate() function. This function can only be applied to specific positions and only when the debt ratio exceeds liquidateDebtRatio. Liquidation is triggered when collateral price changes are significant and rebalance() fails to address the issue in time. Similar to rebalancing, the position is removed from its tick and the necessary debt and collateral shares are recalculated (including a liquidation bonus).

A critical aspect of liquidation is redistributing potential bad debt. In the f(x) protocol, bad debt is "socialized," meaning it is subtracted from the total pool debt. This operation increases the pool's total debt index, which is an unavoidable aspect of this process.

The functions rebalance() and liquidate() utilize fxUSD and USDC from the Stability Pool. This mechanism ensures that liquidations and rebalancing do not affect fxUSD's price on the external market, which is critical to maintaining stability. Excess fxUSD is burned, and any uncovered rest is covered with USDC from users.

The function redeem() is another key part of the stabilization mechanism. It operates at the pool level, redeeming debt by swapping it into collateral when fxUSD is undervalued external to the protocol (always being profitable for redeemers). Redemptions may be paused for governance-related concerns about the fxUSD peg and are disallowed when the pool's debt ratio exceeds 1.

To summarize, BasePool manages these key functions:

operate(): creating and adjusting xPOSITIONs.
redeem(): swapping debt into collateral when debt is undervalued.
rebalance(): calibrating debt ratios when they exceed safe limits.
liquidate(): addressing critical debt ratio states.

Stabilization functions aim to utilize the Stability Pool whenever possible, ensuring fxUSD price stability in the external market.
AaveFundingPool
The AaveFundingPool is a specialized pool derived from BasePool, designed to interact with the Aave protocol and utilize Aave's interest rates for fxUSD.

The function _updateCollAndDebtIndex() updates the interest rates according to Aave's debt, receiving interest from Aave's borrowers. When the external fxUSD price becomes too low, the AaveFundingPool enables funding: funding fees are applied to xPOSITION holders and are delivered to the Stability Pool, making it more attractive to USDC and fxUSD deposits, which, in turn, increases the collateral index. This adjustment reduces total amount of collateral shares, increasing the "collateral-per-share" value for users, protecting the protocol from burning cheaper fxUSD for collateral.
Fees are handled at the pool level in the _deductProtocolFees() function. AaveFundingPool uses two distinct fee rates: one applied when collateral is added and another applied when it is removed. These fees are set by governance.v
FxUSDBasePool
This pool implements the Stability Pool, a special pool designed to operate with the fxUSD and USDC stablecoins. It does not rely on price oracles, except for one used to detect USDC depeg events, which blocks operations in the pool (as seen here).

Unlike the BasePool, this pool does not manage positions. Instead, it simply mints shares to users who deposit fxUSD or USDC into the pool. This is accomplished via the deposit() function, which transfers fxUSD or USDC tokens from the user and mints pool shares. The internal _deposit() function is then used to calculate the final amount of shares based on the type of token deposited (fxUSD or USDC).

The redeem procedure in the Stability Pool differs from that in a BasePool. The Stability Pool does not use price ticks. Instead, the redeem() process burns the user's shares and returns the underlying tokens to them according to the pool's current disribution.

Additionally, the redeem() function in the Stability Pool can only be called after a cooldown period by first creating a redeemRequest() (one per user). This "lockup" period allows the protocol sufficient time to manage mass redemptions effectively, enabling market makers to observe significant user activity that indicates exits from the Stability Pool.

Rebalancing and liquidation procedures are unnecessary in the Stability Pool. They are not relevant for two stablecoins that are pegged to the same value, where a USDC depeg event halts all operations. While the rebalance() and liquidate() functions are present in the code of FxUSDBasePool.sol, they merely pass execution to the PoolManager and function only when used by another pool. The FxUSDBasePool.sol itself lacks implementations for these procedures.

An important component of the Stability Pool's logic is the arbitrage() function, which can only be called by the PegKeeper (discussed below). This function allows swapping between fxUSD and USDC via PegKeeper, which uses an external AMM for this purpose. The goal is to adjust fxUSD and USDC amounts in the pool when significant imbalances occur. The fxUSD tokens in the Stability Pool are essential for rebalancing and liquidations. Their availability ensures these operations can proceed without needing to acquire fxUSD from external markets, which could drive up its price and increase protocol debt.
PegKeeper
The PegKeeper contract forms part of the stabilization mechanics. It interacts with Curve's StableSwapNG pool for fxUSD and provides the fxUSD price from the external market using the EMA price from Curve's pool's price_oracle() function. This price is compared with the "fxUSD depeg price" to allow/deny borrowing of fxUSD and allow/deny funding in the AaveFundingPool.

This is a permissioned contract with two main roles: "buyback" and "stabilize," which are activated during fxUSD peg issues. The stabilize() function in PegKeeper swaps fxUSD and USDC within Curve's AMM to rebalance the fxUSD/USDC amounts in the AMM and performs the swap operation.

The buyback() function, on the other hand, withdraws USDC from the FxUSDRegeneracy stable reserves and swaps it for fxUSD using Curve's AMM. It then burns the fxUSD, creating buying pressure to increase its price and reduce its supply.
Stabilization Mechanism
After reviewing the key contracts, let's examine the functions involved in the stabilization mechanism.

  • The buyback() function is initiated by the PegKeeper. It uses accumulated USDC to purchase fxUSD from the Curve AMM and burns the fxUSD. This reduces USDC reserves, decreases managed fxUSD debt, and raises the fxUSD price.
  • The stabilize() function in PegKeeper rebalances the fxUSD/USDC amounts in the Stability Pool, reducing fxUSD supply and increasing the fxUSD price.
  • The arbitrage() function ensures the Stability Pool has sufficient fxUSD for rebalancing and liquidations, stabilizing the fxUSD/USDC price within the protocol.
  • The wrap() function prevents minting new fxUSD when the fxUSD price is low, thereby helping to reduce supply and increase the price. It operates in reverse when the fxUSD price is high.
  • The rebalance()/liquidate() functions primarily use fxUSD from the Stability Pool (instead of external markets), preventing downward pressure on the fxUSD price. These functions begin with the top tick, which contains the riskiest positions with the highest leverage ratios.

The f(x) protocol also includes components such as the ReservePool for "emergency" actions, a treasury system, and incentivization mechanisms for fxUSD. However, these elements are beyond the scope of this article.
Positions leverage
Now, after reviewing the stabilization mechanism, it's time to explore a very powerful feature of the f(x) protocol — Target xPOSITION Leverage. Minting fxUSD when a position is created enables an intriguing mechanism. Let's consider a "zero-sum" example (excluding fees, ETH price fluctuations, and other complications):

  • A user deposits 1 ETH using "open position with flashloan" and selects a 10x leverage.
  • The f(x) protocol interacts with an external protocol (e.g., Morpho, Balancer, etc.) to take a flashloan of 9 ETH (1 ETH + 9 ETH = 10 ETH // 10x leverage).
  • The user mints 20,000 fxUSD (collateralized by 10 ETH).
  • The 18,000 of borrowed fxUSD are immediately swapped back to 9 ETH, repaying the flashloan.
  • The user is left with 2,000 fxUSD and a 10 ETH collateral position.

Time passes…

  • The user closes their position using "close position with flashloan" (with a debt of 18,000 fxUSD).
  • The f(x) protocol takes a flashloan of 9 ETH, which is swapped to USDC and then to the fxUSD in the Curve AMM
  • fxUSD is redeemed for 9 ETH and "flashloaned" 9 ETH is payed back
  • 1 ETH is returned to the user.

In cases where ETH prices fluctuate, this mechanism lets users open long positions by leveraging much more collateral than they actually possess, while risking only their initial 1 ETH. This approach showcases how leverage schemes used in traditional finance can be implemented in DeFi.

The function openOrAddPositionFlashLoanV2() opens such a position, while the function closeOrRemovePositionFlashLoanV2() closes it. Both utilize the _invokeFlashloan() function, which can work with any flashloan contract. In the current version, it uses Morpho, which does not charge any flashloan fees.
Fees
Now, let's examine the various fees involved in the protocol.

All accumulated fees within the protocol flow through a designated RevenuePool contract, which distributes them to Stability Pool stakers, external veFXN token holders (Curve), Aave pools, and the treasury.

The primary fee is applied when users deposit or withdraw collateral, such as in the operate() function (see here). The fee amount is determined by the specific pool's implementation. For instance, the currently used AaveFundingPool protocol introduces an "opening" fee and a "closing" fee, both set at the pool's initialization phase. These fees take a fraction of the added or removed collateral.

Next fee is the "redemption" fee, applied in the redeem() function. This fee is determined by governance and also takes a fraction of the collateral.

Another fee pertains to minting fTokens/xTokens in MarketV2. This fee structure involves two tiers: one ratio applies to amounts below a threshold, while a different ratio applies above it. MarketV2 also applies fees during fToken/xToken redemptions, which function similarly to the minting logic.

Additionally, there is a fee, related to the harvest() function. This function collects accumulated rewards, funding, and performance fees within the pool. These fees include a performanceFee that goes to the treasury and a harvestBounty that goes to the user performing the harvest.

The f(x) protocol is integrated with various external DeFi protocols, all of which also impose their respective fees.
MarketV2
This contract is another essential component of the f(x) protocol, providing yield to users through yield-bearing shares: fToken and xToken. These represent "fxUSD-derived" and "xPOSITION-derived" tokens, respectively. fToken offers a more conservative yield with lower risk, representing a fractional claim on the underlying collateral (which can be added to or removed from the protocol). On the other hand, xToken represents leveraged positions in the protocol, promising higher potential returns at a higher risk. As shown previously, leveraged positions operate with significantly larger amounts, incurring more fees. However, these positions are more volatile, and their liquidations have a greater impact on the yield of xToken compared to fToken.

The fToken functions are represented by mintFToken() and redeemFToken(). These functions make use of a pre-set parameter stabilityRatio, which controls the maximum amount of inflow or outflow of the baseToken (USDC) during mint/redeem operations. These functions interact with the treasury (where all fees are collected) to accept/send baseToken (USDC) and mint/burn fTokens (see here and here).

The minting of xToken operates similarly to fToken. Both tokens utilize the redeem() function from the treasury, which returns the underlying assets along with their accumulated yield.
Implementation details
F(x) protocol does not incorporate "timestamp-related" indexing within its core mechanics. There are no "linearly increasing" values in user positions or incentives. Timestamps are utilized solely as "heartbeats" to update Aave reserves snapshots, retrieve the fxUSD price, and determine the lockup period for redeem requests. Rewards, which serve an additional role, are not discussed here.

As with other protocols (a topic we frequently reference), mathematical functions apply rounding in both directions, both upwards and downwards. This ensures the protocol benefits in edge cases, avoiding issues related to small or large values during the calculation of shares, amounts, and ratios.

Additionally, binary ranges are employed for efficient storage usage. For example, see PoolStruct and the usage of this structure.

The f(x) protocol includes specialized view functions, such as previewDeposit() and previewRedeem(). These allow users to preview the results of their actions within the protocol before execution.
Conclusion
The f(x) V2 Protocol is a fascinating example of a stablecoin with stabilization mechanics that involve dynamic rebalancing, redemptions, liquidations, specialized stability pools, and user positions in a "concentrated liquidity"-like structure.

It integrates traditional lending primitives with staking, shares, and collaborations with external DeFi projects. Additionally, it offers an impressive leverage mechanism for user positions, made possible through flash loans.

This protocol is undoubtedly one of the most complex stablecoins. Essentially, the f(x) protocol serves as a versatile DeFi toolset, centered around its primary instrument, the fxUSD stablecoin. It is definitely a subject worth studying for DeFi developers and auditors.

See you in our next articles!
  • Who is MixBytes?
    MixBytes is a team of expert blockchain auditors and security researchers specializing in providing comprehensive smart contract audits and technical advisory services for EVM-compatible and Substrate-based projects. Join us on X to stay up-to-date with the latest industry trends and insights.
  • Disclaimer
    The information contained in this Website is for educational and informational purposes only and shall not be understood or construed as financial or investment advice.
Other posts