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

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}