// Package ammmath implements integer bonding-curve and constant-product AMM // helpers for the gnomemepad factory (Pump-style virtual CPMM + real CPMM pool). package ammmathv2 import ( "math/bits" "math/overflow" ) // FeeResult is the split of a gross trade fee. type FeeResult struct { Gross int64 Net int64 Fee int64 Creator int64 Protocol int64 Remainder int64 } // maxInt64 is 2^63-1; MulDiv results must fit here. const maxInt64 = int64(^uint64(0) >> 1) // MulDiv returns floor((x * y) / d) for x,y >= 0 and d > 0. // Uses a 128-bit intermediate so mainnet-scale k (VU*VT ≈ 1.1e20) does not overflow. // Panics on bad domain or if the quotient does not fit in int64. func MulDiv(x, y, d int64) int64 { if x < 0 || y < 0 { panic("ammmath: MulDiv negative operand") } if d <= 0 { panic("ammmath: MulDiv non-positive divisor") } if x == 0 || y == 0 { return 0 } if prod, ok := overflow.Mul64(x, y); ok { return prod / d } hi, lo := bits.Mul64(uint64(x), uint64(y)) // bits.Div64 panics if d == 0 or uint64(d) <= hi (quotient ≥ 2^64). quo, _ := bits.Div64(hi, lo, uint64(d)) if quo > uint64(maxInt64) { panic("ammmath: MulDiv result overflow") } return int64(quo) } // ApplyFee takes gross quote paid by the user and feeBPS (e.g. 120 = 1.20%). func ApplyFee(gross, feeBPS, creatorShareBPS, protocolShareBPS int64) FeeResult { if gross <= 0 { panic("ammmath: gross must be positive") } if feeBPS < 0 || feeBPS >= 10000 { panic("ammmath: feeBPS out of range") } if creatorShareBPS < 0 || protocolShareBPS < 0 || creatorShareBPS+protocolShareBPS > 10000 { panic("ammmath: fee share BPS invalid") } fee := gross * feeBPS / 10000 net := gross - fee creator := fee * creatorShareBPS / 10000 protocol := fee * protocolShareBPS / 10000 remainder := fee - creator - protocol return FeeResult{ Gross: gross, Net: net, Fee: fee, Creator: creator, Protocol: protocol, Remainder: remainder, } } // ApplyFeeOnOutput charges fee on assets leaving the pool/curve. func ApplyFeeOnOutput(grossOut, feeBPS, creatorShareBPS, protocolShareBPS int64) FeeResult { if grossOut <= 0 { panic("ammmath: grossOut must be positive") } if feeBPS < 0 || feeBPS >= 10000 { panic("ammmath: feeBPS out of range") } if creatorShareBPS < 0 || protocolShareBPS < 0 || creatorShareBPS+protocolShareBPS > 10000 { panic("ammmath: fee share BPS invalid") } fee := grossOut * feeBPS / 10000 net := grossOut - fee if net <= 0 { panic("ammmath: fee consumes entire output") } creator := fee * creatorShareBPS / 10000 protocol := fee * protocolShareBPS / 10000 remainder := fee - creator - protocol return FeeResult{ Gross: grossOut, Net: net, Fee: fee, Creator: creator, Protocol: protocol, Remainder: remainder, } } // BuyTokens quotes a virtual constant-product buy (net ugnot in). func BuyTokens(virtualUgnot, virtualToken, ugnotIn int64) (tokensOut, newVU, newVT int64) { if ugnotIn <= 0 { panic("ammmath: ugnotIn must be positive") } if virtualUgnot <= 0 || virtualToken <= 0 { panic("ammmath: invalid virtual reserves") } newVU, ok := overflow.Add64(virtualUgnot, ugnotIn) if !ok { panic("ammmath: virtual ugnot overflow") } // newVT = floor((VU*VT)/newVU) without materializing k in int64 newVT = MulDiv(virtualUgnot, virtualToken, newVU) if newVT <= 0 { panic("ammmath: empty virtual token reserve") } if newVT >= virtualToken { panic("ammmath: zero tokens out") } tokensOut = virtualToken - newVT return tokensOut, newVU, newVT } // MaxNetInForTokenOut is the largest net ugnot in that yields tokensOut ≤ maxTokensOut // (integer CPMM, same floor rules as BuyTokens). Used to fill the last curve tokens // without panicking when the user sends too much GNOT. func MaxNetInForTokenOut(virtualUgnot, virtualToken, maxTokensOut int64) int64 { if maxTokensOut <= 0 || virtualUgnot <= 0 || virtualToken <= 0 { return 0 } if maxTokensOut >= virtualToken { return 0 } targetNewVT := virtualToken - maxTokensOut // maxNewVU = floor((VU*VT)/targetNewVT) maxNewVU := MulDiv(virtualUgnot, virtualToken, targetNewVT) if maxNewVU <= virtualUgnot { return 0 } return maxNewVU - virtualUgnot } // SellTokens quotes a virtual constant-product sell (gross ugnot out). func SellTokens(virtualUgnot, virtualToken, tokensIn int64) (ugnotOut, newVU, newVT int64) { if tokensIn <= 0 { panic("ammmath: tokensIn must be positive") } if virtualUgnot <= 0 || virtualToken <= 0 { panic("ammmath: invalid virtual reserves") } newVT, ok := overflow.Add64(virtualToken, tokensIn) if !ok { panic("ammmath: virtual token overflow") } newVU = MulDiv(virtualUgnot, virtualToken, newVT) if newVU <= 0 { panic("ammmath: empty virtual ugnot reserve") } if newVU >= virtualUgnot { panic("ammmath: zero ugnot out") } ugnotOut = virtualUgnot - newVU return ugnotOut, newVU, newVT } // PoolSwapUgnotForToken swaps net ugnot for tokens; remainderToPool adds to ugnot reserve. func PoolSwapUgnotForToken(poolUgnot, poolToken, ugnotIn, remainderToPool int64) (tokensOut, newPU, newPT int64) { if ugnotIn <= 0 { panic("ammmath: ugnotIn must be positive") } if poolUgnot <= 0 || poolToken <= 0 { panic("ammmath: invalid pool reserves") } if remainderToPool < 0 { panic("ammmath: negative remainder") } addU, ok := overflow.Add64(ugnotIn, remainderToPool) if !ok { panic("ammmath: add overflow") } newPU, ok = overflow.Add64(poolUgnot, addU) if !ok { panic("ammmath: pool ugnot overflow") } den, ok := overflow.Add64(poolUgnot, ugnotIn) if !ok { panic("ammmath: pool denom overflow") } tokensOut = MulDiv(poolToken, ugnotIn, den) if tokensOut <= 0 { panic("ammmath: zero tokens out of pool") } if tokensOut >= poolToken { panic("ammmath: would drain pool tokens") } newPT = poolToken - tokensOut return tokensOut, newPU, newPT } // PoolSwapTokenForUgnot swaps tokens for gross ugnot out. func PoolSwapTokenForUgnot(poolUgnot, poolToken, tokensIn int64) (ugnotOut, newPU, newPT int64) { if tokensIn <= 0 { panic("ammmath: tokensIn must be positive") } if poolUgnot <= 0 || poolToken <= 0 { panic("ammmath: invalid pool reserves") } newPT, ok := overflow.Add64(poolToken, tokensIn) if !ok { panic("ammmath: pool token overflow") } ugnotOut = MulDiv(poolUgnot, tokensIn, newPT) if ugnotOut <= 0 { panic("ammmath: zero ugnot out of pool") } if ugnotOut >= poolUgnot { panic("ammmath: would drain pool ugnot") } newPU = poolUgnot - ugnotOut return ugnotOut, newPU, newPT } // CanGraduate reports whether raised net ugnot meets the threshold. func CanGraduate(raisedUgnot, threshold int64) bool { return raisedUgnot >= threshold && threshold > 0 } // SpotPriceUgnotPerToken returns ugnot per token scaled by 1e6 (display only). func SpotPriceUgnotPerToken(ugnotReserve, tokenReserve int64) int64 { if tokenReserve <= 0 { return 0 } if ugnotReserve <= 0 { return 0 } return MulDiv(ugnotReserve, 1_000_000, tokenReserve) }