Notional AMM

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

Special Requirements for Trading fCash:

  • 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.

Notional AMM Basics:

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)Proportion = totalFCash / (totalCurrency + totalFCash)

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

ExchangeRate=(1/scalar)ln(proportion/(1proportion))+anchor InterestRate=(ExchangeRate1)periodSize/timeToMaturityExchange Rate = (1 / scalar) * ln(proportion / (1 - proportion)) + anchor \\ ~ \\ Interest Rate = (Exchange Rate - 1) * periodSize / timeToMaturity

Trading on the Notional AMM:

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) TradeExchangeRate=(1/scalar)ln(tradeProportion/(1tradeProportion))+anchor±liquidityFeetradeProportion = (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.

Trading Fees:

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.

Trading on the Notional AMM (Example):

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%

Notional AMM Parameters:

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.

Moving from a Static Curve to a Dynamic Curve:

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)=(maturityt) scalar(t)=scalarRootperiodSize/timeToMaturity(t) liquidityFee(t)=liquidityFeeRoottimeToMaturity(t)/periodSizetimeToMaturity(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.

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

Preserving Interest Rate Continuity:

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.

Parameter Choice Implications - Economics:

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.

Parameter Choice Implications - Interest Rate Boundaries:

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.


Traded Exchange Rates:

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) TradeExchangeRate=(1/scalar)ln(tradeProportion/(1tradeProportion))+anchor±liquidityFeetradeProportion = (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)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=tradeSizeY=cashAmountTradedTFC=totalFCashTC=totalCash IfX>0 tradeProportion=TFC+X/(TFC+TC)endProportion=TFC+X/(TFC+X+TCY)=TFC+X/((TFC+TC)+(XY))exchangeRate>1>X>Y>(XY)>0>((TFC+TC)+(XY))>(TFC+TC)>tradeProportion>endProportion>tradeExchangeRate>endExchangeRate IfX<0 tradeProportion=TFC+X/(TFC+TC)endProportion=TFC+X/(TFC+X+TCY)=TFC+X/((TFC+TC)+(XY))exchangeRate>1>X<Y>(XY)<0>((TFC+TC)+(XY))<(TFC+TC)>tradeProportion<endProportion>tradeExchangeRate<endExchangeRateX = 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

Consistent Curve Sensitivity:

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

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

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

interestRate=(ln(proportion/(1proportion))/scalar+anchor1)periodSize/timeToMaturity =ln(p/(1p))/scalarperiodSize/tTM+(anchor1)periodSize/tTM  d interestRate/d proportion=(p/(1p))((1p)/p)(periodSize/(scalartTM)) =1/(1p)2((1p)/p)(periodSize/(scalartTM)) =1/p(1p)(periodSize/(scalartTM)) (d interestRate/d proportion)/d tTM=periodSize/scalartTM2/p(1p)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=scalarRootperiodSize/timeToMaturity d interestRate/d proportion=1/p(1p)(periodSize/(scalartTM)) =1/p(1p)(1/scalarRoot) (d interestRate/d proportion)/d timeToMaturity=0scalar =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

Interest Rate Continuity:

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=currentInterestRatesavedInterestRate newAnchor=anchorinterestRateDifference(tTM/periodSize)interestRateDifference =currentInterestRate - savedInterestRate \\ ~ \\newAnchor =anchor -interestRateDifference * (tTM / periodSize)


newExchangeRate=(1/scalar(t))ln(proportion/(1proportion))+newAnchor newExchangeRate=(1/scalar(t))ln(proportion/(1proportion))+anchorinterestRateDifference(tTM/periodSize) newExchangeRate=currentExchangeRateinterestRateDifference(tTm/periodSize) newInterestRate=(currentExchangeRateinterestRateDifference(tTm/periodSize)1)periodSize/tTM =(currentExchangeRate1)periodSize/tTMinterestRateDifference =currentInterestRateinterestRateDifference =savedInterestRatenewExchangeRate = (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