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.9.0
github.com/code-payments/ocp-protobuf-api v0.10.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.9.0 h1:BWRgd3smO8cz6MUm5DxNqXClp1D2pn2F4VUuna9jzxM=
github.com/code-payments/ocp-protobuf-api v0.9.0/go.mod h1:tw6BooY5a8l6CtSZnKOruyKII0W04n89pcM4BizrgG8=
github.com/code-payments/ocp-protobuf-api v0.10.0 h1:l9Yh3eXdhvgBQS/evg1HYMERXlr7ymASHekAhW/RUmA=
github.com/code-payments/ocp-protobuf-api v0.10.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
4 changes: 4 additions & 0 deletions ocp/common/mint.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ func GetMintQuarksPerUnit(mint *Account) uint64 {
}

func IsSupportedMint(ctx context.Context, data ocp_data.Provider, mintAccount *Account) (bool, error) {
if !IsCoreMint(mintAccount) && !IsCoreMintUsdStableCoin() {
return false, nil
}

_, err := GetVmConfigForMint(ctx, data, mintAccount)
if err == ErrUnsupportedMint {
return false, nil
Expand Down
10 changes: 7 additions & 3 deletions ocp/common/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ type VmConfig struct {
Mint *Account
}

func GetVmConfigForMint(ctx context.Context, data ocp_data.Provider, mint *Account) (*VmConfig, error) {
switch mint.PublicKey().ToBase58() {
func GetVmConfigForMint(ctx context.Context, data ocp_data.Provider, mintAccount *Account) (*VmConfig, error) {
if !IsCoreMint(mintAccount) && !IsCoreMintUsdStableCoin() {
return nil, ErrUnsupportedMint
}

switch mintAccount.PublicKey().ToBase58() {
case CoreMintAccount.PublicKey().ToBase58():
return &VmConfig{
Authority: GetSubsidizer(),
Expand All @@ -50,7 +54,7 @@ func GetVmConfigForMint(ctx context.Context, data ocp_data.Provider, mint *Accou
Authority: jeffyAuthority,
Vm: jeffyVmAccount,
Omnibus: jeffyVmOmnibusAccount,
Mint: mint,
Mint: mintAccount,
}, nil
default:
return nil, ErrUnsupportedMint
Expand Down
22 changes: 22 additions & 0 deletions ocp/currency/exchange_rate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package currency

import (
"math/big"

"github.com/code-payments/ocp-server/ocp/common"
)

// CalculateExchangeRate calculates the exchange rate for a crypto value exchange.
func CalculateExchangeRate(mintAccount *common.Account, nativeAmount float64, quarks uint64) float64 {
quarksPerUnit := common.GetMintQuarksPerUnit(mintAccount)
tokenUnits := new(big.Float).Quo(
big.NewFloat(float64(quarks)).SetPrec(defaultPrecision),
big.NewFloat(float64(quarksPerUnit)).SetPrec(defaultPrecision),
)
exchangeRate := new(big.Float).Quo(
big.NewFloat(float64(nativeAmount)).SetPrec(defaultPrecision),
tokenUnits,
)
res, _ := exchangeRate.Float64()
return res
}
221 changes: 219 additions & 2 deletions ocp/currency/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,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"
Expand All @@ -18,10 +21,224 @@ import (

const (
defaultPrecision = 128

// maxVerifiedExchangeRateAge is the maximum age of a verified exchange rate
maxVerifiedExchangeRateAge = 15 * time.Minute

// maxVerifiedReserveStateAge is the maximum age of a verified reserve state
maxVerifiedReserveStateAge = 2 * time.Minute
)

// ValidateClientExchangeData validates proto exchange data provided by a client
func ValidateClientExchangeData(ctx context.Context, log *zap.Logger, data ocp_data.Provider, proto *transactionpb.ExchangeData) (bool, string, error) {
// ValidateVerifiedExchangeRate validates a server-signed exchange rate provided by a client.
// It verifies that the signature is valid and that the timestamp is not too far in the past.
func ValidateVerifiedExchangeRate(proto *currencypb.VerifiedCoreMintFiatExchangeRate) (bool, string) {
if proto == nil || proto.ExchangeRate == nil || proto.Signature == nil {
return false, "missing required fields"
}

// Verify the timestamp is not too old
if proto.ExchangeRate.Timestamp == nil {
return false, "missing timestamp"
}

age := time.Since(proto.ExchangeRate.Timestamp.AsTime())
if age > maxVerifiedExchangeRateAge {
return false, "exchange rate is stale"
}

// Verify the signature
messageBytes, err := auth.ForceConsistentMarshal(proto.ExchangeRate)
if err != nil {
return false, "failed to marshal exchange rate"
}

if !ed25519.Verify(common.GetSubsidizer().PublicKey().ToBytes(), messageBytes, proto.Signature.Value) {
return false, "invalid signature"
}

return true, ""
}

// ValidateVerifiedReserveState validates a server-signed reserve state provided by a client.
// It verifies that the signature is valid and that the timestamp is not too far in the past.
func ValidateVerifiedReserveState(proto *currencypb.VerifiedLaunchpadCurrencyReserveState) (bool, string) {
if proto == nil || proto.ReserveState == nil || proto.Signature == nil {
return false, "missing required fields"
}

// Verify the timestamp is not too old
if proto.ReserveState.Timestamp == nil {
return false, "missing timestamp"
}

age := time.Since(proto.ReserveState.Timestamp.AsTime())
if age > maxVerifiedReserveStateAge {
return false, "reserve state is stale"
}

// Verify the signature
messageBytes, err := auth.ForceConsistentMarshal(proto.ReserveState)
if err != nil {
return false, "failed to marshal reserve state"
}

if !ed25519.Verify(common.GetSubsidizer().PublicKey().ToBytes(), messageBytes, proto.Signature.Value) {
return false, "invalid signature"
}

return true, ""
}

// ValidateClientExchangeData validates client-provided exchange data with
// provable exchange rates and reserve states that were provided by server.
func ValidateClientExchangeData(proto *transactionpb.VerifiedExchangeData) (bool, string) {
if proto == nil {
return false, "verified exchange data is required"
}

// Validate mint is present
if proto.Mint == nil {
return false, "mint is required"
}

mintAccount, err := common.NewAccountFromProto(proto.Mint)
if err != nil {
return false, "invalid mint"
}

// Validate quarks
if proto.Quarks == 0 {
return false, "quarks must be greater than zero"
}

// Validate native amount
if proto.NativeAmount <= 0 {
return false, "native amount must be greater than zero"
}

// Exchange rate is always required
if proto.CoreMintFiatExchangeRate == nil {
return false, "core mint fiat exchange rate is required"
}

// Validate the exchange rate
if valid, msg := ValidateVerifiedExchangeRate(proto.CoreMintFiatExchangeRate); !valid {
return false, "exchange rate validation failed: " + msg
}

// For core mint, only exchange rate validation is needed along with value checks
if common.IsCoreMint(mintAccount) {
if proto.LaunchpadCurrencyReserveState != nil {
return false, "launchpad currency reserve state is not required for core mint"
}

return validateCoreMintVerifiedExchangeData(proto)
}

// For launchpad currencies, reserve state is required
if proto.LaunchpadCurrencyReserveState == nil {
return false, "reserve state is required for launchpad currencies"
}

// Validate the reserve state
if valid, msg := ValidateVerifiedReserveState(proto.LaunchpadCurrencyReserveState); !valid {
return false, "reserve state validation failed: " + msg
}

// Verify the reserve state mint matches the expected mint
reserveMint, err := common.NewAccountFromProto(proto.LaunchpadCurrencyReserveState.ReserveState.Mint)
if err != nil {
return false, "invalid mint in reserve state"
}

if reserveMint.PublicKey().ToBase58() != mintAccount.PublicKey().ToBase58() {
return false, "reserve state mint does not match expected mint"
}

return validateLaunchpadCurrencyVerifiedExchangeData(proto)
}

func validateCoreMintVerifiedExchangeData(proto *transactionpb.VerifiedExchangeData) (bool, string) {
coreMintQuarksPerUnit := common.GetMintQuarksPerUnit(common.CoreMintAccount)

clientRate := big.NewFloat(proto.CoreMintFiatExchangeRate.ExchangeRate.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.CoreMintFiatExchangeRate.ExchangeRate.CurrencyCode))
one := big.NewFloat(1.0).SetPrec(defaultPrecision)
minTransferValue := new(big.Float).Quo(one, big.NewFloat(math.Pow10(currencyDecimals)))

nativeAmountErrorThreshold := new(big.Float).Quo(minTransferValue, big.NewFloat(2.0))
nativeAmountLowerBound := new(big.Float).Sub(clientNativeAmount, nativeAmountErrorThreshold)
nativeAmountUpperBound := new(big.Float).Add(clientNativeAmount, nativeAmountErrorThreshold)
if nativeAmountLowerBound.Cmp(nativeAmountErrorThreshold) < 0 {
nativeAmountLowerBound = 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)))

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

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

return true, ""
}

func validateLaunchpadCurrencyVerifiedExchangeData(proto *transactionpb.VerifiedExchangeData) (bool, string) {
if !common.IsCoreMintUsdStableCoin() {
return false, "launchpad currencies only support a stable coin core mint"
}

coreMintQuarksPerUnit := common.GetMintQuarksPerUnit(common.CoreMintAccount)

clientFiatExchangeRate := big.NewFloat(proto.CoreMintFiatExchangeRate.ExchangeRate.ExchangeRate).SetPrec(defaultPrecision)
clientNativeAmount := big.NewFloat(proto.NativeAmount).SetPrec(defaultPrecision)

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

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

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

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

// Validate that the native amount within half of the minimum transfer value
coreMintSellValueInUnits := new(big.Float).Quo(
big.NewFloat(float64(coreMintSellValueInQuarks)).SetPrec(defaultPrecision),
big.NewFloat(float64(coreMintQuarksPerUnit)).SetPrec(defaultPrecision),
)
potentialNativeAmount := new(big.Float).Mul(clientFiatExchangeRate, coreMintSellValueInUnits)

if potentialNativeAmount.Cmp(nativeAmountLowerBound) < 0 || potentialNativeAmount.Cmp(nativeAmountUpperBound) > 0 {
return false, "native amount does not match expected sell value"
}

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
Expand Down
14 changes: 8 additions & 6 deletions ocp/rpc/transaction/intent.go
Original file line number Diff line number Diff line change
Expand Up @@ -823,12 +823,14 @@ func (s *transactionServer) GetIntentMetadata(ctx context.Context, req *transact
SendPublicPayment: &transactionpb.SendPublicPaymentMetadata{
Source: sourceAccount.ToProto(),
Destination: destinationAccount.ToProto(),
ExchangeData: &transactionpb.ExchangeData{
Currency: strings.ToLower(string(intentRecord.SendPublicPaymentMetadata.ExchangeCurrency)),
ExchangeRate: intentRecord.SendPublicPaymentMetadata.ExchangeRate,
NativeAmount: intentRecord.SendPublicPaymentMetadata.NativeAmount,
Quarks: intentRecord.SendPublicPaymentMetadata.Quantity,
Mint: mintAccount.ToProto(),
ExchangeData: &transactionpb.SendPublicPaymentMetadata_ServerExchangeData{
ServerExchangeData: &transactionpb.ExchangeData{
Currency: strings.ToLower(string(intentRecord.SendPublicPaymentMetadata.ExchangeCurrency)),
ExchangeRate: intentRecord.SendPublicPaymentMetadata.ExchangeRate,
NativeAmount: intentRecord.SendPublicPaymentMetadata.NativeAmount,
Quarks: intentRecord.SendPublicPaymentMetadata.Quantity,
Mint: mintAccount.ToProto(),
},
},
IsRemoteSend: intentRecord.SendPublicPaymentMetadata.IsRemoteSend,
IsWithdrawal: intentRecord.SendPublicPaymentMetadata.IsWithdrawal,
Expand Down
Loading
Loading