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.
| Entrypoint | Signature | What it gives you |
|---|---|---|
calculate_buy_price | (total_supply: int, count: int) => int | Cost in aettos of buying count more tokens at a given supply |
calculate_sell_return | (total_supply: int, sell_tokens: int) => int | AE returned for selling sell_tokens at a given supply |
buy_price | (total_supply: int) => int | The instantaneous buy price at that supply |
sell_price | (total_supply: int) => int | The instantaneous sell price at that supply |
buy_curve | (x: int) => Frac.frac | The raw buy curve value as an exact rational |
sell_curve | (x: int) => Frac.frac | The raw sell curve value as an exact rational |
get_k | () => int | Exponent coefficient |
get_initial_price | () => int | Curve offset constant |
get_minimum_count | () => int | Smallest tradable count |
get_sell_return_percentage | () => int | The sell factor numerator |
get_sell_return_precision | () => int | The sell factor denominator |
curve_type | () => string | Curve identifier |
supported_decimals | () => int | Token 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
| Constant | Sophia literal | As a number |
|---|---|---|
a | 1 * 10 ^ 15 | 1,000,000,000,000,000 |
k | 1 * 10 ^ 10 | 10,000,000,000 |
initial_price | 1 * 10 ^ 11 | 100,000,000,000 |
decimals | 1 * 10 ^ 18 | 10^18 |
minimum_count | 1 * 10 ^ 12 | 10^12 aettos, i.e. 10^-6 of a token |
sell_return_percentage | 9950 | 99.50% |
sell_return_precision | 10000 | the denominator of that percentage |
curve_type() | "TAYLOR_EXPONENTIAL_V1" | n/a |
supported_decimals() | 18 | n/a |
The closed-form curve these constants encode:
buy(x) = 0.001 * e^(0.00000001 * x) - 0.0009999
sell(x) = buy(x) * 0.995How 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.0000001a 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 * scalewithscale = decimals^2 = 10^36. The series stops as soon asterm < 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, denominatorFrac 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_countis 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
- Bonding curve: the conceptual explanation, with worked examples
- Curve parameters: the same constants from a trader's point of view
- AffiliationBondingCurveTokenSale: the contract that calls this one
- Calling Contracts: how to run the snippets above
- Sophia standard library (opens in a new tab): where
Fracis defined