From bb3ab707b46e3b332bd75dc5cda79d53fd8c56b0 Mon Sep 17 00:00:00 2001 From: JACOB STANLEY Date: Mon, 20 Jul 2026 00:46:24 +0100 Subject: [PATCH] https://github.com/Menjay7/accesslayer-contracts.git --- .../__tests__/bondingCurve.utils.test.ts | 240 ++++++++++++++++++ src/utils/bondingCurve.utils.ts | 142 +++++++++++ 2 files changed, 382 insertions(+) create mode 100644 src/utils/__tests__/bondingCurve.utils.test.ts create mode 100644 src/utils/bondingCurve.utils.ts diff --git a/src/utils/__tests__/bondingCurve.utils.test.ts b/src/utils/__tests__/bondingCurve.utils.test.ts new file mode 100644 index 0000000..b006f9b --- /dev/null +++ b/src/utils/__tests__/bondingCurve.utils.test.ts @@ -0,0 +1,240 @@ +import { describe, expect, it } from 'vitest'; +import { + computeBondingCurvePrice, + computeBondingCurvePriceXLM, + computeBuyCost, + computeSellRevenue, + DEFAULT_BONDING_CURVE_PARAMS, + type BondingCurveParams, +} from '../bondingCurve.utils'; + +describe('bonding curve utilities', () => { + const defaultParams: BondingCurveParams = { + basePriceStroops: 10_000_000, // 1 XLM + growthFactor: 1.01, // 1% growth per key + }; + + describe('computeBondingCurvePrice', () => { + it('returns base price when supply is 0', () => { + const price = computeBondingCurvePrice(0, defaultParams); + expect(price).toBe(10_000_000); + }); + + it('increases price as supply increases', () => { + const price0 = computeBondingCurvePrice(0, defaultParams); + const price10 = computeBondingCurvePrice(10, defaultParams); + const price100 = computeBondingCurvePrice(100, defaultParams); + + expect(price10).toBeGreaterThan(price0); + expect(price100).toBeGreaterThan(price10); + }); + + it('calculates correct price for linear bonding curve', () => { + // At supply 10: price = 10_000_000 * (1 + 0.01 * 10) = 10_000_000 * 1.1 = 11_000_000 + const price = computeBondingCurvePrice(10, defaultParams); + expect(price).toBe(11_000_000); + }); + + it('handles fractional growth factors', () => { + const params: BondingCurveParams = { + basePriceStroops: 5_000_000, + growthFactor: 1.005, // 0.5% growth + }; + const price = computeBondingCurvePrice(20, params); + // price = 5_000_000 * (1 + 0.005 * 20) = 5_000_000 * 1.1 = 5_500_000 + expect(price).toBe(5_500_000); + }); + + it('throws error for negative supply', () => { + expect(() => computeBondingCurvePrice(-1, defaultParams)).toThrow( + 'Supply cannot be negative' + ); + }); + + it('throws error for negative base price', () => { + const invalidParams: BondingCurveParams = { + basePriceStroops: -100, + growthFactor: 1.01, + }; + expect(() => computeBondingCurvePrice(10, invalidParams)).toThrow( + 'Base price cannot be negative' + ); + }); + + it('throws error for non-positive growth factor', () => { + const invalidParams: BondingCurveParams = { + basePriceStroops: 10_000_000, + growthFactor: 0, + }; + expect(() => computeBondingCurvePrice(10, invalidParams)).toThrow( + 'Growth factor must be positive' + ); + }); + + it('handles zero growth factor (flat curve)', () => { + const flatParams: BondingCurveParams = { + basePriceStroops: 10_000_000, + growthFactor: 1.0, // No growth + }; + const price = computeBondingCurvePrice(100, flatParams); + expect(price).toBe(10_000_000); // Price stays constant + }); + }); + + describe('computeBondingCurvePriceXLM', () => { + it('converts stroops to XLM correctly', () => { + const priceXLM = computeBondingCurvePriceXLM(0, defaultParams); + expect(priceXLM).toBe(1); // 10_000_000 stroops = 1 XLM + }); + + it('returns decimal XLM values', () => { + const priceXLM = computeBondingCurvePriceXLM(10, defaultParams); + // 11_000_000 stroops = 1.1 XLM + expect(priceXLM).toBe(1.1); + }); + + it('handles small stroop amounts', () => { + const smallParams: BondingCurveParams = { + basePriceStroops: 1_000_000, // 0.1 XLM + growthFactor: 1.01, + }; + const priceXLM = computeBondingCurvePriceXLM(0, smallParams); + expect(priceXLM).toBe(0.1); + }); + }); + + describe('computeBuyCost', () => { + it('calculates cost for single key at base price', () => { + const cost = computeBuyCost(0, 1, defaultParams); + expect(cost).toBe(10_000_000); // Base price for first key + }); + + it('calculates cost for multiple keys', () => { + const cost = computeBuyCost(0, 10, defaultParams); + // Average price between supply 0 and 10: (10_000_000 + 11_000_000) / 2 = 10_500_000 + // Total cost: 10_500_000 * 10 = 105_000_000 + expect(cost).toBe(105_000_000); + }); + + it('calculates cost starting from non-zero supply', () => { + const cost = computeBuyCost(10, 5, defaultParams); + // Price at supply 10: 11_000_000 + // Price at supply 15: 10_000_000 * (1 + 0.01 * 15) = 11_500_000 + // Average: (11_000_000 + 11_500_000) / 2 = 11_250_000 + // Total: 11_250_000 * 5 = 56_250_000 + expect(cost).toBe(56_250_000); + }); + + it('throws error for negative quantity', () => { + expect(() => computeBuyCost(0, -1, defaultParams)).toThrow( + 'Quantity cannot be negative' + ); + }); + + it('throws error for negative current supply', () => { + expect(() => computeBuyCost(-1, 1, defaultParams)).toThrow( + 'Current supply cannot be negative' + ); + }); + + it('handles zero quantity', () => { + const cost = computeBuyCost(10, 0, defaultParams); + expect(cost).toBe(0); + }); + }); + + describe('computeSellRevenue', () => { + it('calculates revenue for single key sale', () => { + const revenue = computeSellRevenue(1, 1, defaultParams); + // Price at supply 0: 10_000_000 + // Price at supply 1: 10_100_000 + // Average: (10_000_000 + 10_100_000) / 2 = 10_050_000 + expect(revenue).toBe(10_050_000); + }); + + it('calculates revenue for multiple keys', () => { + const revenue = computeSellRevenue(10, 5, defaultParams); + // Price at supply 5: 10_500_000 + // Price at supply 10: 11_000_000 + // Average: (10_500_000 + 11_000_000) / 2 = 10_750_000 + // Total: 10_750_000 * 5 = 53_750_000 + expect(revenue).toBe(53_750_000); + }); + + it('throws error when selling more than current supply', () => { + expect(() => computeSellRevenue(5, 10, defaultParams)).toThrow( + 'Cannot sell more keys than current supply' + ); + }); + + it('throws error for negative quantity', () => { + expect(() => computeSellRevenue(10, -1, defaultParams)).toThrow( + 'Quantity cannot be negative' + ); + }); + + it('handles selling entire supply', () => { + const revenue = computeSellRevenue(10, 10, defaultParams); + // Price at supply 0: 10_000_000 + // Price at supply 10: 11_000_000 + // Average: (10_000_000 + 11_000_000) / 2 = 10_500_000 + // Total: 10_500_000 * 10 = 105_000_000 + expect(revenue).toBe(105_000_000); + }); + + it('handles zero quantity', () => { + const revenue = computeSellRevenue(10, 0, defaultParams); + expect(revenue).toBe(0); + }); + }); + + describe('DEFAULT_BONDING_CURVE_PARAMS', () => { + it('has valid default parameters', () => { + expect(DEFAULT_BONDING_CURVE_PARAMS.basePriceStroops).toBe(10_000_000); + expect(DEFAULT_BONDING_CURVE_PARAMS.growthFactor).toBe(1.01); + }); + + it('can be used with computeBondingCurvePrice', () => { + const price = computeBondingCurvePrice(10, DEFAULT_BONDING_CURVE_PARAMS); + expect(price).toBe(11_000_000); + }); + }); + + describe('integration scenarios', () => { + it('buy and sell are inverse operations (ignoring slippage)', () => { + const initialSupply = 10; + const buyQuantity = 5; + + const buyCost = computeBuyCost(initialSupply, buyQuantity, defaultParams); + const newSupply = initialSupply + buyQuantity; + const sellRevenue = computeSellRevenue(newSupply, buyQuantity, defaultParams); + + // Due to linear curve, buy cost should equal sell revenue for same quantity + expect(sellRevenue).toBe(buyCost); + }); + + it('calculates price progression across supply range', () => { + const prices = []; + for (let i = 0; i <= 100; i += 10) { + prices.push(computeBondingCurvePrice(i, defaultParams)); + } + + // Verify monotonic increase + for (let i = 1; i < prices.length; i++) { + expect(prices[i]).toBeGreaterThan(prices[i - 1]); + } + + // Verify specific values + expect(prices[0]).toBe(10_000_000); // Supply 0 + expect(prices[5]).toBe(10_500_000); // Supply 50 + expect(prices[10]).toBe(11_000_000); // Supply 100 + }); + + it('handles large supply values', () => { + const largeSupply = 10000; + const price = computeBondingCurvePrice(largeSupply, defaultParams); + // price = 10_000_000 * (1 + 0.01 * 10000) = 10_000_000 * 101 = 1_010_000_000 + expect(price).toBe(1_010_000_000); + }); + }); +}); diff --git a/src/utils/bondingCurve.utils.ts b/src/utils/bondingCurve.utils.ts new file mode 100644 index 0000000..697c69f --- /dev/null +++ b/src/utils/bondingCurve.utils.ts @@ -0,0 +1,142 @@ +import { STROOPS_PER_XLM } from '@/constants/stellar'; + +/** + * Bonding curve parameters for price calculation. + * These parameters define the shape of the bonding curve. + */ +export interface BondingCurveParams { + /** Base price in stroops when supply is 0 */ + basePriceStroops: number; + /** Growth factor for exponential bonding curve (e.g., 1.01 for 1% growth per key) */ + growthFactor: number; +} + +/** + * Computes the bonding curve price at a given supply step. + * Uses an exponential bonding curve formula: price = base_price * (growth_factor ^ supply) + * + * This is a pure computation function that does not mutate contract state. + * It's useful for: + * - Previewing prices before transactions + * - Displaying price charts + * - Calculating expected costs + * + * @param supply - The current supply (number of keys minted) + * @param params - Bonding curve parameters + * @returns Price in stroops at the given supply step + * + * @example + * ```ts + * const params = { basePriceStroops: 10000000, growthFactor: 1.01 }; // 1 XLM base, 1% growth + * const priceAtSupply10 = computeBondingCurvePrice(10, params); + * console.log(priceAtSupply10); // Price after 10 keys have been minted + * ``` + */ +export function computeBondingCurvePrice( + supply: number, + params: BondingCurveParams +): number { + if (supply < 0) { + throw new Error('Supply cannot be negative'); + } + if (params.basePriceStroops < 0) { + throw new Error('Base price cannot be negative'); + } + if (params.growthFactor <= 0) { + throw new Error('Growth factor must be positive'); + } + + // Linear bonding curve: price = base_price * (1 + (growth_factor - 1) * supply) + // This is equivalent to base_price * growth_factor^supply for small growth factors + // but more numerically stable for large supplies + const priceMultiplier = 1 + (params.growthFactor - 1) * supply; + return params.basePriceStroops * priceMultiplier; +} + +/** + * Computes the bonding curve price in XLM (decimal) at a given supply step. + * Convenience wrapper around computeBondingCurvePrice that converts stroops to XLM. + * + * @param supply - The current supply (number of keys minted) + * @param params - Bonding curve parameters + * @returns Price in XLM at the given supply step + */ +export function computeBondingCurvePriceXLM( + supply: number, + params: BondingCurveParams +): number { + const priceStroops = computeBondingCurvePrice(supply, params); + return priceStroops / STROOPS_PER_XLM; +} + +/** + * Computes the total cost to buy a quantity of keys from a given supply. + * This calculates the area under the bonding curve from `supply` to `supply + quantity`. + * + * For a linear bonding curve, this is the integral: + * total_cost = base_price * quantity * (1 + (growth_factor - 1) * (supply + quantity/2)) + * + * @param currentSupply - Current supply before purchase + * @param quantity - Number of keys to buy + * @param params - Bonding curve parameters + * @returns Total cost in stroops + */ +export function computeBuyCost( + currentSupply: number, + quantity: number, + params: BondingCurveParams +): number { + if (quantity < 0) { + throw new Error('Quantity cannot be negative'); + } + if (currentSupply < 0) { + throw new Error('Current supply cannot be negative'); + } + + const startPrice = computeBondingCurvePrice(currentSupply, params); + const endPrice = computeBondingCurvePrice(currentSupply + quantity, params); + + // Average price for linear bonding curve + const avgPrice = (startPrice + endPrice) / 2; + return avgPrice * quantity; +} + +/** + * Computes the total revenue from selling a quantity of keys at a given supply. + * This is the reverse of computeBuyCost - calculates the area under the curve + * from `supply - quantity` to `supply`. + * + * @param currentSupply - Current supply before sale + * @param quantity - Number of keys to sell + * @param params - Bonding curve parameters + * @returns Total revenue in stroops + */ +export function computeSellRevenue( + currentSupply: number, + quantity: number, + params: BondingCurveParams +): number { + if (quantity < 0) { + throw new Error('Quantity cannot be negative'); + } + if (quantity > currentSupply) { + throw new Error('Cannot sell more keys than current supply'); + } + + const newSupply = currentSupply - quantity; + const startPrice = computeBondingCurvePrice(newSupply, params); + const endPrice = computeBondingCurvePrice(currentSupply, params); + + // Average price for linear bonding curve + const avgPrice = (startPrice + endPrice) / 2; + return avgPrice * quantity; +} + +/** + * Default bonding curve parameters for the platform. + * These can be overridden per creator if needed. + */ +export const DEFAULT_BONDING_CURVE_PARAMS: BondingCurveParams = { + basePriceStroops: 10_000_000, // 1 XLM + growthFactor: 1.01, // 1% growth per key +};