From 1eca476ea43afb710a3b71131f019181e1e616fa Mon Sep 17 00:00:00 2001 From: Lee Smet Date: Tue, 15 Apr 2025 13:52:04 +0200 Subject: [PATCH 1/4] Change solana destination address encoding Provide the wallet master address instead of the ATA directly. The expected ATA is derived from the master address provided during processing. To remain backwars compatible, reshuffle the check to see if we've already minted to the beginning of the mint function (and mint sign function in cosigners), and add a function to check if this tx has already been refunded after that. Especially the latter is important since previously refunded txes would now be valid, causing the check to see if the refund already happened to not happen as that lives specifically in the refund handler, which would not be invoked in this case. Signed-off-by: Lee Smet --- bridges/stellar-solana/api/bridge/bridge.go | 40 ++++++++++++++----- .../api/bridge/signer_server.go | 39 ++++++++++-------- bridges/stellar-solana/solana/solana.go | 7 ++++ 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/bridges/stellar-solana/api/bridge/bridge.go b/bridges/stellar-solana/api/bridge/bridge.go index df93882..b803a14 100644 --- a/bridges/stellar-solana/api/bridge/bridge.go +++ b/bridges/stellar-solana/api/bridge/bridge.go @@ -104,21 +104,13 @@ func (bridge *Bridge) Close() error { return nil } -func (bridge *Bridge) mint(ctx context.Context, receiver solana.Address, depositedAmount *big.Int, txID string) (err error) { +func (bridge *Bridge) mint(ctx context.Context, memoAddress solana.Address, depositedAmount *big.Int, txID string) (err error) { if !bridge.synced { return errors.New("bridge is not synced, retry later") } - valid, err := bridge.solanaWallet.IsValidReceiver(ctx, receiver) - if err != nil { - return errors.Wrap(err, "Failed to check if receiver is proper") - } - - if !valid { - return faults.ErrInvalidReceiver - } - - log.Info().Str("receiver", receiver.String()).Str("txID", txID).Msg("Minting") + // Check if this tx is a known mint TX + log.Info().Str("receiver", memoAddress.String()).Str("txID", txID).Msg("Minting") // check if we already know this ID known, err := bridge.solanaWallet.IsMintTxID(ctx, txID) if err != nil { @@ -130,6 +122,32 @@ func (bridge *Bridge) mint(ctx context.Context, receiver solana.Address, deposit return } + // Check if we've already refunded this TX + known, err = bridge.wallet.TransactionStorage.TransactionWithMemoExists(ctx, txID) + if err != nil { + return + } + if known { + log.Info().Str("txID", txID).Msg("Skipping minting transaction we've already refunded") + // we already refunded this, so ignore the transaction + return + } + + // Convert receiver address to derived ATA + receiver, err := bridge.solanaWallet.ATAFromMasterAddress(memoAddress) + if err != nil { + return errors.Wrap(err, "could not convert memo master address to derived ATA") + } + + valid, err := bridge.solanaWallet.IsValidReceiver(ctx, receiver) + if err != nil { + return errors.Wrap(err, "Failed to check if receiver is proper") + } + + if !valid { + return faults.ErrInvalidReceiver + } + depositFeeBigInt := big.NewInt(stellar.IntToStroops(bridge.config.DepositFee)) if depositedAmount.Cmp(depositFeeBigInt) <= 0 { diff --git a/bridges/stellar-solana/api/bridge/signer_server.go b/bridges/stellar-solana/api/bridge/signer_server.go index 7219c9e..519160a 100644 --- a/bridges/stellar-solana/api/bridge/signer_server.go +++ b/bridges/stellar-solana/api/bridge/signer_server.go @@ -87,6 +87,29 @@ func (s *SignerService) SignMint(ctx context.Context, request SolanaRequest, res return err } + // Check in transaction storage if the deposit transaction exists + tx, err := s.stellarWallet.TransactionStorage.GetTransactionWithID(ctx, request.TxID) + if err != nil { + log.Info().Str("txid", request.TxID).Msg("transaction not found") + return err + } + + known, err := s.solWallet.IsMintTxID(ctx, tx.ID) + if err != nil { + return errors.Wrap(err, "Could not verify if we already know this mint") + } + if known { + return errors.New("Refusing to sign mint request for transaction we already minted") + } + + known, err = s.stellarWallet.TransactionStorage.TransactionWithMemoExists(ctx, request.TxID) + if err != nil { + return errors.Wrap(err, "Could not verify if we already refunded this tx") + } + if known { + return errors.New("Refusing to sign mint request for transaction we already refunded") + } + amount, memo, receiver, err := solana.ExtractMintvalues(*solTx) if memo != request.TxID { log.Warn().Str("requested txid", request.TxID).Str("embedded txid", memo).Msg("could not unmarshal transaction") @@ -102,13 +125,6 @@ func (s *SignerService) SignMint(ctx context.Context, request SolanaRequest, res return errors.New("Tx receiver does not match requested receiver") } - // Check in transaction storage if the deposit transaction exists - tx, err := s.stellarWallet.TransactionStorage.GetTransactionWithID(ctx, request.TxID) - if err != nil { - log.Info().Str("txid", request.TxID).Msg("transaction not found") - return err - } - // Validate amount depositedAmount, _, err := s.stellarWallet.GetDepositAmountAndSender(request.TxID, s.bridgeMasterAddress) if err != nil { @@ -139,15 +155,6 @@ func (s *SignerService) SignMint(ctx context.Context, request SolanaRequest, res return fmt.Errorf("deposit addresses do not match") } - known, err := s.solWallet.IsMintTxID(ctx, tx.ID) - if err != nil { - return errors.Wrap(err, "Could not verify if we already know this mint") - } - - if known { - return errors.New("Refusing to sign mint request for transaction we already minted") - } - signature, idx, err := s.solWallet.CreateTokenSignature(*solTx) if err != nil { return err diff --git a/bridges/stellar-solana/solana/solana.go b/bridges/stellar-solana/solana/solana.go index c037772..bd802b1 100644 --- a/bridges/stellar-solana/solana/solana.go +++ b/bridges/stellar-solana/solana/solana.go @@ -344,6 +344,13 @@ func (sol *Solana) listTransactionSigs(ctx context.Context) ([]Signature, error) return signatures, nil } +// ATAFromMasterAddress derives the expected ATA for the current mint assuming the given +// adddress is the master account. +func (sol *Solana) ATAFromMasterAddress(master Address) (Address, error) { + addr, _, err := solana.FindAssociatedTokenAddress(master, sol.tokenAddress) + return addr, err +} + // AddressFromHex decodes a hex encoded Solana address func AddressFromHex(encoded string) (Address, error) { b, err := hex.DecodeString(encoded) From c37f4ee6dd6949a0f579f57990ec768ad526fc23 Mon Sep 17 00:00:00 2001 From: Lee Smet Date: Wed, 4 Jun 2025 15:57:06 +0200 Subject: [PATCH 2/4] Log the derived ATA for debugging purposes Signed-off-by: Lee Smet --- bridges/stellar-solana/api/bridge/bridge.go | 1 + 1 file changed, 1 insertion(+) diff --git a/bridges/stellar-solana/api/bridge/bridge.go b/bridges/stellar-solana/api/bridge/bridge.go index b803a14..dac704f 100644 --- a/bridges/stellar-solana/api/bridge/bridge.go +++ b/bridges/stellar-solana/api/bridge/bridge.go @@ -139,6 +139,7 @@ func (bridge *Bridge) mint(ctx context.Context, memoAddress solana.Address, depo return errors.Wrap(err, "could not convert memo master address to derived ATA") } + log.Debug().Str("ATA", receiver.String()).Msg("Checking if computed receiver ATA is valid") valid, err := bridge.solanaWallet.IsValidReceiver(ctx, receiver) if err != nil { return errors.Wrap(err, "Failed to check if receiver is proper") From c62a264a16cce1b1a8c4458a1b525eee1703fe22 Mon Sep 17 00:00:00 2001 From: Lee Smet Date: Wed, 4 Jun 2025 16:21:16 +0200 Subject: [PATCH 3/4] Patch/Manually call solana.FindAsociatedTokenAccount The exposed call assumes usage of the regular token program, while we use Token2022 Signed-off-by: Lee Smet --- bridges/stellar-solana/solana/solana.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bridges/stellar-solana/solana/solana.go b/bridges/stellar-solana/solana/solana.go index bd802b1..04d412e 100644 --- a/bridges/stellar-solana/solana/solana.go +++ b/bridges/stellar-solana/solana/solana.go @@ -347,7 +347,16 @@ func (sol *Solana) listTransactionSigs(ctx context.Context) ([]Signature, error) // ATAFromMasterAddress derives the expected ATA for the current mint assuming the given // adddress is the master account. func (sol *Solana) ATAFromMasterAddress(master Address) (Address, error) { - addr, _, err := solana.FindAssociatedTokenAddress(master, sol.tokenAddress) + // Rather than calling solana.FindAssociatedTokenAddress(master, sol.tokenAddress), we call the internal function that function call directly. + // The reason for this is that we use Token2022 programs, and the library uses the regular token program, which leads to different ATA derivations. + addr, _, err := solana.FindProgramAddress([][]byte{ + master[:], + solana.Token2022ProgramID[:], + sol.tokenAddress[:], + }, + solana.SPLAssociatedTokenAccountProgramID, + ) + return addr, err } From 8dbf050de0cb157cdaf43236c8425790706f1147 Mon Sep 17 00:00:00 2001 From: Lee Smet Date: Wed, 4 Jun 2025 17:09:20 +0200 Subject: [PATCH 4/4] Compute ATA from stellar tx memo for signers In the mint tx and the request the ATA itself is passed, the stellar tx obviously contains the master. This allows us to also check if the derivation was done properly. Signed-off-by: Lee Smet --- bridges/stellar-solana/api/bridge/signer_server.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bridges/stellar-solana/api/bridge/signer_server.go b/bridges/stellar-solana/api/bridge/signer_server.go index 519160a..42c03d2 100644 --- a/bridges/stellar-solana/api/bridge/signer_server.go +++ b/bridges/stellar-solana/api/bridge/signer_server.go @@ -146,11 +146,19 @@ func (s *SignerService) SignMint(ctx context.Context, request SolanaRequest, res if tx.MemoType != "hash" { return errors.New("memo is not of type memo hash") } + + // Extract master address from stellar tx addr, err := solana.AddressFromB64(tx.Memo) if err != nil { return err } + // convert to ATA address, which is the one passed in the solana mint tx and the signing request + addr, err = s.solWallet.ATAFromMasterAddress(addr) + if err != nil { + return err + } + if addr != request.Receiver { return fmt.Errorf("deposit addresses do not match") }