Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
filippo.io/edwards25519 v1.1.0
github.com/aws/aws-sdk-go-v2 v0.17.0
github.com/code-payments/code-vm-indexer v1.2.0
github.com/code-payments/ocp-protobuf-api v0.11.0
github.com/code-payments/ocp-protobuf-api v0.12.0
github.com/emirpasic/gods v1.12.0
github.com/envoyproxy/protoc-gen-validate v1.2.1
github.com/golang/protobuf v1.5.4
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,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 v0.11.0 h1:Zq0H3tg+bjTuXF9T6kRXEX8sGXu1NQ0szvUD1MdVs4Q=
github.com/code-payments/ocp-protobuf-api v0.11.0/go.mod h1:tw6BooY5a8l6CtSZnKOruyKII0W04n89pcM4BizrgG8=
github.com/code-payments/ocp-protobuf-api v0.12.0 h1:X5lo7ABDYxsiC/D3viFuNJUhTWKISZBg6MzAy2PP10Y=
github.com/code-payments/ocp-protobuf-api v0.12.0/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=
Expand Down
26 changes: 0 additions & 26 deletions ocp/currency/time.go

This file was deleted.

217 changes: 0 additions & 217 deletions ocp/currency/validation.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
package currency

import (
"context"
"crypto/ed25519"
"math"
"math/big"
"time"

currencypb "github.com/code-payments/ocp-protobuf-api/generated/go/currency/v1"
transactionpb "github.com/code-payments/ocp-protobuf-api/generated/go/transaction/v1"
"go.uber.org/zap"

currency_lib "github.com/code-payments/ocp-server/currency"
"github.com/code-payments/ocp-server/ocp/auth"
"github.com/code-payments/ocp-server/ocp/common"
ocp_data "github.com/code-payments/ocp-server/ocp/data"
"github.com/code-payments/ocp-server/ocp/data/currency"
"github.com/code-payments/ocp-server/solana/currencycreator"
)

Expand Down Expand Up @@ -236,216 +232,3 @@ func validateLaunchpadCurrencyVerifiedExchangeData(proto *transactionpb.Verified

return true, ""
}

// ValidateLegacyClientExchangeData validates legacy proto exchange data provided by a client
func ValidateLegacyClientExchangeData(ctx context.Context, log *zap.Logger, data ocp_data.Provider, proto *transactionpb.ExchangeData) (bool, string, error) {
mint, err := common.GetBackwardsCompatMint(proto.Mint)
if err != nil {
return false, "", err
}

if common.IsCoreMint(mint) {
return validateCoreMintClientExchangeData(ctx, log, data, proto)
}
return validateCurrencyLaunchpadClientExchangeData(ctx, log, data, proto)
}

func validateCoreMintClientExchangeData(ctx context.Context, log *zap.Logger, data ocp_data.Provider, proto *transactionpb.ExchangeData) (bool, string, error) {
latestExchangeRateTime := GetLatestExchangeRateTime()

coreMintQuarksPerUnit := common.GetMintQuarksPerUnit(common.CoreMintAccount)

clientRate := big.NewFloat(proto.ExchangeRate).SetPrec(defaultPrecision)
clientNativeAmount := big.NewFloat(proto.NativeAmount).SetPrec(defaultPrecision)
clientQuarks := big.NewFloat(float64(proto.Quarks)).SetPrec(defaultPrecision)

currencyDecimals := currency_lib.GetDecimals(currency_lib.Code(proto.Currency))
one := big.NewFloat(1.0).SetPrec(defaultPrecision)
minTransferValue := new(big.Float).Quo(one, big.NewFloat(math.Pow10(currencyDecimals)))

rateErrorThreshold := big.NewFloat(0.001).SetPrec(defaultPrecision)
nativeAmountErrorThreshold := new(big.Float).Quo(minTransferValue, big.NewFloat(2.0))

nativeAmountLowerBound := new(big.Float).Sub(clientNativeAmount, nativeAmountErrorThreshold)
if nativeAmountLowerBound.Cmp(nativeAmountErrorThreshold) < 0 {
nativeAmountLowerBound = nativeAmountErrorThreshold
}
nativeAmountUpperBound := new(big.Float).Add(clientNativeAmount, nativeAmountErrorThreshold)
quarksLowerBound := new(big.Float).Mul(new(big.Float).Quo(nativeAmountLowerBound, clientRate), big.NewFloat(float64(coreMintQuarksPerUnit)))
quarksUpperBound := new(big.Float).Mul(new(big.Float).Quo(nativeAmountUpperBound, clientRate), big.NewFloat(float64(coreMintQuarksPerUnit)))

log = log.With(
zap.String("currency", proto.Currency),
zap.String("client_native_amount", clientNativeAmount.Text('f', 10)),
zap.String("client_exchange_rate", clientRate.Text('f', 10)),
zap.Uint64("client_quarks", proto.Quarks),
zap.String("min_transfer_value", minTransferValue.Text('f', 10)),
zap.String("native_amount_lower_bound", nativeAmountLowerBound.Text('f', 10)),
zap.String("native_amount_upper_bound", nativeAmountUpperBound.Text('f', 10)),
zap.String("quarks_lower_bound", quarksLowerBound.Text('f', 10)),
zap.String("quarks_upper_bound", quarksUpperBound.Text('f', 10)),
)

if clientNativeAmount.Cmp(nativeAmountErrorThreshold) < 0 {
log.Info("native amount is less than minimum transfer value error threshold")
return false, "native amount is less than minimum transfer value error threshold", nil
}

// Find an exchange rate that the client could have fetched from a RPC call
// within a reasonable time in the past
var isClientRateValid bool
for i := range 2 {
exchangeRateTime := latestExchangeRateTime.Add(time.Duration(-i) * timePerExchangeRateUpdate)

exchangeRateRecord, err := data.GetExchangeRate(ctx, currency_lib.Code(proto.Currency), exchangeRateTime)
if err == currency.ErrNotFound {
continue
} else if err != nil {
return false, "", err
}
actualRate := big.NewFloat(exchangeRateRecord.Rate)

percentDiff := new(big.Float).Quo(new(big.Float).Abs(new(big.Float).Sub(clientRate, actualRate)), actualRate)
if percentDiff.Cmp(rateErrorThreshold) < 0 {
isClientRateValid = true
break
}

log.With(zap.String("found_rate", actualRate.Text('f', 10))).Info("exchange rate doesn't match")
}

if !isClientRateValid {
log.Info("fiat exchange rate is stale or invalid")
return false, "fiat exchange rate is stale or invalid", nil
}

// Validate that the native amount within half of the minimum transfer value
if clientQuarks.Cmp(quarksLowerBound) < 0 || clientQuarks.Cmp(quarksUpperBound) > 0 {
log.Info("native amount is outside error threshold")
return false, "payment native amount and quark value mismatch", nil
}

return true, "", nil
}

func validateCurrencyLaunchpadClientExchangeData(ctx context.Context, log *zap.Logger, data ocp_data.Provider, proto *transactionpb.ExchangeData) (bool, string, error) {
mintAccount, err := common.GetBackwardsCompatMint(proto.Mint)
if err != nil {
return false, "", err
}

coreMintQuarksPerUnit := common.GetMintQuarksPerUnit(common.CoreMintAccount)
otherMintQuarksPerUnit := common.GetMintQuarksPerUnit(mintAccount)

clientQuarks := big.NewFloat(float64(proto.Quarks)).SetPrec(defaultPrecision)
clientTokenUnits := new(big.Float).Quo(
clientQuarks,
big.NewFloat(float64(otherMintQuarksPerUnit)).SetPrec(defaultPrecision),
)
clientRate := big.NewFloat(proto.ExchangeRate).SetPrec(defaultPrecision)
clientNativeAmount := big.NewFloat(proto.NativeAmount).SetPrec(defaultPrecision)

currencyDecimals := currency_lib.GetDecimals(currency_lib.Code(proto.Currency))
one := big.NewFloat(1.0).SetPrec(defaultPrecision)
minTransferValue := new(big.Float).Quo(one, big.NewFloat(math.Pow10(currencyDecimals)))

rateErrorThreshold := big.NewFloat(0.001).SetPrec(defaultPrecision)
nativeAmountErrorThreshold := new(big.Float).Quo(minTransferValue, big.NewFloat(2.0))

nativeAmountLowerBound := new(big.Float).Sub(clientNativeAmount, nativeAmountErrorThreshold)
if nativeAmountLowerBound.Cmp(nativeAmountErrorThreshold) < 0 {
nativeAmountLowerBound = nativeAmountErrorThreshold
}
nativeAmountUpperBound := new(big.Float).Add(clientNativeAmount, nativeAmountErrorThreshold)

log = log.With(
zap.String("currency", proto.Currency),
zap.String("client_native_amount", clientNativeAmount.Text('f', 10)),
zap.String("client_exchange_rate", clientRate.Text('f', 10)),
zap.String("client_token_units", clientTokenUnits.Text('f', 10)),
zap.Uint64("client_quarks", proto.Quarks),
zap.String("min_transfer_value", minTransferValue.Text('f', 10)),
zap.String("native_amount_lower_bound", nativeAmountLowerBound.Text('f', 10)),
zap.String("native_amount_upper_bound", nativeAmountUpperBound.Text('f', 10)),
zap.String("mint", mintAccount.PublicKey().ToBase58()),
)

if clientNativeAmount.Cmp(nativeAmountErrorThreshold) < 0 {
log.Info("native amount is less than minimum transfer value error threshold")
return false, "native amount is less than minimum transfer value error threshold", nil
}

latestExchangeRateTime := GetLatestExchangeRateTime()
for i := range 2 {
exchangeRateTime := latestExchangeRateTime.Add(time.Duration(-i) * timePerExchangeRateUpdate)

reserveRecord, err := data.GetCurrencyReserveAtTime(ctx, mintAccount.PublicKey().ToBase58(), exchangeRateTime)
if err == currency.ErrNotFound {
continue
} else if err != nil {
return false, "", err
}

usdExchangeRateRecord, err := data.GetExchangeRate(ctx, currency_lib.USD, exchangeRateTime)
if err == currency.ErrNotFound {
continue
} else if err != nil {
return false, "", err
}
usdRate := big.NewFloat(usdExchangeRateRecord.Rate).SetPrec(defaultPrecision)

var otherExchangeRateRecord *currency.ExchangeRateRecord
if proto.Currency == string(currency_lib.USD) {
otherExchangeRateRecord = usdExchangeRateRecord
} else {
otherExchangeRateRecord, err = data.GetExchangeRate(ctx, currency_lib.Code(proto.Currency), exchangeRateTime)
if err == currency.ErrNotFound {
continue
} else if err != nil {
return false, "", err
}
}
otherRate := big.NewFloat(otherExchangeRateRecord.Rate).SetPrec(defaultPrecision)

// How much core mint would be received for a sell against the currency creator program?
coreMintSellValueInQuarks, _ := currencycreator.EstimateSell(&currencycreator.EstimateSellArgs{
CurrentSupplyInQuarks: reserveRecord.SupplyFromBonding,
SellAmountInQuarks: proto.Quarks,
ValueMintDecimals: uint8(common.CoreMintDecimals),
SellFeeBps: 0,
})

// Given the sell value, does it align with the native amount in the target currency
// within half a minimum transfer unit?
coreMintSellValueInUnits := new(big.Float).Quo(
big.NewFloat(float64(coreMintSellValueInQuarks)).SetPrec(defaultPrecision),
big.NewFloat(float64(coreMintQuarksPerUnit)).SetPrec(defaultPrecision),
)
potentialNativeAmount := new(big.Float).Mul(new(big.Float).Quo(otherRate, usdRate), coreMintSellValueInUnits)

log := log.With(
zap.String("core_mint_sell_value", coreMintSellValueInUnits.Text('f', 10)),
zap.String("potential_native_amount", potentialNativeAmount.Text('f', 10)),
zap.String("found_usd_rate", usdRate.Text('f', 10)),
zap.String("found_other_rate", otherRate.Text('f', 10)),
)

if potentialNativeAmount.Cmp(nativeAmountLowerBound) < 0 || potentialNativeAmount.Cmp(nativeAmountUpperBound) > 0 {
log.Info("native amount is outside error threshold")
continue
}

// For the valid native amount, is the exchange rate calculated correctly?
expectedRate := new(big.Float).Quo(clientNativeAmount, clientTokenUnits)
percentDiff := new(big.Float).Quo(new(big.Float).Abs(new(big.Float).Sub(clientRate, expectedRate)), expectedRate)
log = log.With(zap.String("potential_exchange_rate", expectedRate.Text('f', 10)))
if percentDiff.Cmp(rateErrorThreshold) > 0 {
log.Info("exchange rate is outside error threshold")
continue
}

return true, "", nil
}

return false, "fiat exchange data is stale or invalid", nil
}
55 changes: 9 additions & 46 deletions ocp/rpc/currency/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"time"

"github.com/google/uuid"
"github.com/pkg/errors"
"go.uber.org/zap"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
Expand All @@ -23,7 +22,6 @@ import (
"github.com/code-payments/ocp-server/grpc/client"
"github.com/code-payments/ocp-server/ocp/common"
"github.com/code-payments/ocp-server/ocp/config"
currency_util "github.com/code-payments/ocp-server/ocp/currency"
ocp_data "github.com/code-payments/ocp-server/ocp/data"
"github.com/code-payments/ocp-server/ocp/data/currency"
"github.com/code-payments/ocp-server/protoutil"
Expand Down Expand Up @@ -69,47 +67,6 @@ func NewCurrencyServer(
}
}

func (s *currencyServer) GetAllRates(ctx context.Context, req *currencypb.GetAllRatesRequest) (resp *currencypb.GetAllRatesResponse, err error) {
log := s.log.With(zap.String("method", "GetAllRates"))
log = client.InjectLoggingMetadata(ctx, log)

var record *currency.MultiRateRecord
if req.Timestamp != nil && req.Timestamp.AsTime().Before(time.Now().Add(-15*time.Minute)) {
record, err = s.loadExchangeRatesForTime(ctx, req.Timestamp.AsTime())
} else if req.Timestamp == nil || req.Timestamp.AsTime().Sub(time.Now()) < time.Hour {
record, err = s.loadExchangeRatesLatest(ctx)
} else {
return nil, status.Error(codes.InvalidArgument, "timestamp too far in the future")
}

if err != nil {
log.With(zap.Error(err)).Warn("failed to load latest rate")
return nil, status.Error(codes.Internal, err.Error())
}

protoTime := timestamppb.New(record.Time)
return &currencypb.GetAllRatesResponse{
AsOf: protoTime,
Rates: record.Rates,
}, nil
}

func (s *currencyServer) loadExchangeRatesForTime(ctx context.Context, t time.Time) (*currency.MultiRateRecord, error) {
record, err := s.data.GetAllExchangeRates(ctx, t)
if err != nil {
return nil, errors.Wrap(err, "failed to get price record by date")
}
return record, nil
}

func (s *currencyServer) loadExchangeRatesLatest(ctx context.Context) (*currency.MultiRateRecord, error) {
latest, err := s.data.GetAllExchangeRates(ctx, currency_util.GetLatestExchangeRateTime())
if err != nil {
return nil, errors.Wrap(err, "failed to get latest price record")
}
return latest, nil
}

func (s *currencyServer) GetMints(ctx context.Context, req *currencypb.GetMintsRequest) (*currencypb.GetMintsResponse, error) {
log := s.log.With(zap.String("method", "GetMints"))
log = client.InjectLoggingMetadata(ctx, log)
Expand Down Expand Up @@ -160,9 +117,15 @@ func (s *currencyServer) GetMints(ctx context.Context, req *currencypb.GetMintsR
return nil, status.Error(codes.Internal, "")
}

reserveRecord, err := s.data.GetCurrencyReserveAtTime(ctx, mintAccount.PublicKey().ToBase58(), currency_util.GetLatestExchangeRateTime())
err = s.liveMintStateWorker.waitForData(ctx)
if err != nil {
log.With(zap.Error(err)).Warn("failed to wait for live mint data")
return nil, status.Error(codes.Internal, "")
}

liveReserveState, err := s.liveMintStateWorker.getReserveState(mintAccount)
if err != nil {
log.With(zap.Error(err)).Warn("failed to load currency reserve record")
log.With(zap.Error(err)).Warn("failed to get live mint reserve state")
return nil, status.Error(codes.Internal, "")
}

Expand Down Expand Up @@ -223,7 +186,7 @@ func (s *currencyServer) GetMints(ctx context.Context, req *currencypb.GetMintsR
Authority: currencyAuthorityAccount.ToProto(),
MintVault: mintVaultAccount.ToProto(),
CoreMintVault: coreMintVaultAccount.ToProto(),
SupplyFromBonding: reserveRecord.SupplyFromBonding,
SupplyFromBonding: liveReserveState.SupplyFromBonding,
SellFeeBps: uint32(metadataRecord.SellFeeBps),
},
CreatedAt: timestamppb.New(metadataRecord.CreatedAt),
Expand Down
Loading
Loading