Our bonding curve has been improved

Dan Octavian
Nexus Mutual
Published in
11 min readMar 10, 2021

The NexusMutual token bonding curve implementation was upgraded! What does that mean for you, and why was it done in the first place? We’ll be diving into the details in this article.

First a quick refresher: Nexus Mutual uses a Continuous Token Model where the token price depends on how the mutual is performing financially and is formula driven where the price value is a function of the MCR% (ratio between total asset value in the pool and the Minimal Capital Requirement).

Check out the token model full explanation to see how NXM aligns incentives and has functionality like voting on claims and governance.

The last upgrade reduced the gas costs of the buy and sell NXM operations significantly while increasing the precision of each computation and ensuring the 2.5% minimum spread between buy and sell price.

The big motivator for these changes was that the previous implementation was potentially exploitable at very high MCR% to make the spread negative. This theoretically allowed, under certain conditions, users to sell at a higher price than the one which they bought and exploit the buy/sell functionality to make profit. Such levels of the MCR% were never reached in practice so the implementation was not exploited, but the upgrade was required to ensure correctness at all times.

Background

The current article assumes basic familiarity with token bonding curves. This article is a good starting point for getting an intuition for how they work. As explained there, vanilla bonding curves are defined as such:

Where:

p = token price

t = token supply

n = exponent parameter

m = slope parameter

The key difference between a vanilla curve and the NXM bonding curve is that the token price is a function of V — total value of assets in the pool, as opposed to being a function of token supply (t in the formula above). This makes both the theoretical derivations and the implementation trickier as we will see in the following sections.

Definitions

V — the value of assets in the mutual in ETH terms.

Let

Ax — Volume of Asset(x)

Fx — exchange rate to ETH for Asset(X)

Therefore the formula for V is:

At the time of writing, n = 2 (ETH and DAI).

MCReth — The minimum amount of capital required to support existing covers, Minimum Capital Requirement, in Ether. The MCR is calibrated to a 99.5% solvency level. Note: This value varies over time but is considered to be a constant in the price formula (it stays constant during buy/sell computations).

MCR% — Ratio of Capital Pool funds to the Minimum Capital Requirement

A and C — Fixed constants, calibrated based on the prevailing Ether price before launch. (A = 0.01028 and C = 5,800,000)

Therefore starting with the basic price formula from the introduction we get to the price formula as a function of V as such:

The problem

The formula above gives us the token spot price. However, NexusMutual needs to be able to:

  • Compute on-chain how much NXM should be given to a buyer supplying ETH to the pool.
  • Compute on-chain how much ETH should be given to a seller supplying NXM to the pool.

The variable for those 2 computations is V (increasing or decreasing respectively).

The previous implementation of the buy/sell computations relied on an iterative loop to compute the nxm-for-eth (buy NXM computation) and eth-for-nxm (sell NXM computation) values. It evaluated the spot price every 1000 NXM and evaluated the buy/sell price in ETH for that 1000 NXM chunk at the NXM spot price.

There are 3 major problems with this approach:

1. Exploitability

The sell computation nullifies the 2.5% spread at high MCR%. Since the price formula is a polynomial of degree 4, the token price starts ‘climbing’ very steeply as MCR% goes up and this makes the for-loop implementation exploitable.

For example at MCR=150k ETH, MCR% = 346%, by purchasing tokens worth 7500 ETH and selling them back, one makes ~24ETH in profit (ignoring gas costs). This margin grows as MCR% grows.

This is due to the fact that for the buyNXM calculation we take the left/lower price of the curve for the token step computation, while for the sellNXM calculation we take the right/higher price of the curve for the token step computation.

2. Gas cost

Executing loops in solidity is expensive. The buy and sell functions for this approach consume gas proportional to the size of the amounts being sold/bought.

3. Precision of the result

Stemming from the Gas limitation issue, the number of loops that can be computed in 1 Ethereum function call is very limited. The precision of the computation improves with the number of loops, but the number of loops cannot trivially be increased by lowering the step size.

Theoretical approach

Buy side

Objective: Calculate how many tokens a user should receive if the user supplies an amount of ETH to the pool.

Let:
ΔT be a very small change in the token supply

ΔV the corresponding very small change in V
P(V) — the point price at value V.

We can state the following:

This means that for an infinitesimally small change in V equal to ΔV, we get an infinitesimally small change in token supply T equal to ΔT. Key assumption: for this change we assume P(V) constant since the change in V is very small.

MCReth is a constant for this computation. Let’s define another constant m for simplicity:

Thus the spot price formula is now:

To find out the tokens to be minted for a large ΔV (let that be called ethIn) which takes us from V0 to V1 = V0 + ethIn in total asset value, we would want to integrate the formula above with respect to V to get the resulting token amount T to be minted.

Integrating this with respect to V is not trivial and if achieved it results in a very complex formula that cannot be implemented on chain. We need some adjustment or approximation in the implementation that gets close enough to this (previous implementation used the iterative token steps method).

Sell side

Objective: Calculate how much ETH a user should receive if the user supplies tokens to the pool for burning.

No clean closed-form formula could be found for the sell-side computation for the token curve described above. This would effectively require reversing the formula.

The implementation will be using an approximation that is satisfactory, while still being a closed-form computation (cheap gas-wise) that creates enough of a sell spread to prevent arbitrage (ideally 2.5% sell spread).

Actual implementation

Buy function — buyNXM

function buyNXM(uint minTokensOut) public payable isMember checkPause

A closed-form function is picked to approximate the integral of T = V/P(V) from V0 to V1. This is done in order to drastically reduce the complexity of the resulting integral and make it computable on chain.

The key trick used here is to remove the base price denoted by the constant A from the price calculation temporarily and compute the integral value without it. After the average buy price is computed with this adjusted formula, add back the base price to compute the final price.

While this trick will prove to be inaccurate for very large sums (eg. buying NXM worth 20%+ of the current MCR), this proves to be a very good approximation if buy amounts are limited to 5% of the MCR.

Let’s walk through the steps of how it was derived.

Let there be an adjusted price formula AdjustedP which no longer uses the A constant as a price base-line.

If we use this as our new price function the antiderivative for dV/P(V) has a much simpler formula:

We can therefore compute the amount of tokens to be given to the buyer as the adjustedTokens:

Where:

We use the adjustedTokens to compute an adjusted price:

This is not the final price however. We compute the final price by adding “back” the price offset constant A:

finalPrice = A + adjustedPrice

Finally, use the finalPrice to compute the tokens that will be send back to the buyer:

Here’s the computation in pseudocode as a summary:

Relative error analysis

Benchmarking

One important question is how to benchmark the function above to find out its relative error.

Let’s go back to the theoretical insight which tells us how many T tokens are minted for an infinitesimally small change in pool value ΔV.

We can deduce from here that an iterative computation where we take small ΔV steps and compute the spot price P(V) at each step and compute the T for those values. Sum each of those token values which will give us the final token amount that should be minted in exchange for the ETH amount. Intuitively, the smaller the ΔV step is, the more precise the computation is.

In practice, since this function has O(n) algorithmic complexity where N is the number of ΔV steps, it takes very long to compute to get precise results since the ETH amount may be large and preciseness increases the smaller the step is.

For practical reasons we made use of an automatically generated formula approximation generated by the Wolfram Alpha engine, which can be computed with O(1) algorithmic complexity. This allowed for building fuzzing tests that can quickly benchmark the function’s precision by varying V, MCR and ethIn over a wide range of values.

The input for Wolfram Alpha is:

integral dx / (a + m* x ^ 4)

And results in the following formula (assuming x, m and a are positive).

https://www.wolframalpha.com/input/?i=integral++dx+%2F+%28a+%2B+m*+x+%5E+4%29

Benchmark Results

We compute the ideal value using the benchmark function generated by Wolfram Alpha and plot the relative error for the Adjusted formula and the legacy token steps formula.

Let’s take for example the plot showing relative error (Y axis) of the adjusted formula (orange line) versus the token steps legacy formula (blue) by varying MCR% (X axis) for MCR = 150k ETH and ethIn = 7500 ETH (purchase size).

We can see that the adjusted formula has a much lower relative error rate.

Sell function — sellNXM

function sellNXM(uint tokenAmount, uint minEthOut) public isMember noReentrancy checkPause

The sell function should have a sell spread of at least 2.5%. This means that if ethAmount was spent for acquiring tokenAmount, selling nxmAmount back to the pool would yield 97.5% * ethAmount. Since no closed-form formula was found to represent this we use an approximation that yields good results.

There are 3 steps to this computation as follows:

Step 1

Calculate spot price at current value of assets(V0). Let this be spotPrice(V0).

Let spotPriceAmount be the amount of ETH returned to the seller if the tokenAmount was sold at this current spot price.

Step 2

Calculate the spot price at a value V1 = V0 — spotEthAmount from the first step (hypothetical value in case tokens were sold at the current spot price):

Step 3

Calculate the final price as the minimum between the average of spotPrice(V0) and spotPrice(V1) with the sell spread applied and the spotPrice(V1).

Intuition behind the approximation

This formula may not be immediately intuitive, here’s a few points that can make it more understandable.

When token amounts sold are low (the ETH equivalent of around 1% of current MCReth or less) the final price becomes:

average[spotPrice(V0), spotPrice(V1)] x ( 1 — sellSpread)

This gives a good approximation of a sell with 2.5% spread (only slightly higher than that ideal value).

If sell amounts get higher (approaching 5% of MCReth) the function starts using the spotPrice(V1) as the minimum value. This price is lower than the current spot price spotPrice(V0) and thus effectively gives high sell spreads. Sell spreads can go as high as 10% depending on the MCReth and V values at the time of evaluation.

This high-spread behaviour effectively punishes sellers who want to sell a lot with 1 transaction. This is considered acceptable from an end-user perspective. If, however, a use case requires selling large sums in 1 Ethereum transaction, an auxiliary smart contract can call this function in a loop and sell multiple times.

Sell spread behavior

Let’s take a look at the sell spread at an MCR of 150k ETH.

The sell spread for MCR% varying between 100% and 400% with sell amounts worth 1% of MCR, is very close to 2.5%.

The sell spread for MCR% varying between 100% and 400% with sells worth 5% of MCR approaches 8% as MCR% approaches 100% (effectively punishing sellers that sell large amounts at small MCR% values).

Conclusions

The Nexus bonding curve is not a vanilla bonding curve defined in terms of token supply, but instead defined in terms of the total value of assets in the pool. This makes the on-chain computation of tokens to be minted or burned when buying or selling respectively trickier, but not impossible.

With a clever choice of approximations and applying reasonable bounds, the computation can have very good precision.

When implementing token bonding curves, looping behaviour should be generally avoided given the limitations of block size and cost of gas, and a great deal of attention needs to be paid to any arbitrage exploits that can be done by making use of poor computation precision at edge-case parameter values.

Acknowledgments

This solution would not have been possible without the help of some great contributors.

Special thanks go to:

Adam Kolar for finding that the computation is exploitable,
Robert Koschig for building up a theoretical mathematical model around the computation to validate its soundness and confirm the arbitrage possibility and, last but not least,
Catalin Amza for providing mathematical insights into the price formula that lead to the solution.

For detailed and technical discussions on our roadmap use our forum.

Join our community on Discord.

For Telegram updates join the announcements channel and discussion group.

For all stats and data visit the Nexus tracker built by Richard Chen.

--

--