Notional

Search…

English

Notional Basics

Risk and Collateralization

Technical Topics

Notional AMM

This document offers a detailed technical description of Notional's liquidity curve

**Dynamic Curve Sensitivity:**The optimal liquidity curve sensitivity (how much slippage a given trade size incurs) will vary vastly as a function of time to maturity. A static curve sensitivity will only be appropriate for a narrow window of time within a fCash token’s lifespan and will make trading uneconomical for the rest of its lifespan.

**Interest Rate Continuity:**The same exchange-rate produces a different interest rate depending on a fCash token’s time to maturity. The interest rate is the more relevant measure of a fCash token’s “price”, and the AMM must keep*this*rate constant between trades, not the exchange-rate. Failing to do so will result in a fCash token’s prevailing interest rate constantly drifting off-market with time. The effect would be minimal throughout most of a fCash token’s lifespan but would blow up exponentially as it approached maturity. The result would be increasingly volatile and unpredictable trading close to maturity and a negative outcome for all involved.

First, we need a curve with the right shape. The curve needs to be relatively flat most of the time so that normal trading conditions produce reasonable levels of slippage. But the curve also needs to be able to accommodate market repricings - if the curve is too flat, or flat throughout, it won’t be able to respond effectively to large changes in the equilibrium interest rate. The logit curve has the right general characteristics. Here’s what it looks like.

This curve maps a 0 to 1 x-range onto an exchange-rate. In order to use this curve, we need a measure of the balance between a given pool’s currency to fCash that sits on a 0 to 1 scale. We can use the following:

$Proportion = totalFCash / (totalCurrency + totalFCash)$

This gives us the following formula for the currency / fCash exchange-rate and the associated implied interest rate:

$Exchange Rate = (1 / scalar) * ln(proportion / (1 - proportion)) + anchor \\ ~ \\ Interest Rate = (Exchange Rate - 1) * periodSize / timeToMaturity$

When a user trades on the Notional AMM, the traded exchange rate is calculated in two steps according to the following formulas:

$tradeProportion = (totalFCash ± tradeSize) / (totalFCash + totalCash) \\ ~ \\ Trade Exchange Rate = (1 / scalar) * ln(tradeProportion / (1 - tradeProportion)) \\+ anchor ± liquidityFee$

Unlike Uniswap, Notional’s AMM does not allow us to easily solve for an exchange rate such that the traded exchange rate equals the prevailing exchange rate after the trade has occurred. The traded exchange rate we use is an approximation of what the exchange rate will be post-trade. Because of this, we need to ensure that this approximation method is suitable for our purposes. The relevant discussion and proofs are included in this document’s appendix.

When a user places a trade, the Notional AMM first updates the mid rate to account for the user’s trade (this is always against the user’s favor) and then adds a liquidity fee on top of that updated mid rate. For example, a given trade might move the prevailing mid interest rate from 5% to 5.25% and execute at 5.5% (a liquidity fee of .25%). This user therefore traded at .5% from the prevailing mid at the time of their trade - .25% from the difference in the mids and .25% from the liquidity fee. So a user’s all-in fee - and a liquidity provider’s all-in charge - can be thought of as a combination of the explicit liquidity fee and the difference between the old mid and the new mid (the slippage). Noting this distinction helps inform the discussion of parameter choice later on in this document.

Consider a Notional liquidity pool between Dai and 1M Dai (fDai maturing in one month’s time) with the following values:

Scalar: 100

Anchor: 1.01

Liquidity Fee (absolute terms): .00025

Liquidity Fee (annualized interest rate terms): .30%

Total Currency: 100,000 Dai

Total fCash: 100,000 1M Dai

Proportion: .5

Prevailing Exchange Rate = (1 / 100) * ln(1) + 1.01 = 1.01

Prevailing Interest Rate (annualized) = 12%

A lender comes to Notional to buy 1,000 1M Dai. Notional calculates the user’s trade exchange rate and updates the pool balances accordingly.

Trade Details:

tradeProportion = (100,000 - 1,000) / (100,000 + 100,000) = .495

Trade Exchange Rate = (1/100) * ln(.495/.505) + 1.01 - .00025 = 1.00955

Trade Interest Rate (annualized) = 11.46%

Dai sold in exchange for 1,000 1M Dai = 990.5403 Dai

All-In Trading Fee (Dai) = 990.5403 - (1000 / 1.01) = .4403 Dai

All-In Trading Fee (annualized interest rate terms) = .54%

Updated Pool Details:

Total Currency: 100,990 Dai

Total fCash: 99,000 1M Dai

Proportion: .49502

Prevailing Exchange Rate = 1.0098

Prevailing Interest Rate = 11.76%

The Notional AMM is parameterized by three variables - the scalar, anchor and liquidity fee. The scalar and anchor allow us to vary the steepness of the curve and its position in the xy-plane, respectively. Here is the same logit curve with different scalar and anchor values.

Increasing the scalar value flattens the curve, and decreasing the value steepens it. This translates to a **less** sensitive curve, and **less** slippage on a given trade, with a **greater** scalar value. Conversely, a **smaller** scalar value results in a **more** sensitive curve, and **more** slippage on a given trade. Varying the anchor value shifts the curve up and down in the xy-plane.

The problem of static sensitivity is relevant not only to the liquidity curve itself, but also to the liquidity fee. The same reasoning applies - a constant fee in exchange rate terms will grow exponentially more punitive to end users as fCash tokens approach maturity. To solve this problem, we convert the scalar and liquidity fee into **functions of time to maturity**, each parameterized by a root value. Making the scalar a function of time to maturity means that the shape of the liquidity curve changes as we approach maturity:

In the Notional system, we normalize rates associated with a given fCash market to periodSize - the amount of time between the inception of a fCash token and its maturity.

$timeToMaturity(t) = (maturity - t) \\ ~ \\ scalar(t) = scalarRoot * periodSize / timeToMaturity(t) \\ ~ \\ liquidityFee(t) = liquidityFeeRoot * timeToMaturity(t) / periodSize$

Expressing the scalar and liquidity fee in this form maintains a consistent sensitivity and fee throughout the lifecycle of a fCash token. It may seem intuitive that this is true, but it’s not totally obvious - we include a more detailed exposition and proof in the appendix.

Substituting in the changes to scalar and liquidityFee, we have the final exchange rate equations for the Notional AMM.

$Exchange Rate = (1 / scalar(t)) * ln(proportion / (1 - proportion)) + anchor \\ ~ \\ Trade Exchange Rate = (1/scalar(t)) * ln(tradeProportion / (1-tradeProportion)) \\+ anchor ± liquidityFee(t)$

As fCash tokens approach maturity, the exchange rate will drift in the absence of any trading. This is problematic because it means that the prevailing interest rate of a pool will also drift over time in the absence of trading. This drift will increase exponentially as fCash tokens approach maturity. Interest rate drift presents clever traders the opportunity to gradually siphon value from liquidity providers over time.

The Notional AMM prevents this by updating the anchor upon each trade such that the pre-trade interest rate equals the interest rate immediately after the last trade occurred. This mechanism preserves consistent interest rates over time in the absence of trading. Implementation is relatively straightforward, details are included in the appendix.

The values chosen for the scalar and the liquidity fee have economic implications for the system. Both of these variables have the effect of shifting the economic balance between users and liquidity providers. The liquidity fee has that effect explicitly. But the scalar changes that balance as well, though somewhat less directly. Recall that the all-in fee a user pays can be decomposed into the liquidity fee + the slippage. Changing the scalar changes the slippage. It’s important to pick appropriate scalar and liquidity fee values that balance the interests of users and liquidity providers. Liquidity provider profitability is critical, but charging users too much will impede the system’s growth and success.

Choosing the anchor value decides where the flatter part of the liquidity curve sits in interest rate terms. Given the shape of the logit curve, trading conditions between proportion values of .1 and .9 can broadly be described as “normal”. The exponential curvature really starts to kick in past those points. The anchor value chosen upon the curve’s instantiation determines the range of interest rates that can be traded “normally”. The scalar value determines the absolute distance in interest rate terms between the interest rate at a proportion of .9 and the interest rate at a proportion of .1. For example, a scalar value of 100 implies a distance of 52.73% between the interest rate at .1 and the interest rate at .9 in a one-month maturity. The anchor value determines what interest rate sits in the middle of that range.

We need to ensure that the traded exchange rate is always worse than (from the user’s perspective) or equal to the pool’s exchange rate after the trade has occurred. If this is not true, the mechanism is vulnerable to arbitrage and manipulation. Recall the formulas for determining traded exchange rates:

$tradeProportion = (totalFCash ± tradeSize) / (totalFCash + totalCash) \\ ~ \\ Trade Exchange Rate = (1 / scalar) * ln(tradeProportion / (1 - tradeProportion)) \\+ anchor ± liquidityFee$

For the purposes of this proof we are going to allow tradeSize to be a negative number (this is easier to deal with mathematically than the above equation for tradeProportion). This gives the below equation.

$tradeProportion = (totalFCash + tradeSize) / (totalFCash + totalCash)$

A positive value of tradeSize means that a user has **sold** fCash and **increased** the supply of fCash within the pool. A negative value of X means that a user has **bought** fCash and **decreased** the supply of fCash within the pool. Trades and exchange rates are always specified in terms of fCash, never in terms of current cash. Thus, somewhat counterintuitively, a user would always prefer to sell her fCash at a **lower** exchange rate (a lower exchange rate implies that fCash is worth more in current cash terms).

Here is the proof that tradeExchangeRate is always worse than endExchangeRate (the exchange rate after the trade has occurred). Note - we rely on the fact that we do not allow exchange rates below 1 (i.e. negative interest rates) in this proof. Notional will revert upon a trade if it produces a negative interest rate.

$X = tradeSize \\ Y = cashAmountTraded \\ TFC = totalFCash \\ TC = totalCash \\ ~ \\ If X > 0 \\ ~ \\tradeProportion = TFC + X / (TFC + TC) \\ endProportion = TFC+X/(TFC+X+TC-Y)=TFC+X/((TFC+TC)+(X-Y)) \\ exchangeRate > 1 -> X > Y \\ -> (X - Y) > 0 \\ -> ((TFC + TC) + (X - Y)) > (TFC + TC) \\ -> tradeProportion > endProportion \\ -> tradeExchangeRate > endExchangeRate \\ ~ \\ If X < 0 \\ ~\\tradeProportion = TFC + X / (TFC + TC) \\ endProportion = TFC+X/(TFC+X+TC-Y)=TFC+X/((TFC+TC)+(X-Y)) \\ exchangeRate > 1 -> X < Y \\ -> (X - Y) < 0 \\ -> ((TFC + TC) + (X - Y)) < (TFC + TC) \\ -> tradeProportion < endProportion \\ -> tradeExchangeRate < endExchangeRate$

We can represent the sensitivity of the liquidity curve as the following derivative.

$d \text{ }interestRate / d \text{ }proportion$

It’s straightforward to show that this derivative changes as a function of time to maturity.

$interestRate=(ln(proportion / (1-proportion)) / scalar+anchor-1)\\*periodSize / timeToMaturity \\~\\= ln(p / (1-p)) / scalar *periodSize / tTM + (anchor-1) * periodSize / tTM \\ ~ \\ ~\\ d \text{ } interestRate / d \text{ }proportion = (p / (1-p))' * ((1-p) / p) * (periodSize / (scalar*tTM)) \\ ~ \\ = 1 / (1-p)2 * ((1-p) / p) * (periodSize / (scalar*tTM)) \\~\\ = 1 / p(1-p)* (periodSize / (scalar*tTM))
\\~\\(d\text{ }interestRate / d \text{ }proportion) / d\text{ } tTM = -periodSize / scalar * tTM2 / p(1-p)$

We want the sensitivity of the liquidity curve to be constant through time - in effect we want the derivative of the sensitivity with respect to time to maturity to equal 0. Varying scalar with time to maturity achieves this goal.

$scalar =scalarRoot * periodSize / timeToMaturity \\ ~ \\ d \text{ } interestRate / d\text{ } proportion =1 / p(1-p)* (periodSize / (scalar*tTM)) \\ ~ \\ =1 / p(1-p)* (1 / scalarRoot) \\~\\ (d \text{ }interestRate / d\text{ } proportion) / d\text{ } timeToMaturity = 0$

To counteract implied interest rate drift, we use the anchor variable to keep interest rates consistent over time in the absence of any trading. After each trade we save the implied interest rate. Upon the next trade we check to see if the current implied interest rate == the saved implied interest rate. If it does not, we update the anchor such that it does prior to executing the trade. The anchor is just a constant in Notional’s exchange rate formula, so we don’t need to worry that this has any unintended effects. Here is how we solve for a new anchor value.

$interestRateDifference =currentInterestRate - savedInterestRate \\ ~ \\newAnchor =anchor -interestRateDifference * (tTM / periodSize)$

Proof:

$newExchangeRate = (1 / scalar(t)) * ln(proportion / (1 - proportion)) + newAnchor \\~\\newExchangeRate = (1 / scalar(t)) * ln(proportion / (1 - proportion)) \\ + anchor -interestRateDifference * (tTM / periodSize) \\~\\newExchangeRate = currentExchangeRate - interestRateDifference * (tTm / periodSize)\\~\\newInterestRate = (currentExchangeRate -interestRateDifference *(tTm / periodSize)-1) \\* periodSize/tTM\\~\\= (currentExchangeRate - 1) * periodSize/tTM - interestRateDifference\\~\\= currentInterestRate - interestRateDifference\\~\\= savedInterestRate$

Copy link