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}