From 3a8f1b59e6d5d1fa079741ef5ac270492aa9c2ce Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Fri, 14 Aug 2026 13:18:25 -0400 Subject: [PATCH] Support 1% fee on reserve buy flow swaps --- go.mod | 2 +- go.sum | 4 +- ocp/currency/fee.go | 83 ++++++++++++++++++ ocp/currency/fee_test.go | 81 ++++++++++++++++++ ocp/rpc/transaction/stateful_swap.go | 16 +++- ocp/rpc/transaction/swap_handler.go | 111 +++++++++++++++++-------- ocp/transaction/compute_budget.go | 15 ++++ ocp/transaction/compute_budget_test.go | 4 + ocp/worker/swap/metrics.go | 11 +-- ocp/worker/swap/util.go | 35 ++++---- ocp/worker/swap/worker.go | 2 +- 11 files changed, 299 insertions(+), 65 deletions(-) create mode 100644 ocp/currency/fee.go create mode 100644 ocp/currency/fee_test.go diff --git a/go.mod b/go.mod index 65ef1ae..cfb0919 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/dynamodb v1.59.0 github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2 github.com/code-payments/code-vm-indexer v1.2.0 - github.com/code-payments/ocp-protobuf-api v1.14.0 + github.com/code-payments/ocp-protobuf-api v1.14.1-0.20260814155826-8088d9d58830 github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.3.3 github.com/golang/protobuf v1.5.4 diff --git a/go.sum b/go.sum index ff32a5f..1a771c1 100644 --- a/go.sum +++ b/go.sum @@ -80,8 +80,8 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/code-payments/code-vm-indexer v1.2.0 h1:rSHpBMiT9BKgmKcXg/VIoi/h0t7jNxGx07Qz59m+6Q0= github.com/code-payments/code-vm-indexer v1.2.0/go.mod h1:vn91YN2qNqb+gGJeZe2+l+TNxVmEEiRHXXnIn2Y40h8= -github.com/code-payments/ocp-protobuf-api v1.14.0 h1:Q0IqyF4q31Mf+wvOILz+BM6P+xOnrPVAm/85/YolKDk= -github.com/code-payments/ocp-protobuf-api v1.14.0/go.mod h1:tw6BooY5a8l6CtSZnKOruyKII0W04n89pcM4BizrgG8= +github.com/code-payments/ocp-protobuf-api v1.14.1-0.20260814155826-8088d9d58830 h1:PVX61XNEbm8iBKAUw6i+NN04qOX9bbenkpRWs0R6CCc= +github.com/code-payments/ocp-protobuf-api v1.14.1-0.20260814155826-8088d9d58830/go.mod h1:tw6BooY5a8l6CtSZnKOruyKII0W04n89pcM4BizrgG8= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= diff --git a/ocp/currency/fee.go b/ocp/currency/fee.go new file mode 100644 index 0000000..b03647c --- /dev/null +++ b/ocp/currency/fee.go @@ -0,0 +1,83 @@ +package currency + +import ( + "math/big" + + "github.com/code-payments/ocp-server/solana/currencycreator" +) + +const ( + // BuyFeeBps is the fee charged when buying a launchpad currency with the + // core mint, which mirrors currencycreator.DefaultSellFeeBps. + BuyFeeBps = currencycreator.DefaultSellFeeBps + + // FeeQuarkTolerance is the maximum difference allowed between a client + // provided fee amount and the server computed value. It absorbs rounding + // differences between client and server. + FeeQuarkTolerance = 1 + + bpsPerUnit = 10_000 + + // sellFeeRemainder is the fraction of a swap's value that remains after + // the launchpad sell fee is taken by the liquidity pool. + sellFeeRemainder = 1 - currencycreator.DefaultSellFeeBps/float64(bpsPerUnit) +) + +// ExpectedBuyFeeQuarks returns the fee in quarks charged on a buy of the +// provided quark amount. +func ExpectedBuyFeeQuarks(swapAmount uint64) uint64 { + fee := new(big.Int).Mul( + new(big.Int).SetUint64(swapAmount), + big.NewInt(BuyFeeBps), + ) + return fee.Div(fee, big.NewInt(bpsPerUnit)).Uint64() +} + +// IsExpectedFeeQuarks returns whether a client provided fee amount matches the +// server computed value within FeeQuarkTolerance. +func IsExpectedFeeQuarks(actual, expected uint64) bool { + if actual > expected { + return actual-expected <= FeeQuarkTolerance + } + return expected-actual <= FeeQuarkTolerance +} + +// DiscountValueForBuyFee scales a value quoted over the full amount funding a +// swap (swap plus fee) down to the portion that was actually swapped. +func DiscountValueForBuyFee(value float64, swapAmount, feeAmount uint64) float64 { + if feeAmount == 0 { + return value + } + + swapAmountBig := new(big.Float).SetPrec(defaultPrecision).SetUint64(swapAmount) + feeAmountBig := new(big.Float).SetPrec(defaultPrecision).SetUint64(feeAmount) + fundedAmountBig := new(big.Float).Add(swapAmountBig, feeAmountBig) + if fundedAmountBig.Sign() == 0 { + return value + } + + discounted, _ := new(big.Float).Mul( + big.NewFloat(value).SetPrec(defaultPrecision), + new(big.Float).Quo(swapAmountBig, fundedAmountBig), + ).Float64() + return discounted +} + +// ApplySellFee returns the value remaining after the launchpad sell fee. +func ApplySellFee(value float64) float64 { + discounted, _ := new(big.Float).Mul( + big.NewFloat(sellFeeRemainder).SetPrec(defaultPrecision), + big.NewFloat(value).SetPrec(defaultPrecision), + ).Float64() + return discounted +} + +// GrossUpSellFee returns the value prior to the launchpad sell fee being taken, +// which is the inverse of ApplySellFee. +func GrossUpSellFee(value float64) float64 { + grossedUp, _ := new(big.Float).Quo( + big.NewFloat(value).SetPrec(defaultPrecision), + big.NewFloat(sellFeeRemainder).SetPrec(defaultPrecision), + ).Float64() + return grossedUp +} diff --git a/ocp/currency/fee_test.go b/ocp/currency/fee_test.go new file mode 100644 index 0000000..a6f2cd6 --- /dev/null +++ b/ocp/currency/fee_test.go @@ -0,0 +1,81 @@ +package currency + +import ( + "math" + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/code-payments/ocp-server/ocp/common" +) + +func TestExpectedBuyFeeQuarks(t *testing.T) { + for _, tc := range []struct { + swapAmount uint64 + expected uint64 + }{ + {swapAmount: 0, expected: 0}, + {swapAmount: 99, expected: 0}, + {swapAmount: 100, expected: 1}, + {swapAmount: 199, expected: 1}, + {swapAmount: 10 * common.CoreMintQuarksPerUnit, expected: common.CoreMintQuarksPerUnit / 10}, + {swapAmount: 12_345_678, expected: 123_456}, + + // The maximum swap amount doesn't overflow + {swapAmount: math.MaxUint64, expected: math.MaxUint64 / 100}, + } { + assert.Equal(t, tc.expected, ExpectedBuyFeeQuarks(tc.swapAmount)) + } +} + +func TestIsExpectedFeeQuarks(t *testing.T) { + assert.True(t, IsExpectedFeeQuarks(100, 100)) + assert.True(t, IsExpectedFeeQuarks(99, 100)) + assert.True(t, IsExpectedFeeQuarks(101, 100)) + + assert.False(t, IsExpectedFeeQuarks(98, 100)) + assert.False(t, IsExpectedFeeQuarks(102, 100)) + + assert.True(t, IsExpectedFeeQuarks(0, 0)) + assert.True(t, IsExpectedFeeQuarks(1, 0)) + assert.False(t, IsExpectedFeeQuarks(2, 0)) +} + +func TestDiscountValueForBuyFee(t *testing.T) { + // Without a fee, the value is untouched + assert.Equal(t, 10.0, DiscountValueForBuyFee(10.0, 10*common.CoreMintQuarksPerUnit, 0)) + + // A $10.10 payment buying $10.00 of a currency is valued at the swap amount + swapAmount := 10 * common.CoreMintQuarksPerUnit + feeAmount := ExpectedBuyFeeQuarks(swapAmount) + fundedValue := float64(swapAmount+feeAmount) / float64(common.CoreMintQuarksPerUnit) + assert.InDelta(t, 10.0, DiscountValueForBuyFee(fundedValue, swapAmount, feeAmount), 1e-9) + + // The discount is proportional regardless of the exchange currency + assert.InDelta(t, 100.0, DiscountValueForBuyFee(101.0, 10_000, 100), 1e-9) +} + +func TestSellFeeRoundTrip(t *testing.T) { + for _, value := range []float64{0.01, 1.0, 12.34, 1_000_000.0} { + assert.InDelta(t, value, GrossUpSellFee(ApplySellFee(value)), 1e-9) + } +} + +// The sell fee was previously applied with hardcoded 0.99 literals. The helpers +// must remain bit-for-bit identical so reported values don't shift. +func TestSellFeeMatchesLegacyLiterals(t *testing.T) { + for _, value := range []float64{0.01, 1.0, 12.34, 99.99, 1_000_000.0} { + legacyApplied, _ := new(big.Float).Mul( + big.NewFloat(0.99).SetPrec(128), + big.NewFloat(value).SetPrec(128), + ).Float64() + assert.Equal(t, legacyApplied, ApplySellFee(value)) + + legacyGrossedUp, _ := new(big.Float).Quo( + big.NewFloat(value).SetPrec(128), + big.NewFloat(0.99).SetPrec(128), + ).Float64() + assert.Equal(t, legacyGrossedUp, GrossUpSellFee(value)) + } +} diff --git a/ocp/rpc/transaction/stateful_swap.go b/ocp/rpc/transaction/stateful_swap.go index 359021d..015127c 100644 --- a/ocp/rpc/transaction/stateful_swap.go +++ b/ocp/rpc/transaction/stateful_swap.go @@ -339,8 +339,21 @@ func (s *transactionServer) handleReserveStatefulSwap( return handleStatefulSwapError(streamer, NewSwapValidationError("owner cannot be swap authority")) } + // A buy fee is only charged when buying a currency with the core mint. + // Legacy clients don't provide a fee amount, and get the flow that + // doesn't collect one. if initiateReserveSwapReq.FeeAmount != 0 { - return handleStatefulSwapError(streamer, NewSwapValidationError("fee amount must be 0")) + if !isBuy || isSell { + return handleStatefulSwapError(streamer, NewSwapValidationError("fee amount must be 0")) + } + + expectedFeeAmount := currency_util.ExpectedBuyFeeQuarks(initiateReserveSwapReq.SwapAmount) + if expectedFeeAmount == 0 { + return handleStatefulSwapError(streamer, NewSwapValidationError("swap amount is too small to charge a fee")) + } + if !currency_util.IsExpectedFeeQuarks(initiateReserveSwapReq.FeeAmount, expectedFeeAmount) { + return handleStatefulSwapError(streamer, NewSwapDeniedErrorf("fee amount must be %d quarks", expectedFeeAmount)) + } } destinationVmConfig, err := common.GetVmConfigForMint(ctx, s.data, toMint) @@ -542,6 +555,7 @@ func (s *transactionServer) handleReserveStatefulSwap( swapAuthority, toMint, initiateReserveSwapReq.SwapAmount, + initiateReserveSwapReq.FeeAmount, selectedNonce, ) } else { diff --git a/ocp/rpc/transaction/swap_handler.go b/ocp/rpc/transaction/swap_handler.go index 9012d3d..1d5df0e 100644 --- a/ocp/rpc/transaction/swap_handler.go +++ b/ocp/rpc/transaction/swap_handler.go @@ -41,7 +41,8 @@ type ReserveBuySwapHandler struct { buyer *common.Account temporaryHolder *common.Account mint *common.Account - amount uint64 + swapAmount uint64 + feeAmount uint64 alts []solana.AddressLookupTable selectedNonce *transaction_util.Nonce @@ -49,8 +50,9 @@ type ReserveBuySwapHandler struct { computeUnitPrice uint64 memoValue string - memoryAccount *common.Account - memoryIndex uint16 + feeDestination *common.Account + memoryAccount *common.Account + memoryIndex uint16 } func NewReserveBuySwapHandler( @@ -58,21 +60,28 @@ func NewReserveBuySwapHandler( buyer *common.Account, temporaryHolder *common.Account, mint *common.Account, - amount uint64, + swapAmount, feeAmount uint64, selectedNonce *transaction_util.Nonce, ) SwapHandler { - return &ReserveBuySwapHandler{ + h := &ReserveBuySwapHandler{ data: data, buyer: buyer, temporaryHolder: temporaryHolder, mint: mint, - amount: amount, + swapAmount: swapAmount, + feeAmount: feeAmount, selectedNonce: selectedNonce, computeUnitPrice: 10_000, memoValue: "buy_v0", } + + if feeAmount > 0 { + h.feeDestination = common.CoreMintFeesAccount + } + + return h } func (h *ReserveBuySwapHandler) GetAlts(ctx context.Context) ([]solana.AddressLookupTable, error) { @@ -80,24 +89,37 @@ func (h *ReserveBuySwapHandler) GetAlts(ctx context.Context) ([]solana.AddressLo if err != nil { return nil, err } + h.alts = []solana.AddressLookupTable{alt} + + // The fee destination is only available in the core mint's ALT + if h.feeDestination != nil { + h.alts = append(h.alts, transaction_util.GetAltForCoreMint()) + } + return h.alts, nil } func (h *ReserveBuySwapHandler) GetServerParameters() *transactionpb.StatefulSwapResponse_ServerParameters { + serverParameters := &transactionpb.StatefulSwapResponse_ServerParameters_ReserveExistingCurrencyServerParameters{ + Payer: common.GetSubsidizer().ToProto(), + Nonce: h.selectedNonce.Account.ToProto(), + Blockhash: &commonpb.Blockhash{Value: h.selectedNonce.Blockhash[:]}, + Alts: transaction_util.ToProtoAlts(h.alts), + ComputeUnitLimit: h.computeUnitLimit, + ComputeUnitPrice: h.computeUnitPrice, + MemoValue: h.memoValue, + MemoryAccount: h.memoryAccount.ToProto(), + MemoryIndex: uint32(h.memoryIndex), + } + + if h.feeDestination != nil { + serverParameters.FeeDestination = h.feeDestination.ToProto() + } + return &transactionpb.StatefulSwapResponse_ServerParameters{ Kind: &transactionpb.StatefulSwapResponse_ServerParameters_ReserveExistingCurrency{ - ReserveExistingCurrency: &transactionpb.StatefulSwapResponse_ServerParameters_ReserveExistingCurrencyServerParameters{ - Payer: common.GetSubsidizer().ToProto(), - Nonce: h.selectedNonce.Account.ToProto(), - Blockhash: &commonpb.Blockhash{Value: h.selectedNonce.Blockhash[:]}, - Alts: transaction_util.ToProtoAlts(h.alts), - ComputeUnitLimit: h.computeUnitLimit, - ComputeUnitPrice: h.computeUnitPrice, - MemoValue: h.memoValue, - MemoryAccount: h.memoryAccount.ToProto(), - MemoryIndex: uint32(h.memoryIndex), - }, + ReserveExistingCurrency: serverParameters, }, } } @@ -158,22 +180,45 @@ func (h *ReserveBuySwapHandler) MakeInstructions(ctx context.Context) ([]solana. if err != nil { return nil, err } - h.computeUnitLimit = transaction_util.ReserveBuySwapComputeUnitLimit(temporaryCoreMintAtaBump) - transferFromSourceVmSwapAtaIxn := vm.NewTransferForSwapInstruction( - &vm.TransferForSwapInstructionAccounts{ - VmAuthority: sourceVmConfig.Authority.PublicKey().ToBytes(), - Vm: sourceVmConfig.Vm.PublicKey().ToBytes(), - Swapper: h.buyer.PublicKey().ToBytes(), - SwapPda: sourceTimelockAccounts.VmSwapAccounts.Pda.PublicKey().ToBytes(), - SwapAta: sourceTimelockAccounts.VmSwapAccounts.Ata.PublicKey().ToBytes(), - Destination: temporaryCoreMintAta.PublicKey().ToBytes(), - }, - &vm.TransferForSwapInstructionArgs{ - Amount: h.amount, - Bump: sourceTimelockAccounts.VmSwapAccounts.PdaBump, - }, - ) + var transferFromSourceVmSwapAtaIxn solana.Instruction + if h.feeDestination != nil { + h.computeUnitLimit = transaction_util.ReserveBuyWithFeeSwapComputeUnitLimit(temporaryCoreMintAtaBump) + + transferFromSourceVmSwapAtaIxn = vm.NewTransferForSwapWithFeeInstruction( + &vm.TransferForSwapWithFeeInstructionAccounts{ + VmAuthority: sourceVmConfig.Authority.PublicKey().ToBytes(), + Vm: sourceVmConfig.Vm.PublicKey().ToBytes(), + Swapper: h.buyer.PublicKey().ToBytes(), + SwapPda: sourceTimelockAccounts.VmSwapAccounts.Pda.PublicKey().ToBytes(), + SwapAta: sourceTimelockAccounts.VmSwapAccounts.Ata.PublicKey().ToBytes(), + SwapDestination: temporaryCoreMintAta.PublicKey().ToBytes(), + FeeDestination: h.feeDestination.PublicKey().ToBytes(), + }, + &vm.TransferForSwapWithFeeInstructionArgs{ + SwapAmount: h.swapAmount, + FeeAmount: h.feeAmount, + Bump: sourceTimelockAccounts.VmSwapAccounts.PdaBump, + }, + ) + } else { + h.computeUnitLimit = transaction_util.ReserveBuySwapComputeUnitLimit(temporaryCoreMintAtaBump) + + transferFromSourceVmSwapAtaIxn = vm.NewTransferForSwapInstruction( + &vm.TransferForSwapInstructionAccounts{ + VmAuthority: sourceVmConfig.Authority.PublicKey().ToBytes(), + Vm: sourceVmConfig.Vm.PublicKey().ToBytes(), + Swapper: h.buyer.PublicKey().ToBytes(), + SwapPda: sourceTimelockAccounts.VmSwapAccounts.Pda.PublicKey().ToBytes(), + SwapAta: sourceTimelockAccounts.VmSwapAccounts.Ata.PublicKey().ToBytes(), + Destination: temporaryCoreMintAta.PublicKey().ToBytes(), + }, + &vm.TransferForSwapInstructionArgs{ + Amount: h.swapAmount, + Bump: sourceTimelockAccounts.VmSwapAccounts.PdaBump, + }, + ) + } buyAndDepositIntoDestinationVmIxn := currencycreator.NewBuyAndDepositIntoVmInstruction( ¤cycreator.BuyAndDepositIntoVmInstructionAccounts{ @@ -192,7 +237,7 @@ func (h *ReserveBuySwapHandler) MakeInstructions(ctx context.Context) ([]solana. VtaOwner: h.buyer.PublicKey().ToBytes(), }, ¤cycreator.BuyAndDepositIntoVmInstructionArgs{ - InAmount: h.amount, + InAmount: h.swapAmount, MinOutAmount: 0, VmMemoryIndex: h.memoryIndex, }, diff --git a/ocp/transaction/compute_budget.go b/ocp/transaction/compute_budget.go index 77ccbd1..0bb00a3 100644 --- a/ocp/transaction/compute_budget.go +++ b/ocp/transaction/compute_budget.go @@ -15,6 +15,14 @@ const ( baseReserveSellSwapComputeUnits = 90_000 + baseAtaCreateComputeUnits baseReserveBuySellSwapComputeUnits = 130_000 + 2*baseAtaCreateComputeUnits + // transfer_for_swap_with_fee makes a second token transfer to the fee + // destination on top of the one made by transfer_for_swap. Measured on + // mainnet: the token program consumes 145 CUs for a transfer, and the CPI + // invoke plus account serialization make up the remainder. The + // transfer_for_swap instruction consumes 5,869 CUs against the 8,395 CUs + // of transfer_for_swap_with_fee. + baseReserveBuyWithFeeSwapComputeUnits = baseReserveBuySwapComputeUnits + 2_600 + // todo: optimize baseExternalDepositComputeUnits = 25_000 baseCloseVmDepositComputeUnits = 10_000 @@ -92,6 +100,13 @@ func ReserveBuySwapComputeUnitLimit(temporaryAtaBump uint8) uint32 { return WithComputeUnitMargin(baseReserveBuySwapComputeUnits + findPdaComputeUnits(temporaryAtaBump)) } +// ReserveBuyWithFeeSwapComputeUnitLimit computes the compute unit limit for a +// reserve buy swap transaction that also collects a buy fee, whose only +// bump-dependent cost is creating the temporary core mint ATA. +func ReserveBuyWithFeeSwapComputeUnitLimit(temporaryAtaBump uint8) uint32 { + return WithComputeUnitMargin(baseReserveBuyWithFeeSwapComputeUnits + findPdaComputeUnits(temporaryAtaBump)) +} + // ReserveSellSwapComputeUnitLimit computes the compute unit limit for a // reserve sell swap transaction, whose only bump-dependent cost is creating // the temporary source currency ATA. diff --git a/ocp/transaction/compute_budget_test.go b/ocp/transaction/compute_budget_test.go index 3c1164f..56a0964 100644 --- a/ocp/transaction/compute_budget_test.go +++ b/ocp/transaction/compute_budget_test.go @@ -31,6 +31,10 @@ func TestReserveSwapComputeUnitLimits(t *testing.T) { // 15% margin assert.EqualValues(t, 116_725, ReserveBuySwapComputeUnitLimit(255)) + // base 80,000 + fee transfer CPI (2,600) + ATA create (20,000) + ATA find + // (1,500) = 104,100, plus 15% margin + assert.EqualValues(t, 119_715, ReserveBuyWithFeeSwapComputeUnitLimit(255)) + // base 90,000 + ATA create (20,000) + ATA find (1,500) = 111,500, plus // 15% margin assert.EqualValues(t, 128_225, ReserveSellSwapComputeUnitLimit(255)) diff --git a/ocp/worker/swap/metrics.go b/ocp/worker/swap/metrics.go index 035b0ee..11da732 100644 --- a/ocp/worker/swap/metrics.go +++ b/ocp/worker/swap/metrics.go @@ -5,7 +5,6 @@ import ( "time" "github.com/code-payments/ocp-server/metrics" - "github.com/code-payments/ocp-server/ocp/common" "github.com/code-payments/ocp-server/ocp/data/swap" ) @@ -51,18 +50,12 @@ func recordSwapCountEvent(ctx context.Context, state swap.State, count uint64) { }) } -func recordSwapFinalizedEvent(ctx context.Context, swapRecord *swap.Record, quarksBought uint64, isMintInit bool) { - quarksSold := swapRecord.SwapAmount - - if isMintInit && swapRecord.FromMint != common.CoreMintAccount.PublicKey().ToBase58() { - quarksSold += swapRecord.FeeAmount - } - +func recordSwapFinalizedEvent(ctx context.Context, swapRecord *swap.Record, quarksBought uint64) { metrics.RecordEvent(ctx, swapFinalizedEventName, map[string]interface{}{ "id": swapRecord.Id, "from_mint": swapRecord.FromMint, "to_mint": swapRecord.ToMint, - "quarks_sold": quarksSold, + "quarks_sold": swapRecord.SwapAmount + swapRecord.FeeAmount, "quarks_bought": quarksBought, }) } diff --git a/ocp/worker/swap/util.go b/ocp/worker/swap/util.go index 1b2ebdd..68af8b9 100644 --- a/ocp/worker/swap/util.go +++ b/ocp/worker/swap/util.go @@ -5,7 +5,6 @@ import ( "crypto/sha256" "database/sql" "fmt" - "math/big" "slices" "time" @@ -609,7 +608,13 @@ func (p *runtime) maybeUpdateBalancesForFinalizedReserveSwap(ctx context.Context nativeAmountWithoutFees = fundingIntentRecord.SendPublicPaymentMetadata.NativeAmount usdMarketValueWithoutFees = fundingIntentRecord.SendPublicPaymentMetadata.UsdMarketValue - if !common.IsCoreMint(fromMint) { + if common.IsCoreMint(fromMint) { + // A buy with the core mint is funded for the swap and the buy fee, + // but only the swap amount bought tokens. The funding payment's own + // value is left as is, since it reflects what the user spent. + nativeAmountWithoutFees = currency_util.DiscountValueForBuyFee(nativeAmountWithoutFees, swapRecord.SwapAmount, swapRecord.FeeAmount) + usdMarketValueWithoutFees = currency_util.DiscountValueForBuyFee(usdMarketValueWithoutFees, swapRecord.SwapAmount, swapRecord.FeeAmount) + } else { coreMintQuarksFromSell := uint64(deltaQuarksIntoOmnibus) if !common.IsCoreMint(toMint) { destinationCurrencyAccounts, err := common.GetLaunchpadCurrencyAccounts(destinationCurrencyMetadataRecord) @@ -633,10 +638,7 @@ func (p *runtime) maybeUpdateBalancesForFinalizedReserveSwap(ctx context.Context return 0, false, err } - usdMarketValueWithoutFees, _ = new(big.Float).Quo( - big.NewFloat(usdMarketValue).SetPrec(128), - big.NewFloat(0.99).SetPrec(128), - ).Float64() + usdMarketValueWithoutFees = currency_util.GrossUpSellFee(usdMarketValue) // A sell settles into the core mint, so its native value is reported in USD. A // cross-currency swap keeps the client's original exchange currency and @@ -673,14 +675,8 @@ func (p *runtime) maybeUpdateBalancesForFinalizedReserveSwap(ctx context.Context nativeAmount := nativeAmountWithoutFees usdMarketValue := usdMarketValueWithoutFees if !common.IsCoreMint(fromMint) { - nativeAmount, _ = new(big.Float).Mul( - big.NewFloat(0.99).SetPrec(128), - big.NewFloat(nativeAmountWithoutFees).SetPrec(128), - ).Float64() - usdMarketValue, _ = new(big.Float).Mul( - big.NewFloat(0.99).SetPrec(128), - big.NewFloat(usdMarketValueWithoutFees).SetPrec(128), - ).Float64() + nativeAmount = currency_util.ApplySellFee(nativeAmountWithoutFees) + usdMarketValue = currency_util.ApplySellFee(usdMarketValueWithoutFees) } exchangeRate := currency_util.CalculateExchangeRate(toMint, uint64(deltaQuarksIntoOmnibus), nativeAmount) @@ -819,6 +815,12 @@ func (p *runtime) notifySwapFinalized(ctx context.Context, swapRecord *swap.Reco currencyCode = fundingIntentRecord.SendPublicPaymentMetadata.ExchangeCurrency nativeAmount = fundingIntentRecord.SendPublicPaymentMetadata.NativeAmount + + if common.IsCoreMint(fromMint) { + // A buy with the core mint is funded for the swap and the buy fee, + // but only the swap amount bought tokens + nativeAmount = currency_util.DiscountValueForBuyFee(nativeAmount, swapRecord.SwapAmount, swapRecord.FeeAmount) + } case swap.FundingSourceExternalWallet, swap.FundingSourceCoinbaseOnramp: if !common.IsCoreMint(fromMint) { return errors.New("unexpected source mint") @@ -836,10 +838,7 @@ func (p *runtime) notifySwapFinalized(ctx context.Context, swapRecord *swap.Reco valueReceived := nativeAmount if !common.IsCoreMint(fromMint) { - valueReceived, _ = new(big.Float).Mul( - big.NewFloat(0.99).SetPrec(128), - big.NewFloat(valueReceived).SetPrec(128), - ).Float64() + valueReceived = currency_util.ApplySellFee(valueReceived) } return p.integration.OnSwapFinalized(ctx, owner, isBuy, targetMint, targetCurrencyMetadataRecord.Name, currencyCode, valueReceived) diff --git a/ocp/worker/swap/worker.go b/ocp/worker/swap/worker.go index 16d772b..bc694df 100644 --- a/ocp/worker/swap/worker.go +++ b/ocp/worker/swap/worker.go @@ -299,7 +299,7 @@ func (p *runtime) handleStateSubmitting(ctx context.Context, record *swap.Record return errors.Wrap(err, "error marking swap as finalized") } - recordSwapFinalizedEvent(ctx, record, quarksBought, isMintInit) + recordSwapFinalizedEvent(ctx, record, quarksBought) go p.notifySwapFinalized(ctx, record, isMintInit)