Batched Bonding Curves

Three Solutions to Prevent Front-Running

The goal of this document is to outline various implementations of front-running resistant Bonding Curves. For a refresher on the finer points of Bonding Curves, check out this great deep dive from Slava Balasanov. A Batched Bonding Curve would accept a group of buy and sell orders over a span of blocks and calculate a clearing price after that span of blocks. This would prevent front-running scenarios that since being published about have increased dramatically in the wild and affect almost all decentralized exchanges (DEX). It takes the Gnosis DutchX as inspiration, the only DEX that completely thwarts front-running attacks and does so by batching orders. This technique would furthermore mitigate the mass exit issue with Bonding Curves. Depending on the number of blocks included in a batch, it would allow for a mass exit to occur at a uniform price rather than the last out eating their hat.

Finding a uniform price for all buy and sell orders is difficult to do as the order of the orders will have an effect on the resulting prices. The math required to do so isn’t practical on the Ethereum Virtual Machine (EVM). Below are two solutions that find a separate price for all buyers and all sellers as well as a third method that finds a unified price for both groups without being restricted by the EVM. All methods begin by combining buys together and sells together while keeping track of individual amounts. This results in computations that incorporate one large buy order and one large sell. The different implementations diverge at that point with different features that may be desired for different scenarios.

At the end of this document is a link to a github repository where all three implementations have been built. Please take a look at the code and make an issue if you have any questions or comments. Furthermore this document is also available as an Observable that contains values and functions which can be edited to see how different scenarios would play out.

Initial State

All of the following scenarios will be demonstrated with a Bonding Curve contract that uses the initial state s. This initial state s is designed with the Bancor Formula in mind and a Connector Weight of 0.5. I believe the Bancor Formula has better properties for implementing Bonding Curves, but that it is easier to think about the curve as a slope graph. For that reason the slope m and the power n is included in the state and this particular contract can be represented as f(x) = 0.0002 * x^1 where x is the total supply of tokens.

s = {
balance: 100,
totalSupply: 1000,
connectorWeight: .5,
currentPrice: 0.2,
slope: 0.0002,
power: 1
}

Now consider a series of buys and sells. The buys are amounts of collateral (like Eth or Dai) to be spent in exchange for some amount of new tokens, and the sells are amounts of tokens to be sold for some amount of collateral.

buys = Array(3) [10, 20, 5]
sells = Array(3) [100, 200, 50]

These can be thought of as a single large buy order and a single large sell order. This will make it easier to calculate clearing prices for each of the following scenarios.

allBuys = 35
allSells = 350

Photo by Waldemar Brandt on Unsplash

Implementation #1 Batched & Ordered

The first scenario is the simplest. It just executes all of the buys and then all of the sells, or all of the sells and then all of the buys. Both options give preference to one group over the other, which can be part of a token’s design depending on the situation. To understand the benefits of each version consider what happens when one group is executed before the other group.

First all the buys are executed and then all the sells:

{
balance: 65.9173,
totalSupply: 811.895,
connectorWeight: .5,
currentPrice: 0.1624,
slope: 0.00020000000880536034,
power: 1
}

While the buys began pushing the price per token upwards in this scenario, the sells ultimately capitalized on this high price and were able to extract a larger amount of collateral per token than would have been possible had the order of the orders been switched. This means that in general the buys resulted in fewer yielded tokens, and the sells resulted in greater yielded collateral. This situation benefits the group making sell orders over the group making buy orders.

First all the sells are executed and then all the buys:

{
balance: 77.25,
totalSupply: 878.9198,
connectorWeight: .5,
currentPrice: 0.1758,
slope: 0.0002,
power: 1
}

While the sells began pushing the price per token downwards in this scenario, the buys ultimately capitalized on this low price and were able to extract a larger amount of token per collateral. This means that in general the sells resulted in fewer yielded collateral, and the buys resulted in greater yielded tokens. This situation benefits the group making buy orders over the group making sell orders.

If a token model incorporated the desire to reward buyers over sellers it might be beneficial to use the seller-first Batched & Ordered implementation. This result is preferable for a scenario where the goal of the token is to appreciate in value. It also rewards users entering into the token ecosystem by buying new tokens rather than reward the users that are leaving.

Here are both scenarios laid out with price averages to give you an idea of what each would look like. Keep in mind that buyers want low prices and sellers want high prices:

sellsThenBuys Scenario (rewards buyers)

  • The price for the sellers would have been 0.1650 👎
  • The price for the buyers would have been 0.1529 👍
  • The final market price would be 0.1758 👍
  • The non-weighted average of the two would have been 0.159

buysThenSells Scenario (rewards sellers)

  • The price for the sellers would have been 0.1974 👍
  • The price for the buyers would have been 0.2162 👎
  • The final market price would be 0.1624 👎
  • The non-weighted average of the two would have been 0.2068

Photo by Alex Block on Unsplash

Implementation #2 Match & Fill

Martin Köppelmann suggested an alternative scenario which also breaks the price up for buyers and sellers but does a better job at keeping the prices similar and doesn’t arbitrarily choose one party to benefit over another. It works like this:

Collect and combine all buys and all sells over a span of some blocks like before then take the current price per token and execute as many as possible of the orders at that price. Of course with a Bonding Curve, executing any amount would change the current price. For this scenario we are pretending that the current price would not change when some amount of orders were executed, because they’re essentially being matched buyers to sellers:

price = 0.2 collateral per token
totalBuy / price =
35 / 0.2 =
175 tokens as a result of the buys
totalSell * price = 
350 * 0.2 =
70 collateral as a result of the sells

The next step depends on the outcome of this fixed price execution.

If the sells outweigh the buys

If the result of the execution of the totalSells (70 collateral) were larger than the amount of the original totalBuy (35 collateral) then all of the totalBuy orders would get executed at the price 0.2. However, this would only satisfy part of the sell orders and there would be some outstanding amount of tokens still needing to be sold.

totalSell - (totalBuy / price) = 
350 - (35 / 0.2) =
350 - 175 =
175 token that still need to be sold

This remaining 175 tokens to be sold would go into the normal bonding curve price calculator:

saleReturn(175, s) = 
31.937500000000007 collateral

This bonding curve sell is combined with the earlier match order sell to give all sellers a uniform price:

(matchedSellResult + curvedSellResult) / totalSell =
(35 + 31.937500000000007) / 350 =
0.19125 collateral/token

In Summary

  • The price for the sellers would be 0.1913
  • The price for the buyers would be 0.2
  • The final market price would be 0.165
  • The non-weighted average of the two would have been 0.1956

Photo by rawpixel on Unsplash

Implementation #3 Common Clearing Price

As mentioned before the ability to calculate a uniform price for buys and sells requires a level of math that is not practical for the EVM. The solution for doing so was found thanks to the help of Tom Walther. Regardless of the ability to calculate the solution on the EVM, it is possible to verify the solution with the EVM. This is a similar tactic being used on the Gnosis dfusion exchange.

One method for calculating the uniform price is to first assume some universal price (_p) and then describe the group of buys and sells as part of a total change in tokens and a total change in collateral. This can be written as Δc (total change in collateral) and Δx (total change in tokens).

The total change of collateral (Δc) is a combination of the addition of new collateral from buy orders (_c) and the subtraction of collateral from the sell orders. Sell orders can be thought of as some number of tokens (_x) times that universal price _p.

The total change of tokens (Δx) is a combination of the subtraction of tokens from sell orders (_x) and the addition of new tokens from the buy orders. Buy orders can be thought of as some amount of collateral (_c) divided by that universal price _p.

When dealing with a slope formula Bonding Curve it is possible to calculate the cost of purchasing some amount of tokens by taking the integral of the slope between two different token supplies. This looks like:

We can use this same method but substitute Δc for the purchase (which is really just a change in collateral) and Δx for the value k (which is the difference in token supplies). This will give us a formula that can be solved for _p:

We can substitute all of the variables with values from our example state s from earlier:

_c = 35
_x = 350
m = 0.0002
n = 1
totalSupply = 1000

When you put this equation into Wolfram Alpha you get a variety of answers depending on the value of n. Unfortunately Wolfram Alpha can’t handle solving the function for _p, so we have to do with specific examples.

WOLFRAM ALPHA LINK

In the scenario where m = 0.0002, n = 1, s = 1000, _x = 350, _c = 35 this yields the following approximate solutions for _p:

p_1 = -0.0190197
p_2 = 0.1
p_3 = 0.18402

We can try executing orders with each of these prices to see what happens. We should be able to tell whether or not they worked based on whether or not the slope of our Bonding Curve was altered from the original 0.0002.

p_1

The state of the contract after executing orders using price p_1 (-0.0190197) would look like:

{
balance: 141.656895,
totalSupply: -1190.1972691472524,
connectorWeight: 0.5,
currentPrice: 0.2,
slope: 0.0002,
power: 1
}

The slope of this state would be 0.00019999991677280065 👍

p_2

The state of the contract after executing orders using price p_2 (0.1) would look like:

{
balance: 100,
totalSupply: 1000,
connectorWeight: 0.5,
currentPrice: 0.2,
slope: 0.0002,
power: 1
}

The slope of this state would be 0.0002 👍

p_3

The state of the contract after executing orders using price p_3 (0.18402) would look like:

{
balance: 70.593,
totalSupply: 840.1967177480708,
connectorWeight: 0.5,
currentPrice: 0.2,
slope: 0.0002,
power: 1
}

The slope of this state would be 0.0001999998513976622 👍

Selecting a Solution

Each of the prices seem to leave the state of the contract with an appropriate slope of approximately 0.0002.

p_1 would result in a negative totalSupply, which doesn’t seem good.

p_2 would result in no change to the state of the bonding curve at all which also doesn’t seem good.

p_3 seems to be the best candidate after process of elimination. The net difference of the state of the contract would result in a little lower totalSupply and a little lower balance. That would imply that there were more sells than buys, which also seems to be the case based on the scenarios outlined in the previous implementations using the same initial state.

Finally this solution needs to be implemented on the EVM. Instead of trying to compute this answer, it would be better to allow anyone to provide the solution to the contract and let the contract confirm that

  • it does not result in a negative token supply
  • it does not result in no change at all
  • it does not change the slope of the contract (within some margin of error for rounding)

Photo by Neil Thomas on Unsplash

Conclusion

The first scenario, Batched & Ordered might be best if it is preferable to give one party an advantage over another, as in the case of preferring buyers over sellers in order to encourage a climbing price.

The second scenario, Match & Fill might be best if it is preferable to have a more unified price between buyers and sellers, determined by the order quantities rather than a specific design decision.

The third scenario, Common Clearing Price might be best if it is preferable to have a unified buy and sell price for all buyers and sellers. This scenario comes with a UX overhead of orders not being cleared until a suitable price has been supplied and verified, but it gives the most intuitive price for users.

All three scenarios involve an asynchronous order execution which is also not a great user experience. However each order could come with an extra fee that could be used to reward a third party for finalizing an order on behalf of another user. This would allow a user to “set it and forget it” so that the results of their trade would show up without further interaction.

As mentioned before all three scenarios have been implemented in the github repository @okwme/BatchedBondingCurves. This solution is not implemented in the wild but if you’re interested in incorporating it please let me know! I also welcome any critiques or feedback on the solutions I’ve proposed via github or here.


My name is Billy Rennekamp. I’m a co-founder of Bin Studio, a multidisciplinary research, design and development studio based in Berlin. We’re currently developing Clovers Network, a novel proof-of-work game supported by the ECF — Ethereum Community Fund. I work on a lot of other great projects including Cosmos Network, Gnosis, ENS Nifty, Meme Lordz and Doneth. If any of that sounds interesting to you, follow my twitter, my github or just say hello 👋