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

ammmath.gno

6.88 Kb · 240 lines
  1// Package ammmath implements integer bonding-curve and constant-product AMM
  2// helpers for the gnomemepad factory (Pump-style virtual CPMM + real CPMM pool).
  3package ammmathv2
  4
  5import (
  6	"math/bits"
  7	"math/overflow"
  8)
  9
 10// FeeResult is the split of a gross trade fee.
 11type FeeResult struct {
 12	Gross     int64
 13	Net       int64
 14	Fee       int64
 15	Creator   int64
 16	Protocol  int64
 17	Remainder int64
 18}
 19
 20// maxInt64 is 2^63-1; MulDiv results must fit here.
 21const maxInt64 = int64(^uint64(0) >> 1)
 22
 23// MulDiv returns floor((x * y) / d) for x,y >= 0 and d > 0.
 24// Uses a 128-bit intermediate so mainnet-scale k (VU*VT ≈ 1.1e20) does not overflow.
 25// Panics on bad domain or if the quotient does not fit in int64.
 26func MulDiv(x, y, d int64) int64 {
 27	if x < 0 || y < 0 {
 28		panic("ammmath: MulDiv negative operand")
 29	}
 30	if d <= 0 {
 31		panic("ammmath: MulDiv non-positive divisor")
 32	}
 33	if x == 0 || y == 0 {
 34		return 0
 35	}
 36	if prod, ok := overflow.Mul64(x, y); ok {
 37		return prod / d
 38	}
 39	hi, lo := bits.Mul64(uint64(x), uint64(y))
 40	// bits.Div64 panics if d == 0 or uint64(d) <= hi (quotient ≥ 2^64).
 41	quo, _ := bits.Div64(hi, lo, uint64(d))
 42	if quo > uint64(maxInt64) {
 43		panic("ammmath: MulDiv result overflow")
 44	}
 45	return int64(quo)
 46}
 47
 48// ApplyFee takes gross quote paid by the user and feeBPS (e.g. 120 = 1.20%).
 49func ApplyFee(gross, feeBPS, creatorShareBPS, protocolShareBPS int64) FeeResult {
 50	if gross <= 0 {
 51		panic("ammmath: gross must be positive")
 52	}
 53	if feeBPS < 0 || feeBPS >= 10000 {
 54		panic("ammmath: feeBPS out of range")
 55	}
 56	if creatorShareBPS < 0 || protocolShareBPS < 0 || creatorShareBPS+protocolShareBPS > 10000 {
 57		panic("ammmath: fee share BPS invalid")
 58	}
 59	fee := gross * feeBPS / 10000
 60	net := gross - fee
 61	creator := fee * creatorShareBPS / 10000
 62	protocol := fee * protocolShareBPS / 10000
 63	remainder := fee - creator - protocol
 64	return FeeResult{
 65		Gross:     gross,
 66		Net:       net,
 67		Fee:       fee,
 68		Creator:   creator,
 69		Protocol:  protocol,
 70		Remainder: remainder,
 71	}
 72}
 73
 74// ApplyFeeOnOutput charges fee on assets leaving the pool/curve.
 75func ApplyFeeOnOutput(grossOut, feeBPS, creatorShareBPS, protocolShareBPS int64) FeeResult {
 76	if grossOut <= 0 {
 77		panic("ammmath: grossOut must be positive")
 78	}
 79	if feeBPS < 0 || feeBPS >= 10000 {
 80		panic("ammmath: feeBPS out of range")
 81	}
 82	if creatorShareBPS < 0 || protocolShareBPS < 0 || creatorShareBPS+protocolShareBPS > 10000 {
 83		panic("ammmath: fee share BPS invalid")
 84	}
 85	fee := grossOut * feeBPS / 10000
 86	net := grossOut - fee
 87	if net <= 0 {
 88		panic("ammmath: fee consumes entire output")
 89	}
 90	creator := fee * creatorShareBPS / 10000
 91	protocol := fee * protocolShareBPS / 10000
 92	remainder := fee - creator - protocol
 93	return FeeResult{
 94		Gross:     grossOut,
 95		Net:       net,
 96		Fee:       fee,
 97		Creator:   creator,
 98		Protocol:  protocol,
 99		Remainder: remainder,
100	}
101}
102
103// BuyTokens quotes a virtual constant-product buy (net ugnot in).
104func BuyTokens(virtualUgnot, virtualToken, ugnotIn int64) (tokensOut, newVU, newVT int64) {
105	if ugnotIn <= 0 {
106		panic("ammmath: ugnotIn must be positive")
107	}
108	if virtualUgnot <= 0 || virtualToken <= 0 {
109		panic("ammmath: invalid virtual reserves")
110	}
111	newVU, ok := overflow.Add64(virtualUgnot, ugnotIn)
112	if !ok {
113		panic("ammmath: virtual ugnot overflow")
114	}
115	// newVT = floor((VU*VT)/newVU) without materializing k in int64
116	newVT = MulDiv(virtualUgnot, virtualToken, newVU)
117	if newVT <= 0 {
118		panic("ammmath: empty virtual token reserve")
119	}
120	if newVT >= virtualToken {
121		panic("ammmath: zero tokens out")
122	}
123	tokensOut = virtualToken - newVT
124	return tokensOut, newVU, newVT
125}
126
127// MaxNetInForTokenOut is the largest net ugnot in that yields tokensOut ≤ maxTokensOut
128// (integer CPMM, same floor rules as BuyTokens). Used to fill the last curve tokens
129// without panicking when the user sends too much GNOT.
130func MaxNetInForTokenOut(virtualUgnot, virtualToken, maxTokensOut int64) int64 {
131	if maxTokensOut <= 0 || virtualUgnot <= 0 || virtualToken <= 0 {
132		return 0
133	}
134	if maxTokensOut >= virtualToken {
135		return 0
136	}
137	targetNewVT := virtualToken - maxTokensOut
138	// maxNewVU = floor((VU*VT)/targetNewVT)
139	maxNewVU := MulDiv(virtualUgnot, virtualToken, targetNewVT)
140	if maxNewVU <= virtualUgnot {
141		return 0
142	}
143	return maxNewVU - virtualUgnot
144}
145
146// SellTokens quotes a virtual constant-product sell (gross ugnot out).
147func SellTokens(virtualUgnot, virtualToken, tokensIn int64) (ugnotOut, newVU, newVT int64) {
148	if tokensIn <= 0 {
149		panic("ammmath: tokensIn must be positive")
150	}
151	if virtualUgnot <= 0 || virtualToken <= 0 {
152		panic("ammmath: invalid virtual reserves")
153	}
154	newVT, ok := overflow.Add64(virtualToken, tokensIn)
155	if !ok {
156		panic("ammmath: virtual token overflow")
157	}
158	newVU = MulDiv(virtualUgnot, virtualToken, newVT)
159	if newVU <= 0 {
160		panic("ammmath: empty virtual ugnot reserve")
161	}
162	if newVU >= virtualUgnot {
163		panic("ammmath: zero ugnot out")
164	}
165	ugnotOut = virtualUgnot - newVU
166	return ugnotOut, newVU, newVT
167}
168
169// PoolSwapUgnotForToken swaps net ugnot for tokens; remainderToPool adds to ugnot reserve.
170func PoolSwapUgnotForToken(poolUgnot, poolToken, ugnotIn, remainderToPool int64) (tokensOut, newPU, newPT int64) {
171	if ugnotIn <= 0 {
172		panic("ammmath: ugnotIn must be positive")
173	}
174	if poolUgnot <= 0 || poolToken <= 0 {
175		panic("ammmath: invalid pool reserves")
176	}
177	if remainderToPool < 0 {
178		panic("ammmath: negative remainder")
179	}
180	addU, ok := overflow.Add64(ugnotIn, remainderToPool)
181	if !ok {
182		panic("ammmath: add overflow")
183	}
184	newPU, ok = overflow.Add64(poolUgnot, addU)
185	if !ok {
186		panic("ammmath: pool ugnot overflow")
187	}
188	den, ok := overflow.Add64(poolUgnot, ugnotIn)
189	if !ok {
190		panic("ammmath: pool denom overflow")
191	}
192	tokensOut = MulDiv(poolToken, ugnotIn, den)
193	if tokensOut <= 0 {
194		panic("ammmath: zero tokens out of pool")
195	}
196	if tokensOut >= poolToken {
197		panic("ammmath: would drain pool tokens")
198	}
199	newPT = poolToken - tokensOut
200	return tokensOut, newPU, newPT
201}
202
203// PoolSwapTokenForUgnot swaps tokens for gross ugnot out.
204func PoolSwapTokenForUgnot(poolUgnot, poolToken, tokensIn int64) (ugnotOut, newPU, newPT int64) {
205	if tokensIn <= 0 {
206		panic("ammmath: tokensIn must be positive")
207	}
208	if poolUgnot <= 0 || poolToken <= 0 {
209		panic("ammmath: invalid pool reserves")
210	}
211	newPT, ok := overflow.Add64(poolToken, tokensIn)
212	if !ok {
213		panic("ammmath: pool token overflow")
214	}
215	ugnotOut = MulDiv(poolUgnot, tokensIn, newPT)
216	if ugnotOut <= 0 {
217		panic("ammmath: zero ugnot out of pool")
218	}
219	if ugnotOut >= poolUgnot {
220		panic("ammmath: would drain pool ugnot")
221	}
222	newPU = poolUgnot - ugnotOut
223	return ugnotOut, newPU, newPT
224}
225
226// CanGraduate reports whether raised net ugnot meets the threshold.
227func CanGraduate(raisedUgnot, threshold int64) bool {
228	return raisedUgnot >= threshold && threshold > 0
229}
230
231// SpotPriceUgnotPerToken returns ugnot per token scaled by 1e6 (display only).
232func SpotPriceUgnotPerToken(ugnotReserve, tokenReserve int64) int64 {
233	if tokenReserve <= 0 {
234		return 0
235	}
236	if ugnotReserve <= 0 {
237		return 0
238	}
239	return MulDiv(ugnotReserve, 1_000_000, tokenReserve)
240}