Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

curve.gno

8.63 Kb · 209 lines
  1// Package curve is a linear, one-way bonding curve for issuing a token against a
  2// reserve: price rises linearly with the position on the curve, and the cost of
  3// minting is the exact integral, computed in 128-bit so it never overflows and
  4// never issues a coin for less than its backing.
  5//
  6// # Why a reciprocal slope
  7//
  8// The marginal price is p(s) = s/D — a RECIPROCAL denominator D, not a numerator
  9// slope. That is not cosmetic. The cost of moving from s0 to s1 is the integral
 10// (s1²−s0²)/(2D). With a numerator slope k the product k·(s1²−s0²) can exceed
 11// 2^128 for a realistic k at a large supply, and the single 128-bit multiply
 12// overflows. As 1/D the numerator is only s1²−s0², at most cap² which for a cap
 13// near 9.2e14 is about 2^100 — always inside 128 bits — and D sits safely in the
 14// divisor. D is chosen from economics: larger D is a gentler curve.
 15//
 16// # No coin is ever minted below its backing
 17//
 18// Two rounding rules, and one belt-and-suspenders check, guarantee it:
 19//
 20//   - Cost rounds UP. The buyer pays at least the integral, so backing (the sum
 21//     of integrals) never exceeds treasury (the sum charged).
 22//   - Minted rounds DOWN. It floors the number of coins a payment buys — but it
 23//     does not trust the square root. It uses isqrt only for a CANDIDATE, then
 24//     corrects ±1 against the canonical Cost, so an off-by-one in the root can
 25//     never over-issue. This re-check is mandatory, not optional.
 26//
 27// # A value, not an object
 28//
 29// A Curve is immutable configuration (the slope denominator and the position
 30// cap); it holds no mutable state and is never a heap object. The CURVE POSITION
 31// — how far up the curve issuance has walked — lives in the consuming realm as a
 32// monotonic counter it passes in as `from`. Burning or redeeming the token must
 33// NOT move that counter back: the curve prices the next mint off total-ever-
 34// minted, and walking it backward would let the same region be bought twice.
 35package curve
 36
 37import (
 38	"math/bits"
 39	"math/overflow"
 40)
 41
 42const maxInt64 = int64(9223372036854775807)
 43
 44// maxCap bounds the position cap so cap² ≤ 2^100 and every 128-bit operand here
 45// stays well inside 2^126 (where isqrt128's search bound is exact). 2^50 ≈ 1.1e15
 46// comfortably covers a token's ~9.2e14 supply ceiling.
 47const maxCap = int64(1) << 50
 48
 49// Curve is a linear one-way bonding curve. Build it with New.
 50type Curve struct {
 51	d   int64 // reciprocal slope: marginal price p(s) = s/d
 52	cap int64 // the highest position issuance may reach
 53}
 54
 55// New builds a curve with reciprocal slope d and position cap. A larger d is a
 56// gentler price rise. cap bounds the position so cap² stays inside 128 bits and
 57// the token's own supply ceiling is respected.
 58func New(d, cap int64) Curve {
 59	if d < 1 || d > maxInt64/2 {
 60		panic("curve: d must be in [1, MaxInt64/2] so 2d does not overflow")
 61	}
 62	if cap < 1 || cap > maxCap {
 63		panic("curve: cap must be in [1, 2^50]")
 64	}
 65	return Curve{d: d, cap: cap}
 66}
 67
 68// D and Cap expose the construction parameters.
 69func (c Curve) D() int64   { return c.d }
 70func (c Curve) Cap() int64 { return c.cap }
 71
 72// Cost is the coin a buyer must pay to move the position from `from` to
 73// `from+delta`, the integral of the price over that span, ROUNDED UP. ok is false
 74// when the move would pass the cap or the cost would not fit in an int64 (only at
 75// absurd positions). Minting zero costs zero.
 76func (c Curve) Cost(from, delta int64) (coin int64, ok bool) {
 77	if from < 0 || delta < 0 || from > c.cap {
 78		return 0, false
 79	}
 80	s1, add := overflow.Add64(from, delta)
 81	if !add || s1 > c.cap {
 82		return 0, false
 83	}
 84	// diff = s1² − s0², an exact 128-bit value (s1 ≥ s0 so it never borrows past 0).
 85	sh1, sl1 := bits.Mul64(uint64(s1), uint64(s1))
 86	sh0, sl0 := bits.Mul64(uint64(from), uint64(from))
 87	lo, borrow := bits.Sub64(sl1, sl0, 0)
 88	hi, _ := bits.Sub64(sh1, sh0, borrow)
 89
 90	m := uint64(2 * c.d)
 91	// Ceil-divide the 128-bit diff by m: (diff + m − 1) / m.
 92	lo2, carry := bits.Add64(lo, m-1, 0)
 93	hi2 := hi + carry
 94	if hi2 >= m {
 95		// The quotient would not fit int64 (and bits.Div64 REQUIRES hi < m). This IS
 96		// reachable well below the position cap — for the court's d=1e9 it fires around
 97		// s1≈1.9e14, far under cap≈9.2e14 — but only at positions whose fill cost exceeds
 98		// MaxInt64 µGNOT, i.e. more GNOT than can exist, so no funded Buy reaches it.
 99		// Do NOT delete this as "dead": Minted relies on ok=false meaning "unaffordable",
100		// and without the guard bits.Div64 would violate its hi<m precondition (over-issue).
101		return 0, false
102	}
103	q, _ := bits.Div64(hi2, lo2, m)
104	if q > uint64(maxInt64) {
105		// Same story one step later: the cost is a valid 128-bit value but exceeds int64,
106		// so it is unaffordable (MaxInt64 µGNOT is more than the whole supply's worth).
107		return 0, false
108	}
109	return int64(q), true
110}
111
112// Minted is the largest whole number of coins `coin` can buy starting at position
113// `from`, and the coin actually spent on them (≤ coin; the caller keeps or refunds
114// the remainder). It floors: it finds a candidate with a 128-bit integer square
115// root, then corrects ±1 against the canonical Cost, so issued coin is never worth
116// more than what was paid, and it is zero when even one coin costs more than
117// `coin`.
118func (c Curve) Minted(from, coin int64) (delta, spent int64) {
119	if from < 0 || from >= c.cap || coin <= 0 {
120		return 0, 0
121	}
122	// Short-circuit a payment large enough to buy the whole remaining curve. This
123	// bounds the isqrt operand below to at most cap² (so isqrt128's search bound is
124	// exact) and handles the huge-coin case explicitly instead of relying on the
125	// later cap clamp. When filling to the cap costs more than an int64 can hold
126	// (a very steep curve), full is !ok and coin (≤ MaxInt64) is necessarily below
127	// it, so we fall through — and then s1 < cap keeps the operand under cap² too.
128	if full, ok := c.Cost(from, c.cap-from); ok && coin >= full {
129		return c.cap - from, full
130	}
131	// operand = from² + 2·d·coin, a 128-bit value; s1 = floor(sqrt(operand)).
132	fh, fl := bits.Mul64(uint64(from), uint64(from))
133	ph, pl := bits.Mul64(uint64(2*c.d), uint64(coin))
134	lo, carry := bits.Add64(fl, pl, 0)
135	hi := fh + ph + carry
136	r := isqrt128(hi, lo)
137
138	// UNREACHABLE AND LOAD-BEARING, which is not a contradiction — the same standing
139	// as the hi2 >= m guard above, and it is worth being explicit because no test can
140	// pin it. Nothing reaches it while the domain check at the top of this function
141	// stands: on fall-through coin < full, so the operand is at most cap²−1 and r is
142	// at most cap−1. There is NO margin in that bound (r does reach exactly cap−1), and
143	// the clamp is the second line, not a redundant one. Delete BOTH and, on a curve
144	// New will happily build, Minted(1, -1) returns 9223372036854775807 units for a
145	// spend of 0 — measured, not argued: uint64(-1) makes the operand exceed isqrt128's
146	// exact range, r comes back as its search bound 2^63, int64(r) is MinInt64, and
147	// s1 - from WRAPS to MaxInt64, which the delta <= 0 exit below then waves through
148	// as a positive mint.
149	var s1 int64
150	if r > uint64(c.cap) {
151		s1 = c.cap // clamp before the correction so Cost never sees an over-cap span
152	} else {
153		s1 = int64(r)
154	}
155
156	// Step DOWN while the rounded-up cost of reaching s1 exceeds the payment.
157	for s1 > from {
158		cst, ok := c.Cost(from, s1-from)
159		if ok && cst <= coin {
160			break
161		}
162		s1--
163	}
164	// Step UP while one more coin still fits (and stays under the cap).
165	for s1 < c.cap {
166		cst, ok := c.Cost(from, s1+1-from)
167		if !ok || cst > coin {
168			break
169		}
170		s1++
171	}
172	delta = s1 - from
173	if delta <= 0 {
174		return 0, 0
175	}
176	spent, _ = c.Cost(from, delta) // round-up cost of exactly what was minted
177	return delta, spent
178}
179
180// Price is the marginal price at position s (coin per unit), floor(s/d). Backing
181// is the reserve behind one unit at position s, exactly half the marginal price —
182// the reason every buyer pays about twice the backing of the coin they buy.
183func (c Curve) Price(s int64) int64   { return s / c.d }
184func (c Curve) Backing(s int64) int64 { return s / (2 * c.d) }
185
186// isqrt128 returns floor(sqrt(x)) for the 128-bit unsigned value x = hi:lo, by
187// binary search on the ≤64-bit result — each step squares a candidate with a
188// 128-bit multiply and compares. Division-free and deterministic.
189func isqrt128(hi, lo uint64) uint64 {
190	var ans, lo_ uint64
191	hi_ := uint64(1) << 63 // search bound: covers every result up to sqrt(2^126)=2^63 (our operands stay < 2^100)
192	for lo_ <= hi_ {
193		mid := lo_ + (hi_-lo_)/2
194		mh, ml := bits.Mul64(mid, mid)
195		if mh < hi || (mh == hi && ml <= lo) { // mid² ≤ x
196			ans = mid
197			if mid == ^uint64(0) {
198				break
199			}
200			lo_ = mid + 1
201		} else {
202			if mid == 0 {
203				break
204			}
205			hi_ = mid - 1
206		}
207	}
208	return ans
209}