Smart Contracts
BondingCurveExponential

BondingCurveExponential

BondingCurveExponential.aes computes every price in the token system. It holds no funds and no per-community state. One instance is shared by every community, and the token sale calls into it through the BondingCurve interface.

Resolve its address with factory.bonding_curve(). Use BondingCurveExponential.aci.json when you call the curve directly; BondingCurve.aci.json is the interface the sale holds.

Entrypoints

Everything here is non-stateful and free to call.

EntrypointSignatureWhat it gives you
calculate_buy_price(total_supply: int, count: int) => intCost in aettos of buying count more tokens at a given supply
calculate_sell_return(total_supply: int, sell_tokens: int) => intAE returned for selling sell_tokens at a given supply
buy_price(total_supply: int) => intThe instantaneous buy price at that supply
sell_price(total_supply: int) => intThe instantaneous sell price at that supply
buy_curve(x: int) => Frac.fracThe raw buy curve value as an exact rational
sell_curve(x: int) => Frac.fracThe raw sell curve value as an exact rational
get_k() => intExponent coefficient
get_initial_price() => intCurve offset constant
get_minimum_count() => intSmallest tradable count
get_sell_return_percentage() => intThe sell factor numerator
get_sell_return_precision() => intThe sell factor denominator
curve_type() => stringCurve identifier
supported_decimals() => intToken decimals the curve is calibrated for

The BondingCurve interface declares the same eight computational entrypoints with unnamed arguments (_1, _2), which is why the sale contract's ACI shows them that way.

calculate_buy_price and buy_price take a supply, not a count. The token sale's price(count) takes a count and reads the current supply for you. Mixing them up produces prices that are wrong by many orders of magnitude.

Use calculate_buy_price and calculate_sell_return for quotes.

Curve constants

ConstantSophia literalAs a number
a1 * 10 ^ 151,000,000,000,000,000
k1 * 10 ^ 1010,000,000,000
initial_price1 * 10 ^ 11100,000,000,000
decimals1 * 10 ^ 1810^18
minimum_count1 * 10 ^ 1210^12 aettos, i.e. 10^-6 of a token
sell_return_percentage995099.50%
sell_return_precision10000the denominator of that percentage
curve_type()"TAYLOR_EXPONENTIAL_V1"n/a
supported_decimals()18n/a

The closed-form curve these constants encode:

buy(x)  = 0.001 * e^(0.00000001 * x) - 0.0009999
sell(x) = buy(x) * 0.995

How the constants map to the formula

The contract works in fixed point against decimals = 10^18, so each decimal constant is that fraction scaled by 10^18:

a             = 10^15  →  10^15 / 10^18 = 0.001
k             = 10^10  →  10^10 / 10^18 = 0.00000001
initial_price = 10^11  →  10^11 / 10^18 = 0.0000001

a and k are the 0.001 and 0.00000001 of the closed form. The 0.0009999 offset is a - initial_price (0.001 - 0.0000001), which is how a curve pinned to f(0) = initial_price is written. So f(0) is 0.0000001 AE per token, not zero: the very first token has a non-zero price.

The 0.995 sell factor is sell_return_percentage / sell_return_precision = 9950 / 10000. That 0.5% is the DAO's half of the spread. The sale contract adds the other 0.5% as the affiliation fee on the way in, which is how the round trip reaches a 1% total spread while the curve itself only knows about 0.5%.

What the contract evaluates

FATE has no floating point, so the contract replaces e^x with a bounded Maclaurin (Taylor) expansion in integer fixed point. The closed form above is the idealised curve the contract approximates.

calculate_price is a difference of two expansions:

function calculate_price(total_supply : int, count : int) =
  require(count >= minimum_count, "MINIMUM_TOKENS_REQUIRED")
  let current_supply_exponent = (k * total_supply)
  let next_supply_exponent    = (k * (total_supply + count))
  (initial_price * count) / decimals
    + ((a * (mc2(next_supply_exponent) - mc2(current_supply_exponent))) / (k * decimals))

mc2(x) is the Maclaurin series for e^x starting at the quadratic term. It omits the 1 and the x term, because those cancel in the difference and are folded into the initial_price * count / decimals term:

function mc2(x : int) = exp_helper(x, x, 0, 2, decimals * decimals)

function exp_helper(x : int, term : int, sum : int, i : int, scale : int) : int =
  if (i > 100 || term < 1)
    sum
  else
    let next_term = (term * x) / (i * scale)   // x^i / i!
    exp_helper(x, next_term, sum + next_term, i + 1, scale)

Two things bound the series:

  • A hard cap of 100 iterations. The recursion stops once i > 100, so at most 99 terms (i = 2 through 100) are ever summed.
  • Integer truncation. Every term is an integer division by i * scale with scale = decimals^2 = 10^36. The series stops as soon as term < 1, because a term that truncates to zero makes every later term zero too. At realistic supplies this fires long before the iteration cap.

The approximation error is negligible: the on-chain result and the closed form agree to within 2e-16 AE at supply 0 and 1.4e-12 AE at supply 10,000,000, far below one aetto of a per-token price. Quote the closed form for explanation; call calculate_buy_price for money.

calculate_price carries the only require on the curve: a count below minimum_count (10^12 aettos) aborts with MINIMUM_TOKENS_REQUIRED. That applies to calculate_buy_price and calculate_sell_return alike, because both route through it.

buy_curve and sell_curve are a separate code path. They call exp_helper with the full series from i = 1, including the constant term (exp_helper(x_exponent, decimals, decimals, 1, decimals)), and return a Frac.frac rather than an integer. They give you the instantaneous curve value for charting; they are not what a trade is priced from.

The Frac.frac type

datatype frac = Pos(int, int) | Zero | Neg(int, int)
              //  numerator, denominator

Frac is the æternity standard library's exact rational type. A value is either Zero or a signed numerator/denominator pair. Decoded in JavaScript it is a one-key object:

const value = await curve.buy_curve(supply).then((r) => r.decodedResult);
// { Pos: [ 123456789n, 1000000000000n ] }   or   { Zero: [] }   or   { Neg: [ n, d ] }

Keep it as a ratio of BigInts until the last possible moment. Never build a JavaScript Number out of either half, because both can exceed 2^53:

import BigNumber from 'bignumber.js';
 
function fracToBigNumber(frac) {
  if ('Zero' in frac) return new BigNumber(0);
  const [num, den] = 'Pos' in frac ? frac.Pos : frac.Neg;
  const value = new BigNumber(num.toString()).dividedBy(den.toString());
  return 'Neg' in frac ? value.negated() : value;
}

For anything that touches money, do not convert at all. Call calculate_buy_price or calculate_sell_return, which return integer aettos computed on chain. buy_curve and sell_curve are for charting and analysis, where a rounding error of one part in 10^18 does not matter.

Reading the live constants

const curveAddress = await factory.bonding_curve().then((r) => r.decodedResult);
 
const curve = await Contract.initialize({
  aci: JSON.parse(fs.readFileSync('./contracts/BondingCurveExponential.aci.json', 'utf8')),
  address: curveAddress,
  onNode: aeSdk.api,
});
 
console.log({
  k: (await curve.get_k().then((r) => r.decodedResult)).toString(),
  initialPrice: (await curve.get_initial_price().then((r) => r.decodedResult)).toString(),
  minimumCount: (await curve.get_minimum_count().then((r) => r.decodedResult)).toString(),
  sellPct: (await curve.get_sell_return_percentage().then((r) => r.decodedResult)).toString(),
  curveType: await curve.curve_type().then((r) => r.decodedResult),
  decimals: (await curve.supported_decimals().then((r) => r.decodedResult)).toString(),
});

Practical notes

  • Price is a pure function of supply. There is no time component, no oracle and no liquidity provider. Two buys of the same size at the same supply cost the same.
  • minimum_count is 10^12 base units. With 18-decimal tokens that is 0.000001 of a token. Quotes below it are not meaningful.
  • The curve is steep by design. It is calibrated so the price rises noticeably from the very first buys; only 10–20% of the nominal supply is expected to be reached.

Related