diff --git a/pkg/solana/currencycreator/accounts_currency_config.go b/pkg/solana/currencycreator/accounts_currency_config.go new file mode 100644 index 00000000..0dada560 --- /dev/null +++ b/pkg/solana/currencycreator/accounts_currency_config.go @@ -0,0 +1,97 @@ +package currencycreator + +import ( + "bytes" + "crypto/ed25519" + "fmt" + + "github.com/mr-tron/base58" +) + +const ( + DefaultMintMaxSupply = 21_000_000_000_000 // 21mm tokens with 6 decimals + DefaultMintDecimals = 6 +) + +const ( + MaxCurrencyConfigAccountNameLength = 32 + MaxCurrencyConfigAccountSymbolLength = 8 +) + +const ( + CurrencyConfigAccountSize = (8 + //discriminator + 32 + // authority + 32 + // creator + 32 + // mint + MaxCurrencyConfigAccountNameLength + // name + MaxCurrencyConfigAccountSymbolLength + // symbol + 32 + // seed + 8 + // max_supply + 8 + // current_supply + 1 + // decimal_places + 1 + // bump + 1 + // mint_bump + 5) // padding +) + +var CurrencyConfigAccountDiscriminator = []byte{byte(AccountTypeCurrencyConfig), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} + +type CurrencyConfigAccount struct { + Authority ed25519.PublicKey + Creator ed25519.PublicKey + Mint ed25519.PublicKey + Name string + Symbol string + Seed ed25519.PublicKey + MaxSupply uint64 + CurrentSupply uint64 + DecimalPlaces uint8 + Bump uint8 + MintBump uint8 +} + +func (obj *CurrencyConfigAccount) Unmarshal(data []byte) error { + if len(data) < CurrencyConfigAccountSize { + return ErrInvalidAccountData + } + + var offset int + + var discriminator []byte + getDiscriminator(data, &discriminator, &offset) + if !bytes.Equal(discriminator, CurrencyConfigAccountDiscriminator) { + return ErrInvalidAccountData + } + + getKey(data, &obj.Authority, &offset) + getKey(data, &obj.Creator, &offset) + getKey(data, &obj.Mint, &offset) + getFixedString(data, &obj.Name, MaxCurrencyConfigAccountNameLength, &offset) + getFixedString(data, &obj.Symbol, MaxCurrencyConfigAccountSymbolLength, &offset) + getKey(data, &obj.Seed, &offset) + getUint64(data, &obj.MaxSupply, &offset) + getUint64(data, &obj.CurrentSupply, &offset) + getUint8(data, &obj.DecimalPlaces, &offset) + getUint8(data, &obj.Bump, &offset) + getUint8(data, &obj.MintBump, &offset) + offset += 5 // padding + + return nil +} + +func (obj *CurrencyConfigAccount) String() string { + return fmt.Sprintf( + "CurrencyConfig{authority=%s,creator=%s,mint=%s,name=%s,symbol=%s,seed=%s,max_supply=%d,currency_supply=%d,decimal_places=%d,bump=%d,mint_bump=%d}", + base58.Encode(obj.Authority), + base58.Encode(obj.Creator), + base58.Encode(obj.Mint), + obj.Name, + obj.Symbol, + base58.Encode(obj.Seed), + obj.MaxSupply, + obj.CurrentSupply, + obj.DecimalPlaces, + obj.Bump, + obj.MintBump, + ) +} diff --git a/pkg/solana/currencycreator/accounts_liquidity_pool.go b/pkg/solana/currencycreator/accounts_liquidity_pool.go new file mode 100644 index 00000000..f8e60720 --- /dev/null +++ b/pkg/solana/currencycreator/accounts_liquidity_pool.go @@ -0,0 +1,131 @@ +package currencycreator + +import ( + "bytes" + "crypto/ed25519" + "fmt" + "time" + + "github.com/mr-tron/base58" +) + +const ( + DefaultBuyCap = 0 // unlimited base tokens + DefaultSaleCap = 0 // unlimited target tokens + + DefaultBuyFeeBps = 0 // 0% fee + DefaultSellFeeBps = 100 // 1% fee +) + +const ( + LiquidityPoolAccountSize = (8 + //discriminator + 32 + // authority + 32 + // currency + 32 + // target_mint + 32 + // base_mint + 32 + // vault_target + 32 + // vault_base + 32 + // fee_target + 32 + // fee_base + 4 + // buy_fee + 4 + // sell_fee + 8 + // created_unix_time + 8 + // go_live_unix_time + 8 + // purchase_cap + 8 + // sale_cap + RawExponentialCurveSize + // curve + 8 + // supply_from_baseonding + 1 + // bump + 1 + // currency_baseump + 1 + // vault_target_baseump + 1 + // vault_base_baseump + 4) // padding +) + +var LiquidityPoolAccountDiscriminator = []byte{byte(AccountTypeLiquidityPool), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} + +type LiquidityPoolAccount struct { + Authority ed25519.PublicKey + Currency ed25519.PublicKey + TargetMint ed25519.PublicKey + BaseMint ed25519.PublicKey + VaultTarget ed25519.PublicKey + VaultBase ed25519.PublicKey + FeeTarget ed25519.PublicKey + FeeBase ed25519.PublicKey + BuyFee uint32 + SellFee uint32 + CreatedUnixTime int64 + GoLiveUnixTime int64 + PurchaseCap uint64 + SaleCap uint64 + Curve RawExponentialCurve + SupplyFromBonding uint64 + Bump uint8 + CurrencyBump uint8 + VaultTargetBump uint8 + VaultBaseBump uint8 +} + +func (obj *LiquidityPoolAccount) Unmarshal(data []byte) error { + if len(data) < LiquidityPoolAccountSize { + return ErrInvalidAccountData + } + + var offset int + + var discriminator []byte + getDiscriminator(data, &discriminator, &offset) + if !bytes.Equal(discriminator, LiquidityPoolAccountDiscriminator) { + return ErrInvalidAccountData + } + + getKey(data, &obj.Authority, &offset) + getKey(data, &obj.Currency, &offset) + getKey(data, &obj.TargetMint, &offset) + getKey(data, &obj.BaseMint, &offset) + getKey(data, &obj.VaultTarget, &offset) + getKey(data, &obj.VaultBase, &offset) + getKey(data, &obj.FeeTarget, &offset) + getKey(data, &obj.FeeBase, &offset) + getUint32(data, &obj.BuyFee, &offset) + getUint32(data, &obj.SellFee, &offset) + getInt64(data, &obj.CreatedUnixTime, &offset) + getInt64(data, &obj.GoLiveUnixTime, &offset) + getUint64(data, &obj.PurchaseCap, &offset) + getUint64(data, &obj.SaleCap, &offset) + getRawExponentialCurve(data, &obj.Curve, &offset) + getUint64(data, &obj.SupplyFromBonding, &offset) + getUint8(data, &obj.Bump, &offset) + getUint8(data, &obj.CurrencyBump, &offset) + getUint8(data, &obj.VaultTargetBump, &offset) + getUint8(data, &obj.VaultBaseBump, &offset) + offset += 4 // padding + + return nil +} + +func (obj *LiquidityPoolAccount) String() string { + return fmt.Sprintf( + "LiquidityPool{authority=%s,currency=%s,target_mint=%s,base_mint=%s,vault_target=%s,vault_base=%s,fee_target=%s,fee_base=%s,buy_fee=%d,sell_fee=%d,created_unix_time=%s,go_live_unix_time=%s,purchase_cap=%d,sale_cap=%d,curve=%s,bump=%d,currency_bump=%d,vault_target_bump=%d,vault_base_bump=%d}", + base58.Encode(obj.Authority), + base58.Encode(obj.Currency), + base58.Encode(obj.TargetMint), + base58.Encode(obj.BaseMint), + base58.Encode(obj.VaultTarget), + base58.Encode(obj.VaultBase), + base58.Encode(obj.FeeTarget), + base58.Encode(obj.FeeBase), + obj.BuyFee, + obj.SellFee, + time.Unix(obj.CreatedUnixTime, 0).UTC().String(), + time.Unix(obj.GoLiveUnixTime, 0).UTC().String(), + obj.PurchaseCap, + obj.SaleCap, + obj.Curve.String(), + obj.Bump, + obj.CurrencyBump, + obj.VaultTargetBump, + obj.VaultBaseBump, + ) +} diff --git a/pkg/solana/currencycreator/address.go b/pkg/solana/currencycreator/address.go new file mode 100644 index 00000000..2d388607 --- /dev/null +++ b/pkg/solana/currencycreator/address.go @@ -0,0 +1,82 @@ +package currencycreator + +import ( + "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" +) + +var ( + MintPrefix = []byte("mint") + CurrencyPrefix = []byte("currency") + PoolPrefix = []byte("pool") + TreasuryPrefix = []byte("treasury") + MetadataPrefix = []byte("metadata") +) + +type GetMintAddressArgs struct { + Authority ed25519.PublicKey + Name string + Seed ed25519.PublicKey +} + +func GetMintAddress(args *GetMintAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + MintPrefix, + args.Authority, + []byte(toFixedString(args.Name, MaxCurrencyConfigAccountNameLength)), + args.Seed, + ) +} + +type GetCurrencyAddressArgs struct { + Mint ed25519.PublicKey +} + +func GetCurrencyAddress(args *GetCurrencyAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + CurrencyPrefix, + args.Mint, + ) +} + +type GetPoolAddressArgs struct { + Currency ed25519.PublicKey +} + +func GetPoolAddress(args *GetPoolAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + PoolPrefix, + args.Currency, + ) +} + +type GetVaultAddressArgs struct { + Pool ed25519.PublicKey + Mint ed25519.PublicKey +} + +func GetVaultAddress(args *GetVaultAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + TreasuryPrefix, + args.Pool, + args.Mint, + ) +} + +type GetMetadataAddressArgs struct { + Mint ed25519.PublicKey +} + +func GetMetadataAddress(args *GetMetadataAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + METADATA_PROGRAM_ID, + MetadataPrefix, + METADATA_PROGRAM_ID, + args.Mint, + ) +} diff --git a/pkg/solana/currencycreator/estimate.go b/pkg/solana/currencycreator/estimate.go new file mode 100644 index 00000000..63539eb2 --- /dev/null +++ b/pkg/solana/currencycreator/estimate.go @@ -0,0 +1,81 @@ +package currencycreator + +import ( + "math" + "math/big" +) + +type EstimateCurrentPriceArgs struct { + Curve *ExponentialCurve + CurrentSupplyInQuarks uint64 + MintDecimals uint8 +} + +func EstimateCurrentPrice(args *EstimateCurrentPriceArgs) *big.Float { + scale := big.NewFloat(math.Pow10(int(args.MintDecimals))).SetPrec(defaultExponentialCurvePrec) + unscaledCurrentSupply := big.NewFloat(float64(args.CurrentSupplyInQuarks)).SetPrec(defaultExponentialCurvePrec) + scaledCurrentSupply := new(big.Float).Quo(unscaledCurrentSupply, scale) + return args.Curve.SpotPriceAtSupply(scaledCurrentSupply) +} + +type EstimateBuyArgs struct { + BuyAmountInQuarks uint64 + Curve *ExponentialCurve + CurrentSupplyInQuarks uint64 + BuyFeeBps uint32 + TargetMintDecimals uint8 + BaseMintDecimals uint8 +} + +func EstimateBuy(args *EstimateBuyArgs) (uint64, uint64) { + scale := big.NewFloat(math.Pow10(int(args.BaseMintDecimals))).SetPrec(defaultExponentialCurvePrec) + unscaledBuyAmount := big.NewFloat(float64(args.BuyAmountInQuarks)).SetPrec(defaultExponentialCurvePrec) + scaledBuyAmount := new(big.Float).Quo(unscaledBuyAmount, scale) + + scale = big.NewFloat(math.Pow10(int(args.TargetMintDecimals))).SetPrec(defaultExponentialCurvePrec) + unscaledCurrentSupply := big.NewFloat(float64(args.CurrentSupplyInQuarks)).SetPrec(defaultExponentialCurvePrec) + scaledCurrentSupply := new(big.Float).Quo(unscaledCurrentSupply, scale) + + scale = big.NewFloat(math.Pow10(int(args.TargetMintDecimals))).SetPrec(defaultExponentialCurvePrec) + scaledTotalValue := args.Curve.ValueToTokens(scaledCurrentSupply, scaledBuyAmount) + unscaledTotalValue := new(big.Float).Mul(scaledTotalValue, scale) + + feePctValue := new(big.Float).SetPrec(defaultExponentialCurvePrec).Quo(big.NewFloat(float64(args.BuyFeeBps)), big.NewFloat(10000)) + scaledFees := new(big.Float).Mul(scaledTotalValue, feePctValue) + unscaledFees := new(big.Float).Mul(scaledFees, scale) + + total, _ := unscaledTotalValue.Int64() + fees, _ := unscaledFees.Int64() + return uint64(total - fees), uint64(fees) +} + +type EstimateSaleArgs struct { + SellAmountInQuarks uint64 + Curve *ExponentialCurve + CurrentSupplyInQuarks uint64 + SellFeeBps uint32 + TargetMintDecimals uint8 + BaseMintDecimals uint8 +} + +func EstimateSale(args *EstimateSaleArgs) (uint64, uint64) { + scale := big.NewFloat(math.Pow10(int(args.TargetMintDecimals))).SetPrec(defaultExponentialCurvePrec) + unscaledSellAmount := big.NewFloat(float64(args.SellAmountInQuarks)).SetPrec(defaultExponentialCurvePrec) + scaledSellAmount := new(big.Float).Quo(unscaledSellAmount, scale) + + scale = big.NewFloat(math.Pow10(int(args.TargetMintDecimals))).SetPrec(defaultExponentialCurvePrec) + unscaledCurrentSupply := big.NewFloat(float64(args.CurrentSupplyInQuarks)).SetPrec(defaultExponentialCurvePrec) + scaledCurrentSupply := new(big.Float).Quo(unscaledCurrentSupply, scale) + + scale = big.NewFloat(math.Pow10(int(args.BaseMintDecimals))).SetPrec(defaultExponentialCurvePrec) + scaledTotalValue := args.Curve.TokensToValue(scaledCurrentSupply, scaledSellAmount) + unscaledTotalValue := new(big.Float).Mul(scaledTotalValue, scale) + + feePctValue := new(big.Float).SetPrec(defaultExponentialCurvePrec).Quo(big.NewFloat(float64(args.SellFeeBps)), big.NewFloat(10000)) + scaledFees := new(big.Float).Mul(scaledTotalValue, feePctValue) + unscaledFees := new(big.Float).Mul(scaledFees, scale) + + total, _ := unscaledTotalValue.Int64() + fees, _ := unscaledFees.Int64() + return uint64(total - fees), uint64(fees) +} diff --git a/pkg/solana/currencycreator/exponential_curve.go b/pkg/solana/currencycreator/exponential_curve.go new file mode 100644 index 00000000..1a600555 --- /dev/null +++ b/pkg/solana/currencycreator/exponential_curve.go @@ -0,0 +1,92 @@ +package currencycreator + +import ( + "math" + "math/big" +) + +// Note: Generated with Grok 4 based on curve.rs, and not 100% accurate with on-chain program + +const defaultExponentialCurvePrec = 128 + +type ExponentialCurve struct { + a *big.Float + b *big.Float + c *big.Float +} + +func (curve *ExponentialCurve) SpotPriceAtSupply(currentSupply *big.Float) *big.Float { + cTimesS := new(big.Float).Mul(curve.c, currentSupply) + exp := expBig(cTimesS) + return new(big.Float).Mul(new(big.Float).Mul(curve.a, curve.b), exp) +} + +func (curve *ExponentialCurve) TokensToValue(currentSupply, tokens *big.Float) *big.Float { + newSupply := new(big.Float).Add(currentSupply, tokens) + cs := new(big.Float).Mul(curve.c, currentSupply) + ns := new(big.Float).Mul(curve.c, newSupply) + expCS := expBig(cs) + expNS := expBig(ns) + abOverC := new(big.Float).Quo(new(big.Float).Mul(curve.a, curve.b), curve.c) + diff := new(big.Float).Sub(expNS, expCS) + return new(big.Float).Mul(abOverC, diff) +} + +func (curve *ExponentialCurve) ValueToTokens(currentSupply, value *big.Float) *big.Float { + abOverC := new(big.Float).Quo(new(big.Float).Mul(curve.a, curve.b), curve.c) + expCS := expBig(new(big.Float).Mul(curve.c, currentSupply)) + term := new(big.Float).Add(new(big.Float).Quo(value, abOverC), expCS) + lnTerm := logBig(term) + result := new(big.Float).Quo(lnTerm, curve.c) + return new(big.Float).Sub(result, currentSupply) +} + +func DefaultExponentialCurve() *ExponentialCurve { + scale, ok := new(big.Float).SetPrec(defaultExponentialCurvePrec).SetString(defaultCurveScaleString) + if !ok { + panic("Invalid scale string") + } + + aInt, _ := new(big.Int).SetString(defaultCurveAString, 10) + bInt, _ := new(big.Int).SetString(defaultCurveBString, 10) + + a := new(big.Float).Quo(new(big.Float).SetPrec(defaultExponentialCurvePrec).SetInt(aInt), scale) + b := new(big.Float).Quo(new(big.Float).SetPrec(defaultExponentialCurvePrec).SetInt(bInt), scale) + c := new(big.Float).Copy(b) + + return &ExponentialCurve{a: a, b: b, c: c} +} + +func expBig(x *big.Float) *big.Float { + prec := x.Prec() + result := big.NewFloat(1).SetPrec(prec) + term := big.NewFloat(1).SetPrec(prec) + for i := 1; i < 1000; i++ { + term = term.Mul(term, x) + term = term.Quo(term, big.NewFloat(float64(i))) + old := new(big.Float).Copy(result) + result = result.Add(result, term) + if term.Cmp(new(big.Float).SetFloat64(0)) == 0 { + break + } + if old.Cmp(result) == 0 { + break + } + } + return result +} + +func logBig(y *big.Float) *big.Float { + if y.Sign() <= 0 { + panic("log of non-positive number") + } + yf, _ := y.Float64() + z := big.NewFloat(math.Log(yf)).SetPrec(y.Prec()) + for range 50 { + expz := expBig(z) + adjustment := new(big.Float).Quo(y, expz) + z = z.Add(z, adjustment) + z = z.Sub(z, big.NewFloat(1)) + } + return z +} diff --git a/pkg/solana/currencycreator/exponential_curve_test.go b/pkg/solana/currencycreator/exponential_curve_test.go new file mode 100644 index 00000000..ea153480 --- /dev/null +++ b/pkg/solana/currencycreator/exponential_curve_test.go @@ -0,0 +1,59 @@ +package currencycreator + +import ( + "fmt" + "math/big" + "testing" +) + +func TestCalculateCurveConstants(t *testing.T) { + curve := DefaultExponentialCurve() + + // Check R'(0) with tolerance + spot0 := curve.SpotPriceAtSupply(big.NewFloat(0)) + expectedStart := big.NewFloat(0.01) + diff := new(big.Float).Sub(spot0, expectedStart) + threshold, _ := new(big.Float).SetString("0.0000000001") + if diff.Abs(diff).Cmp(threshold) > 0 { + t.Errorf("Spot at 0: got %s, expected 0.01", spot0.Text('f', RawExponentialCurveDecimals)) + } + + // Check R'(21000000) with tolerance + supplyEnd := big.NewFloat(21000000) + spotEnd := curve.SpotPriceAtSupply(supplyEnd) + expectedEnd := big.NewFloat(1000000) + diff = new(big.Float).Sub(spotEnd, expectedEnd) + threshold, _ = new(big.Float).SetString("0.0001") + if diff.Abs(diff).Cmp(threshold) > 0 { + t.Errorf("Spot at end: got %s, expected 1000000", spotEnd.Text('f', RawExponentialCurveDecimals)) + } +} + +func TestGenerateCurveTable(t *testing.T) { + t.Skip() + + curve := DefaultExponentialCurve() + + fmt.Println("|------|----------------|----------------------------------|----------------------------|") + fmt.Println("| % | S | R(S) | R'(S) |") + fmt.Println("|------|----------------|----------------------------------|----------------------------|") + + zero := big.NewFloat(0) + buyAmount := big.NewFloat(210000) + supply := new(big.Float).Copy(zero) + + for i := 0; i <= 100; i++ { + cost := curve.TokensToValue(zero, supply) + spotPrice := curve.SpotPriceAtSupply(supply) + + fmt.Printf("| %3d%% | %14s | %32s | %26s |\n", + i, + supply.Text('f', 0), + cost.Text('f', RawExponentialCurveDecimals), + spotPrice.Text('f', RawExponentialCurveDecimals)) + + supply = supply.Add(supply, buyAmount) + } + + fmt.Println("|------|----------------|----------------------------------|----------------------------|") +} diff --git a/pkg/solana/currencycreator/instructions_buy_tokens.go b/pkg/solana/currencycreator/instructions_buy_tokens.go new file mode 100644 index 00000000..e6db97d6 --- /dev/null +++ b/pkg/solana/currencycreator/instructions_buy_tokens.go @@ -0,0 +1,116 @@ +package currencycreator + +import ( + "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" +) + +const ( + BuyTokensInstructionArgsSize = (8 + // in_amount + 8) // min_amount_out +) + +type BuyTokensInstructionArgs struct { + InAmount uint64 + MinAmountOut uint64 +} + +type BuyTokensInstructionAccounts struct { + Buyer ed25519.PublicKey + Pool ed25519.PublicKey + Currency ed25519.PublicKey + TargetMint ed25519.PublicKey + BaseMint ed25519.PublicKey + VaultTarget ed25519.PublicKey + VaultBase ed25519.PublicKey + BuyerTarget ed25519.PublicKey + BuyerBase ed25519.PublicKey + FeeTarget ed25519.PublicKey + FeeBase ed25519.PublicKey +} + +func NewBuyTokensInstruction( + accounts *BuyTokensInstructionAccounts, + args *BuyTokensInstructionArgs, +) solana.Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, 1+BuyTokensInstructionArgsSize) + + putInstructionType(data, InstructionTypeBuyTokens, &offset) + putUint64(data, args.InAmount, &offset) + putUint64(data, args.MinAmountOut, &offset) + + return solana.Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []solana.AccountMeta{ + { + PublicKey: accounts.Buyer, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.Pool, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.Currency, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.TargetMint, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.BaseMint, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: accounts.VaultTarget, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.VaultBase, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.BuyerTarget, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.BuyerBase, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.FeeTarget, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.FeeBase, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SPL_TOKEN_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + }, + } +} diff --git a/pkg/solana/currencycreator/instructions_initialize_currency.go b/pkg/solana/currencycreator/instructions_initialize_currency.go new file mode 100644 index 00000000..fb26c3a0 --- /dev/null +++ b/pkg/solana/currencycreator/instructions_initialize_currency.go @@ -0,0 +1,111 @@ +package currencycreator + +import ( + "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" +) + +const ( + InitializeCurrencyInstructionArgsSize = (MaxCurrencyConfigAccountNameLength + // name + MaxCurrencyConfigAccountSymbolLength + // symbol + 32 + // seed + 8 + // max_supply + 1 + // decimal_places + 1 + // bump + 1 + // mint_bump + 5) // padding +) + +type InitializeCurrencyInstructionArgs struct { + Name string + Symbol string + Seed ed25519.PublicKey + MaxSupply uint64 + DecimalPlaces uint8 + Bump uint8 + MintBump uint8 +} + +type InitializeCurrencyInstructionAccounts struct { + Authority ed25519.PublicKey + Creator ed25519.PublicKey + Mint ed25519.PublicKey + Currency ed25519.PublicKey + Metadata ed25519.PublicKey +} + +func NewInitializeCurrencyInstruction( + accounts *InitializeCurrencyInstructionAccounts, + args *InitializeCurrencyInstructionArgs, +) solana.Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, 1+InitializeCurrencyInstructionArgsSize) + + putInstructionType(data, InstructionTypeInitializeCurrency, &offset) + putFixedString(data, args.Name, MaxCurrencyConfigAccountNameLength, &offset) + putFixedString(data, args.Symbol, MaxCurrencyConfigAccountSymbolLength, &offset) + putKey(data, args.Seed, &offset) + putUint64(data, args.MaxSupply, &offset) + putUint8(data, args.DecimalPlaces, &offset) + putUint8(data, args.Bump, &offset) + putUint8(data, args.MintBump, &offset) + + return solana.Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []solana.AccountMeta{ + { + PublicKey: accounts.Authority, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.Creator, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: accounts.Mint, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.Currency, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.Metadata, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: SPL_TOKEN_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: METADATA_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SYSTEM_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SYSVAR_RENT_PUBKEY, + IsWritable: false, + IsSigner: false, + }, + }, + } +} diff --git a/pkg/solana/currencycreator/instructions_initialize_pool.go b/pkg/solana/currencycreator/instructions_initialize_pool.go new file mode 100644 index 00000000..320149ff --- /dev/null +++ b/pkg/solana/currencycreator/instructions_initialize_pool.go @@ -0,0 +1,137 @@ +package currencycreator + +import ( + "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" +) + +const ( + InitializePoolInstructionArgsSize = (8 + // supply + RawExponentialCurveSize + // curve + 8 + // purchase_cap + 8 + // sale_cap + 4 + // buy_fee + 4 + // sell_fee + 8 + // go_live_wait_time + 1 + // bump + 1 + // vault_target_bump + 1 + // vault_base_bump + 5) // padding +) + +type InitializePoolInstructionArgs struct { + Supply uint64 + Curve RawExponentialCurve + PurchaseCap uint64 + SaleCap uint64 + BuyFee uint32 + SellFee uint32 + GoLiveWaitTime uint64 + Bump uint8 + VaultTargetBump uint8 + VaultBaseBump uint8 +} + +type InitializePoolInstructionAccounts struct { + Authority ed25519.PublicKey + Currency ed25519.PublicKey + TargetMint ed25519.PublicKey + BaseMint ed25519.PublicKey + Pool ed25519.PublicKey + VaultTarget ed25519.PublicKey + VaultBase ed25519.PublicKey + FeeTarget ed25519.PublicKey + FeeBase ed25519.PublicKey +} + +func NewInitializePoolInstruction( + accounts *InitializePoolInstructionAccounts, + args *InitializePoolInstructionArgs, +) solana.Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, 1+InitializePoolInstructionArgsSize) + + putInstructionType(data, InstructionTypeInitializePool, &offset) + putUint64(data, args.Supply, &offset) + putRawExponentialCurve(data, args.Curve, &offset) + putUint64(data, args.PurchaseCap, &offset) + putUint64(data, args.SaleCap, &offset) + putUint32(data, args.BuyFee, &offset) + putUint32(data, args.SellFee, &offset) + putUint64(data, args.GoLiveWaitTime, &offset) + putUint8(data, args.Bump, &offset) + putUint8(data, args.VaultTargetBump, &offset) + putUint8(data, args.VaultBaseBump, &offset) + + return solana.Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []solana.AccountMeta{ + { + PublicKey: accounts.Authority, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.Currency, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.TargetMint, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.BaseMint, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: accounts.Pool, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.VaultTarget, + IsWritable: true, + IsSigner: false, + }, { + PublicKey: accounts.VaultBase, + IsWritable: true, + IsSigner: false, + }, { + PublicKey: accounts.FeeTarget, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: accounts.FeeBase, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SPL_TOKEN_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SYSTEM_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SYSVAR_RENT_PUBKEY, + IsWritable: false, + IsSigner: false, + }, + }, + } +} diff --git a/pkg/solana/currencycreator/instructions_sell_tokens.go b/pkg/solana/currencycreator/instructions_sell_tokens.go new file mode 100644 index 00000000..c41f5464 --- /dev/null +++ b/pkg/solana/currencycreator/instructions_sell_tokens.go @@ -0,0 +1,116 @@ +package currencycreator + +import ( + "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" +) + +const ( + SellTokensInstructionArgsSize = (8 + // in_amount + 8) // min_amount_out +) + +type SellTokensInstructionArgs struct { + InAmount uint64 + MinAmountOut uint64 +} + +type SellTokensInstructionAccounts struct { + Seller ed25519.PublicKey + Pool ed25519.PublicKey + Currency ed25519.PublicKey + TargetMint ed25519.PublicKey + BaseMint ed25519.PublicKey + VaultTarget ed25519.PublicKey + VaultBase ed25519.PublicKey + SellerTarget ed25519.PublicKey + SellerBase ed25519.PublicKey + FeeTarget ed25519.PublicKey + FeeBase ed25519.PublicKey +} + +func NewSellTokensInstruction( + accounts *SellTokensInstructionAccounts, + args *SellTokensInstructionArgs, +) solana.Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, 1+SellTokensInstructionArgsSize) + + putInstructionType(data, InstructionTypeSellTokens, &offset) + putUint64(data, args.InAmount, &offset) + putUint64(data, args.MinAmountOut, &offset) + + return solana.Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []solana.AccountMeta{ + { + PublicKey: accounts.Seller, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.Pool, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.Currency, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.TargetMint, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.BaseMint, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: accounts.VaultTarget, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.VaultBase, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.SellerTarget, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.SellerBase, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.FeeTarget, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: accounts.FeeBase, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: SPL_TOKEN_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + }, + } +} diff --git a/pkg/solana/currencycreator/program.go b/pkg/solana/currencycreator/program.go new file mode 100644 index 00000000..1593150a --- /dev/null +++ b/pkg/solana/currencycreator/program.go @@ -0,0 +1,25 @@ +package currencycreator + +import ( + "crypto/ed25519" + "errors" +) + +var ( + ErrInvalidProgram = errors.New("invalid program id") + ErrInvalidAccountData = errors.New("unexpected account data") + ErrInvalidInstructionData = errors.New("unexpected instruction data") +) + +var ( + PROGRAM_ADDRESS = mustBase58Decode("ccZLx5N31asHhCa7hFmvdC9EGYVam13L8WXPTjPEiJY") + PROGRAM_ID = ed25519.PublicKey(PROGRAM_ADDRESS) +) + +var ( + METADATA_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s")) + SYSTEM_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("11111111111111111111111111111111")) + SPL_TOKEN_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")) + + SYSVAR_RENT_PUBKEY = ed25519.PublicKey(mustBase58Decode("SysvarRent111111111111111111111111111111111")) +) diff --git a/pkg/solana/currencycreator/types_account_type.go b/pkg/solana/currencycreator/types_account_type.go new file mode 100644 index 00000000..9a201d2f --- /dev/null +++ b/pkg/solana/currencycreator/types_account_type.go @@ -0,0 +1,9 @@ +package currencycreator + +type AccountType uint8 + +const ( + AccountTypeUnknown AccountType = iota + AccountTypeCurrencyConfig + AccountTypeLiquidityPool +) diff --git a/pkg/solana/currencycreator/types_instruction_type.go b/pkg/solana/currencycreator/types_instruction_type.go new file mode 100644 index 00000000..35856cdd --- /dev/null +++ b/pkg/solana/currencycreator/types_instruction_type.go @@ -0,0 +1,18 @@ +package currencycreator + +type InstructionType uint8 + +const ( + Unknown InstructionType = iota + + InstructionTypeInitializeCurrency + InstructionTypeInitializePool + + InstructionTypeBuyTokens + InstructionTypeSellTokens +) + +func putInstructionType(dst []byte, v InstructionType, offset *int) { + dst[*offset] = uint8(v) + *offset += 1 +} diff --git a/pkg/solana/currencycreator/types_raw_exponential_curve.go b/pkg/solana/currencycreator/types_raw_exponential_curve.go new file mode 100644 index 00000000..6aa7f0c9 --- /dev/null +++ b/pkg/solana/currencycreator/types_raw_exponential_curve.go @@ -0,0 +1,107 @@ +package currencycreator + +import ( + "fmt" + "math/big" +) + +const ( + RawExponentialCurveDecimals = 18 + + defaultCurveAString = "11400230149967394933471" // 11400.230149967394933471 + defaultCurveBString = "877175273521" // 0.000000877175273521 + defaultCurveScaleString = "1000000000000000000" // 10^18 +) + +var ( + DefaultCurveA [24]byte + DefaultCurveB [24]byte + DefaultCurveC [24]byte +) + +const ( + RawExponentialCurveSize = (24 + // a + 24 + // b + 24) // c +) + +func init() { + aInt, _ := new(big.Int).SetString(defaultCurveAString, 10) + bInt, _ := new(big.Int).SetString(defaultCurveBString, 10) + + aInt.FillBytes(DefaultCurveA[:]) + DefaultCurveA = [24]byte(bigToLittleEndian(DefaultCurveA[:])) + + bInt.FillBytes(DefaultCurveB[:]) + DefaultCurveB = [24]byte(bigToLittleEndian(DefaultCurveB[:])) + + DefaultCurveC = DefaultCurveB +} + +type RawExponentialCurve struct { + A [24]byte + B [24]byte + C [24]byte +} + +func DefaultRawExponentialCurve() RawExponentialCurve { + return RawExponentialCurve{ + A: DefaultCurveA, + B: DefaultCurveB, + C: DefaultCurveC, + } +} + +func (obj *RawExponentialCurve) Unmarshal(data []byte) error { + if len(data) < RawExponentialCurveSize { + return ErrInvalidAccountData + } + + copy(obj.A[:], data[0:24]) + copy(obj.B[:], data[24:48]) + copy(obj.C[:], data[48:72]) + + return nil +} + +func (obj *RawExponentialCurve) ToExponentialCurve() *ExponentialCurve { + scale, ok := new(big.Float).SetPrec(defaultExponentialCurvePrec).SetString("1000000000000000000") // 10^18 + if !ok { + panic("Invalid scale string") + } + + aInt := new(big.Int).SetBytes(littleToBigEndian(obj.A[:])) + bInt := new(big.Int).SetBytes(littleToBigEndian(obj.B[:])) + cInt := new(big.Int).SetBytes(littleToBigEndian(obj.C[:])) + + a := new(big.Float).Quo(new(big.Float).SetPrec(defaultExponentialCurvePrec).SetInt(aInt), scale) + b := new(big.Float).Quo(new(big.Float).SetPrec(defaultExponentialCurvePrec).SetInt(bInt), scale) + c := new(big.Float).Quo(new(big.Float).SetPrec(defaultExponentialCurvePrec).SetInt(cInt), scale) + + return &ExponentialCurve{a: a, b: b, c: c} +} + +func (obj *RawExponentialCurve) String() string { + exponentialCurve := obj.ToExponentialCurve() + return fmt.Sprintf( + "RawExponentialCurve{a=%s,b=%s,c=%s}", + exponentialCurve.a.Text('f', RawExponentialCurveDecimals), + exponentialCurve.b.Text('f', RawExponentialCurveDecimals), + exponentialCurve.c.Text('f', RawExponentialCurveDecimals), + ) +} + +func putRawExponentialCurve(dst []byte, v RawExponentialCurve, offset *int) { + copy(dst[*offset:*offset+24], v.A[:]) + copy(dst[*offset+24:*offset+48], v.B[:]) + copy(dst[*offset+48:*offset+72], v.C[:]) + *offset += RawExponentialCurveSize +} +func getRawExponentialCurve(src []byte, dst *RawExponentialCurve, offset *int) error { + err := dst.Unmarshal(src[*offset:]) + if err != nil { + return err + } + *offset += RawExponentialCurveSize + return nil +} diff --git a/pkg/solana/currencycreator/utils.go b/pkg/solana/currencycreator/utils.go new file mode 100644 index 00000000..c643b2bb --- /dev/null +++ b/pkg/solana/currencycreator/utils.go @@ -0,0 +1,99 @@ +package currencycreator + +import ( + "crypto/ed25519" + "encoding/binary" + "strings" + + "github.com/mr-tron/base58" +) + +func getDiscriminator(src []byte, dst *[]byte, offset *int) { + *dst = make([]byte, 8) + copy(*dst, src[*offset:]) + *offset += 8 +} + +func putKey(dst []byte, v ed25519.PublicKey, offset *int) { + copy(dst[*offset:], v) + *offset += ed25519.PublicKeySize +} +func getKey(src []byte, dst *ed25519.PublicKey, offset *int) { + *dst = make([]byte, ed25519.PublicKeySize) + copy(*dst, src[*offset:]) + *offset += ed25519.PublicKeySize +} + +func putFixedString(dst []byte, v string, length int, offset *int) { + copy(dst[*offset:], toFixedString(v, length)) + *offset += length +} +func getFixedString(data []byte, dst *string, length int, offset *int) { + *dst = string(data[*offset : *offset+length]) + *dst = removeFixedStringPadding(*dst) + *offset += length +} + +func putUint8(dst []byte, v uint8, offset *int) { + dst[*offset] = v + *offset += 1 +} +func getUint8(src []byte, dst *uint8, offset *int) { + *dst = src[*offset] + *offset += 1 +} + +func putUint32(dst []byte, v uint32, offset *int) { + binary.LittleEndian.PutUint32(dst[*offset:], v) + *offset += 4 +} +func getUint32(src []byte, dst *uint32, offset *int) { + *dst = binary.LittleEndian.Uint32(src[*offset:]) + *offset += 4 +} + +func putUint64(dst []byte, v uint64, offset *int) { + binary.LittleEndian.PutUint64(dst[*offset:], v) + *offset += 8 +} +func getUint64(src []byte, dst *uint64, offset *int) { + *dst = binary.LittleEndian.Uint64(src[*offset:]) + *offset += 8 +} + +func getInt64(src []byte, dst *int64, offset *int) { + *dst = int64(binary.LittleEndian.Uint64(src[*offset:])) + *offset += 8 +} + +func toFixedString(value string, length int) string { + fixed := make([]byte, length) + copy(fixed, []byte(value)) + return string(fixed) +} +func removeFixedStringPadding(value string) string { + return strings.TrimRight(value, string([]byte{0})) +} + +func mustBase58Decode(value string) []byte { + decoded, err := base58.Decode(value) + if err != nil { + panic(err) + } + return decoded +} + +func littleToBigEndian(b []byte) []byte { + res := make([]byte, len(b)) + for i := range b { + res[i] = b[len(b)-1-i] + } + return res +} +func bigToLittleEndian(b []byte) []byte { + res := make([]byte, len(b)) + for i := range b { + res[i] = b[len(b)-1-i] + } + return res +} diff --git a/pkg/solana/cvm/address.go b/pkg/solana/cvm/address.go index 16bdac8f..b643e584 100644 --- a/pkg/solana/cvm/address.go +++ b/pkg/solana/cvm/address.go @@ -2,7 +2,6 @@ package cvm import ( "crypto/ed25519" - "encoding/binary" "github.com/code-payments/code-server/pkg/solana" ) @@ -87,75 +86,12 @@ func GetStorageAccountAddress(args *GetMemoryAccountAddressArgs) (ed25519.Public ) } -type GetRelayAccountAddressArgs struct { - Name string - Vm ed25519.PublicKey -} - -func GetRelayAccountAddress(args *GetRelayAccountAddressArgs) (ed25519.PublicKey, uint8, error) { - return solana.FindProgramAddressAndBump( - PROGRAM_ID, - CodeVmPrefix, - VmRelayPrefix, - []byte(toFixedString(args.Name, MaxRelayAccountNameSize)), - args.Vm, - ) -} - type GetRelayProofAddressArgs struct { Relay ed25519.PublicKey MerkleRoot Hash Commitment Hash } -func GetRelayProofAddress(args *GetRelayProofAddressArgs) (ed25519.PublicKey, uint8, error) { - return solana.FindProgramAddressAndBump( - PROGRAM_ID, - CodeVmPrefix, - VmRelayProofPrefix, - args.Relay, - args.MerkleRoot[:], - args.Commitment[:], - ) -} - -type GetRelayCommitmentAddressArgs struct { - Relay ed25519.PublicKey - MerkleRoot Hash - Transcript Hash - Destination ed25519.PublicKey - Amount uint64 -} - -func GetRelayCommitmentAddress(args *GetRelayCommitmentAddressArgs) (ed25519.PublicKey, uint8, error) { - amountBytes := make([]byte, 8) - binary.LittleEndian.PutUint64(amountBytes, args.Amount) - - return solana.FindProgramAddressAndBump( - PROGRAM_ID, - CodeVmPrefix, - VmRelayCommitmentPrefix, - args.Relay, - args.MerkleRoot[:], - args.Transcript[:], - args.Destination, - amountBytes, - ) -} - -type GetRelayDestinationAddressArgs struct { - RelayOrProof ed25519.PublicKey -} - -func GetRelayDestinationAddress(args *GetRelayDestinationAddressArgs) (ed25519.PublicKey, uint8, error) { - return solana.FindProgramAddressAndBump( - SPLITTER_PROGRAM_ID, - CodeVmPrefix, - VmRelayVaultPrefix, - args.RelayOrProof, - ) -} - type GetVirtualTimelockAccountAddressArgs struct { Mint ed25519.PublicKey VmAuthority ed25519.PublicKey diff --git a/pkg/solana/cvm/types_account_type.go b/pkg/solana/cvm/types_account_type.go index 6cdd5842..fb65681a 100644 --- a/pkg/solana/cvm/types_account_type.go +++ b/pkg/solana/cvm/types_account_type.go @@ -11,8 +11,3 @@ const ( AccountTypeUnlockState AccountTypeWithdrawReceipt ) - -func putAccountType(dst []byte, v AccountType, offset *int) { - dst[*offset] = uint8(v) - *offset += 1 -} diff --git a/pkg/solana/cvm/utils.go b/pkg/solana/cvm/utils.go index 6681514e..83caa86e 100644 --- a/pkg/solana/cvm/utils.go +++ b/pkg/solana/cvm/utils.go @@ -8,10 +8,6 @@ import ( "github.com/mr-tron/base58" ) -func putDiscriminator(dst []byte, v []byte, offset *int) { - copy(dst[*offset:], v) - *offset += 8 -} func getDiscriminator(src []byte, dst *[]byte, offset *int) { *dst = make([]byte, 8) copy(*dst, src[*offset:]) @@ -28,28 +24,6 @@ func getKey(src []byte, dst *ed25519.PublicKey, offset *int) { *offset += ed25519.PublicKeySize } -func getBool(src []byte, dst *bool, offset *int) { - if src[*offset] == 1 { - *dst = true - } else { - *dst = false - } - *offset += 1 -} - -func putString(dst []byte, v string, offset *int) { - putUint32(dst, uint32(len(v)), offset) - copy(dst[*offset:], v) - *offset += len(v) -} -func getString(data []byte, dst *string, offset *int) { - length := int(binary.LittleEndian.Uint32(data[*offset:])) - *offset += 4 - - *dst = string(data[*offset : *offset+length]) - *offset += length -} - func putFixedString(dst []byte, v string, length int, offset *int) { copy(dst[*offset:], toFixedString(v, length)) *offset += length @@ -60,11 +34,6 @@ func getFixedString(data []byte, dst *string, length int, offset *int) { *offset += length } -func getBytes(src []byte, dst []byte, length int, offset *int) { - copy(dst[:length], src[*offset:*offset+length]) - *offset += length -} - func putUint8(dst []byte, v uint8, offset *int) { dst[*offset] = v *offset += 1