Skip to content

Commit c32c20e

Browse files
committed
Improve CanWithdrawToAccount by detecting if the public key is on the curve
1 parent d68b34a commit c32c20e

File tree

5 files changed

+71
-25
lines changed

5 files changed

+71
-25
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/code-payments/code-server
33
go 1.23.0
44

55
require (
6+
filippo.io/edwards25519 v1.1.0
67
github.com/aws/aws-sdk-go-v2 v0.17.0
78
github.com/bits-and-blooms/bloom/v3 v3.1.0
89
github.com/code-payments/code-protobuf-api v1.19.1-0.20250603030803-cbe2bfca5052

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
3737
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
3838
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
3939
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
40+
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
41+
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
4042
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
4143
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
4244
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=

pkg/code/common/account.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"crypto/ed25519"
77
"fmt"
88

9+
"filippo.io/edwards25519"
910
"github.com/pkg/errors"
1011

1112
commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1"
@@ -325,6 +326,10 @@ func (a *Account) IsManagedByCode(ctx context.Context, data code_data.Provider)
325326
return IsManagedByCode(ctx, timelockRecord), nil
326327
}
327328

329+
func (a *Account) IsOnCurve() bool {
330+
return isOnCurve(a.publicKey.ToBytes())
331+
}
332+
328333
func (a *Account) Validate() error {
329334
if a == nil {
330335
return errors.New("account is nil")
@@ -455,3 +460,13 @@ func ValidateExternalTokenAccount(ctx context.Context, data code_data.Provider,
455460
return false, "", err
456461
}
457462
}
463+
464+
func isOnCurve(pubKey ed25519.PublicKey) bool {
465+
if len(pubKey) != ed25519.PublicKeySize {
466+
return false
467+
}
468+
469+
// Try to parse the public key as a point
470+
_, err := new(edwards25519.Point).SetBytes(pubKey)
471+
return err == nil
472+
}

pkg/code/common/account_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,21 @@ func TestGetVmDepositAccounts(t *testing.T) {
189189
assert.EqualValues(t, mintAccount.PublicKey().ToBytes(), actual.Mint.PublicKey().ToBytes())
190190
}
191191

192+
func TestIsOnCurve(t *testing.T) {
193+
for _, tc := range []struct {
194+
expected bool
195+
publicKey string
196+
}{
197+
{true, "codeHy87wGD5oMRLG75qKqsSi1vWE3oxNyYmXo5F9YR"}, // Example owner
198+
{false, "Cgx4Ff1mmdqRnUbMQ8SbM23hDEbaNaGuhxUHF4aDN8if"}, // Example ATA
199+
{false, "ChpRZqD1KKdbVsFkRGu85rqMa85MHKhWHuGwJXwAiRCN"}, // Example Timelock vault
200+
} {
201+
account, err := NewAccountFromPublicKeyString(tc.publicKey)
202+
require.NoError(t, err)
203+
assert.Equal(t, tc.expected, account.IsOnCurve())
204+
}
205+
}
206+
192207
func TestIsAccountManagedByCode_TimelockState(t *testing.T) {
193208
ctx := context.Background()
194209
data := code_data.NewTestDataProvider()

pkg/code/server/transaction/intent.go

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -803,47 +803,60 @@ func (s *transactionServer) CanWithdrawToAccount(ctx context.Context, req *trans
803803
}
804804
log = log.WithField("account", accountToCheck.PublicKey().ToBase58())
805805

806+
isOnCurve := accountToCheck.IsOnCurve()
807+
806808
//
807-
// Part 1: Is this a timelock vault? If so, only allow primary accounts.
809+
// Part 1: Is this a Timelock vault? If so, only allow primary accounts.
808810
//
809811

810-
accountInfoRecord, err := s.data.GetAccountInfoByTokenAddress(ctx, accountToCheck.PublicKey().ToBase58())
811-
switch err {
812-
case nil:
813-
return &transactionpb.CanWithdrawToAccountResponse{
814-
IsValidPaymentDestination: accountInfoRecord.AccountType == commonpb.AccountType_PRIMARY,
815-
AccountType: transactionpb.CanWithdrawToAccountResponse_TokenAccount,
816-
}, nil
817-
case account.ErrAccountInfoNotFound:
818-
// Nothing to do
819-
default:
820-
log.WithError(err).Warn("failure checking account info db")
821-
return nil, status.Error(codes.Internal, "")
812+
if !isOnCurve {
813+
accountInfoRecord, err := s.data.GetAccountInfoByTokenAddress(ctx, accountToCheck.PublicKey().ToBase58())
814+
switch err {
815+
case nil:
816+
return &transactionpb.CanWithdrawToAccountResponse{
817+
IsValidPaymentDestination: accountInfoRecord.AccountType == commonpb.AccountType_PRIMARY,
818+
AccountType: transactionpb.CanWithdrawToAccountResponse_TokenAccount,
819+
}, nil
820+
case account.ErrAccountInfoNotFound:
821+
// Nothing to do
822+
default:
823+
log.WithError(err).Warn("failure checking account info db")
824+
return nil, status.Error(codes.Internal, "")
825+
}
822826
}
823827

824828
//
825829
// Part 2: Is this an opened core mint token account? If so, allow it.
826830
//
827831

828-
_, err = s.data.GetBlockchainTokenAccountInfo(ctx, accountToCheck.PublicKey().ToBase58(), solana.CommitmentFinalized)
829-
switch err {
830-
case nil:
831-
return &transactionpb.CanWithdrawToAccountResponse{
832-
IsValidPaymentDestination: true,
833-
AccountType: transactionpb.CanWithdrawToAccountResponse_TokenAccount,
834-
}, nil
835-
case token.ErrAccountNotFound, solana.ErrNoAccountInfo, token.ErrInvalidTokenAccount:
836-
// Nothing to do
837-
default:
838-
log.WithError(err).Warn("failure checking against blockchain as a token account")
839-
return nil, status.Error(codes.Internal, "")
832+
if !isOnCurve {
833+
_, err = s.data.GetBlockchainTokenAccountInfo(ctx, accountToCheck.PublicKey().ToBase58(), solana.CommitmentFinalized)
834+
switch err {
835+
case nil:
836+
return &transactionpb.CanWithdrawToAccountResponse{
837+
IsValidPaymentDestination: true,
838+
AccountType: transactionpb.CanWithdrawToAccountResponse_TokenAccount,
839+
}, nil
840+
case token.ErrAccountNotFound, solana.ErrNoAccountInfo, token.ErrInvalidTokenAccount:
841+
// Nothing to do
842+
default:
843+
log.WithError(err).Warn("failure checking against blockchain as a token account")
844+
return nil, status.Error(codes.Internal, "")
845+
}
840846
}
841847

842848
//
843849
// Part 3: Is this an owner account with an opened Core Mint ATA? If so, allow it.
844850
// If not, indicate to the client to pay a fee for a create-on-send withdrawal.
845851
//
846852

853+
if !isOnCurve {
854+
return &transactionpb.CanWithdrawToAccountResponse{
855+
IsValidPaymentDestination: false,
856+
AccountType: transactionpb.CanWithdrawToAccountResponse_Unknown,
857+
}, nil
858+
}
859+
847860
ata, err := accountToCheck.ToAssociatedTokenAccount(common.CoreMintAccount)
848861
if err != nil {
849862
log.WithError(err).Warn("failure getting ata address")

0 commit comments

Comments
 (0)