From 80510d8f195acbf80bd1bc00d2d10405d67449ab Mon Sep 17 00:00:00 2001 From: skosito Date: Fri, 8 Nov 2024 23:54:57 +0100 Subject: [PATCH 01/10] withdraw spl and e2e test --- cmd/zetae2e/local/local.go | 3 +- e2e/e2etests/e2etests.go | 13 +- e2e/e2etests/test_spl_withdraw.go | 69 +++++++ e2e/runner/setup_solana.go | 24 +++ e2e/runner/solana.go | 43 ++++- go.mod | 2 +- go.sum | 4 +- pkg/contracts/solana/gateway.go | 22 +++ pkg/contracts/solana/gateway_message.go | 138 +++++++++++++- pkg/contracts/solana/instruction.go | 65 +++++++ zetaclient/chains/solana/observer/outbound.go | 2 + zetaclient/chains/solana/signer/signer.go | 97 +++++++++- zetaclient/chains/solana/signer/withdraw.go | 2 +- .../chains/solana/signer/withdraw_spl.go | 178 ++++++++++++++++++ 14 files changed, 646 insertions(+), 16 deletions(-) create mode 100644 e2e/e2etests/test_spl_withdraw.go create mode 100644 zetaclient/chains/solana/signer/withdraw_spl.go diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 9b818ea8c0..64710202e9 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -424,9 +424,10 @@ func localE2ETest(cmd *cobra.Command, _ []string) { e2etests.TestSolanaWithdrawRestrictedName, // TODO move under admin tests // https://github.com/zeta-chain/node/issues/3085 - e2etests.TestSolanaWhitelistSPLName, e2etests.TestSPLDepositName, e2etests.TestSPLDepositAndCallName, + e2etests.TestSPLWithdrawName, + e2etests.TestSolanaWhitelistSPLName, } eg.Go(solanaTestRoutine(conf, deployerRunner, verbose, solanaTests...)) } diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index a880172830..fc02bb7c5a 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -63,6 +63,7 @@ const ( TestSolanaWithdrawRestrictedName = "solana_withdraw_restricted" TestSPLDepositName = "spl_deposit" TestSPLDepositAndCallName = "spl_deposit_and_call" + TestSPLWithdrawName = "spl_withdraw" /** * TON tests @@ -433,6 +434,14 @@ var AllE2ETests = []runner.E2ETest{ }, TestSolanaDepositAndCall, ), + runner.NewE2ETest( + TestSPLWithdrawName, + "withdraw SPL from ZEVM", + []runner.ArgDefinition{ + {Description: "amount in spl tokens", DefaultValue: "1000000"}, + }, + TestSPLWithdraw, + ), runner.NewE2ETest( TestSolanaDepositAndCallRefundName, "deposit SOL into ZEVM and call a contract that reverts; should refund", @@ -469,7 +478,7 @@ var AllE2ETests = []runner.E2ETest{ TestSPLDepositName, "deposit SPL into ZEVM", []runner.ArgDefinition{ - {Description: "amount of spl tokens", DefaultValue: "500000"}, + {Description: "amount of spl tokens", DefaultValue: "12000000"}, }, TestSPLDeposit, ), @@ -477,7 +486,7 @@ var AllE2ETests = []runner.E2ETest{ TestSPLDepositAndCallName, "deposit SPL into ZEVM and call", []runner.ArgDefinition{ - {Description: "amount of spl tokens", DefaultValue: "500000"}, + {Description: "amount of spl tokens", DefaultValue: "12000000"}, }, TestSPLDepositAndCall, ), diff --git a/e2e/e2etests/test_spl_withdraw.go b/e2e/e2etests/test_spl_withdraw.go new file mode 100644 index 0000000000..c0e94e926c --- /dev/null +++ b/e2e/e2etests/test_spl_withdraw.go @@ -0,0 +1,69 @@ +package e2etests + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/runner" +) + +func TestSPLWithdraw(r *runner.E2ERunner, args []string) { + require.Len(r, args, 1) + + withdrawAmount := parseBigInt(r, args[0]) + + // get SPL ZRC20 balance before withdraw + zrc20BalanceBefore, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) + require.NoError(r, err) + r.Logger.Info("runner balance of SPL before withdraw: %d", zrc20BalanceBefore) + + require.Equal(r, 1, zrc20BalanceBefore.Cmp(withdrawAmount), "Insufficient balance for withdrawal") + + // parse withdraw amount (in lamports), approve amount is 1 SOL + approvedAmount := new(big.Int).SetUint64(solana.LAMPORTS_PER_SOL) + require.Equal( + r, + -1, + withdrawAmount.Cmp(approvedAmount), + "Withdrawal amount must be less than the approved amount (1e9)", + ) + + // load deployer private key + privkey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String()) + require.NoError(r, err) + + // get receiver ata balance before withdraw + receiverAta := r.FindOrCreateAssociatedTokenAccount(privkey, privkey.PublicKey(), r.SPLAddr) + receiverBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, receiverAta, rpc.CommitmentConfirmed) + require.NoError(r, err) + r.Logger.Info("receiver balance of SPL before withdraw: %s", receiverBalanceBefore.Value.Amount) + + // withdraw + r.WithdrawSPLZRC20(privkey.PublicKey(), withdrawAmount, approvedAmount) + + // get SPL ZRC20 balance after withdraw + zrc20BalanceAfter, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) + require.NoError(r, err) + r.Logger.Info("runner balance of SPL after withdraw: %d", zrc20BalanceAfter) + + // verify balances are updated + receiverBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, receiverAta, rpc.CommitmentConfirmed) + require.NoError(r, err) + r.Logger.Info("receiver balance of SPL after withdraw: %s", receiverBalanceAfter.Value.Amount) + + // verify amount is added to receiver ata + require.Zero( + r, + new( + big.Int, + ).Add(withdrawAmount, parseBigInt(r, receiverBalanceBefore.Value.Amount)). + Cmp(parseBigInt(r, receiverBalanceAfter.Value.Amount)), + ) + + // verify amount is subtracted on zrc20 + require.Zero(r, new(big.Int).Sub(zrc20BalanceBefore, withdrawAmount).Cmp(zrc20BalanceAfter)) +} diff --git a/e2e/runner/setup_solana.go b/e2e/runner/setup_solana.go index a7589d6af1..c6738c4e3d 100644 --- a/e2e/runner/setup_solana.go +++ b/e2e/runner/setup_solana.go @@ -85,6 +85,30 @@ func (r *E2ERunner) SetupSolana(deployerPrivateKey string) { require.NoError(r, err) r.Logger.Info("initial PDA balance: %d lamports", balance.Value) + // initialize rent payer + var instRentPayer solana.GenericInstruction + rentPayerPdaComputed := r.ComputeRentPayerPdaAddress() + + // create 'initialize_rent_payer' instruction + accountSlice = []*solana.AccountMeta{} + accountSlice = append(accountSlice, solana.Meta(rentPayerPdaComputed).WRITE()) + accountSlice = append(accountSlice, solana.Meta(privkey.PublicKey()).WRITE().SIGNER()) + accountSlice = append(accountSlice, solana.Meta(solana.SystemProgramID)) + instRentPayer.ProgID = r.GatewayProgram + instRentPayer.AccountValues = accountSlice + + instRentPayer.DataBytes, err = borsh.Serialize(solanacontracts.InitializeRentPayerParams{ + Discriminator: solanacontracts.DiscriminatorInitializeRentPayer, + }) + require.NoError(r, err) + + // create and sign the transaction + signedTx = r.CreateSignedTransaction([]solana.Instruction{&instRentPayer}, privkey, []solana.PrivateKey{}) + + // broadcast the transaction and wait for finalization + _, out = r.BroadcastTxSync(signedTx) + r.Logger.Info("initialize_rent_payer logs: %v", out.Meta.LogMessages) + err = r.ensureSolanaChainParams() require.NoError(r, err) diff --git a/e2e/runner/solana.go b/e2e/runner/solana.go index 542968938d..3d2f6c4776 100644 --- a/e2e/runner/solana.go +++ b/e2e/runner/solana.go @@ -30,6 +30,18 @@ func (r *E2ERunner) ComputePdaAddress() solana.PublicKey { return pdaComputed } +// ComputePdaAddress computes the rent payer PDA address for the gateway program +func (r *E2ERunner) ComputeRentPayerPdaAddress() solana.PublicKey { + seed := []byte(solanacontract.RentPayerPDASeed) + GatewayProgramID := solana.MustPublicKeyFromBase58(solanacontract.SolanaGatewayProgramID) + pdaComputed, bump, err := solana.FindProgramAddress([][]byte{seed}, GatewayProgramID) + require.NoError(r, err) + + r.Logger.Info("computed rent payer pda: %s, bump %d\n", pdaComputed, bump) + + return pdaComputed +} + // CreateDepositInstruction creates a 'deposit' instruction func (r *E2ERunner) CreateDepositInstruction( signer solana.PublicKey, @@ -250,7 +262,7 @@ func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey, whitelist bool) *so // minting some tokens to deployer for testing ata := r.FindOrCreateAssociatedTokenAccount(*privateKey, privateKey.PublicKey(), tokenAccount.PublicKey()) - mintToInstruction := token.NewMintToInstruction(uint64(1_000_000), tokenAccount.PublicKey(), ata, privateKey.PublicKey(), []solana.PublicKey{}). + mintToInstruction := token.NewMintToInstruction(uint64(1_000_000_000), tokenAccount.PublicKey(), ata, privateKey.PublicKey(), []solana.PublicKey{}). Build() signedTx = r.CreateSignedTransaction( []solana.Instruction{mintToInstruction}, @@ -379,3 +391,32 @@ func (r *E2ERunner) WithdrawSOLZRC20( return cctx } + +// WithdrawSPLZRC20 withdraws an amount of ZRC20 SPL tokens +func (r *E2ERunner) WithdrawSPLZRC20( + to solana.PublicKey, + amount *big.Int, + approveAmount *big.Int, +) *crosschaintypes.CrossChainTx { + // approve splzrc20 to spend gas tokens to pay gas fee + tx, err := r.SOLZRC20.Approve(r.ZEVMAuth, r.SPLZRC20Addr, approveAmount) + require.NoError(r, err) + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "approve") + + // withdraw + tx, err = r.SPLZRC20.Withdraw(r.ZEVMAuth, []byte(to.String()), amount) + require.NoError(r, err) + r.Logger.EVMTransaction(*tx, "withdraw") + + // wait for tx receipt + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "withdraw") + r.Logger.Info("Receipt txhash %s status %d", receipt.TxHash, receipt.Status) + + // wait for the cctx to be mined + cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) + + return cctx +} diff --git a/go.mod b/go.mod index adb9b1cdb7..5f7d83e448 100644 --- a/go.mod +++ b/go.mod @@ -318,7 +318,7 @@ require ( github.com/montanaflynn/stats v0.7.1 github.com/showa-93/go-mask v0.6.2 github.com/tonkeeper/tongo v1.9.3 - github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20241025181051-d8d49e4fc85b + github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20241108171442-e48d82f94892 ) require ( diff --git a/go.sum b/go.sum index 4297217034..97df1129f4 100644 --- a/go.sum +++ b/go.sum @@ -1531,8 +1531,8 @@ github.com/zeta-chain/go-tss v0.0.0-20241031223543-18765295f992 h1:jpfOoQGHQo29C github.com/zeta-chain/go-tss v0.0.0-20241031223543-18765295f992/go.mod h1:nqelgf4HKkqlXaVg8X38a61WfyYB+ivCt6nnjoTIgCc= github.com/zeta-chain/protocol-contracts v1.0.2-athens3.0.20241021075719-d40d2e28467c h1:ZoFxMMZtivRLquXVq1sEVlT45UnTPMO1MSXtc88nDv4= github.com/zeta-chain/protocol-contracts v1.0.2-athens3.0.20241021075719-d40d2e28467c/go.mod h1:SjT7QirtJE8stnAe1SlNOanxtfSfijJm3MGJ+Ax7w7w= -github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20241025181051-d8d49e4fc85b h1:w4YVBbWxk9TI+7HM8hTvK66IgOo5XvEFsmH7n6WgW50= -github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20241025181051-d8d49e4fc85b/go.mod h1:DcDY828o773soiU/h0XpC+naxitrIMFVZqEvq/EJxMA= +github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20241108171442-e48d82f94892 h1:oI5qCrw2SXDf2a2UYAn0tpaKHbKpJcR+XDtceyY00wE= +github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20241108171442-e48d82f94892/go.mod h1:DcDY828o773soiU/h0XpC+naxitrIMFVZqEvq/EJxMA= github.com/zeta-chain/tss-lib v0.0.0-20240916163010-2e6b438bd901 h1:9whtN5fjYHfk4yXIuAsYP2EHxImwDWDVUOnZJ2pfL3w= github.com/zeta-chain/tss-lib v0.0.0-20240916163010-2e6b438bd901/go.mod h1:d2iTC62s9JwKiCMPhcDDXbIZmuzAyJ4lwso0H5QyRbk= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= diff --git a/pkg/contracts/solana/gateway.go b/pkg/contracts/solana/gateway.go index 12e3a10aa8..d1cc628f93 100644 --- a/pkg/contracts/solana/gateway.go +++ b/pkg/contracts/solana/gateway.go @@ -14,6 +14,9 @@ const ( // PDASeed is the seed for the Solana gateway program derived address PDASeed = "meta" + // RentPayerPDASeed is the seed for the Solana gateway program derived address + RentPayerPDASeed = "rent-payer" + // AccountsNumberOfDeposit is the number of accounts required for Solana gateway deposit instruction // [signer, pda, system_program] accountsNumDeposit = 3 @@ -26,6 +29,8 @@ const ( var ( // DiscriminatorInitialize returns the discriminator for Solana gateway 'initialize' instruction DiscriminatorInitialize = idlgateway.IDLGateway.GetDiscriminator("initialize") + // DiscriminatorInitializeRentPayer returns the discriminator for Solana gateway 'initialize_rent_payer' instruction + DiscriminatorInitializeRentPayer = idlgateway.IDLGateway.GetDiscriminator("initialize_rent_payer") // DiscriminatorDeposit returns the discriminator for Solana gateway 'deposit' instruction DiscriminatorDeposit = idlgateway.IDLGateway.GetDiscriminator("deposit") // DiscriminatorDepositSPL returns the discriminator for Solana gateway 'deposit_spl_token' instruction @@ -54,3 +59,20 @@ func ParseGatewayIDAndPda(address string) (solana.PublicKey, solana.PublicKey, e return gatewayID, pda, err } + +// ParseGatewayAddressAndPda parses the rent payer program derived address from the given string +func ParseRentPayerPda(address string) (solana.PublicKey, error) { + var rentPayerPda solana.PublicKey + + // decode gateway address + gatewayID, err := solana.PublicKeyFromBase58(address) + if err != nil { + return rentPayerPda, errors.Wrap(err, "unable to decode address") + } + + // compute gateway PDA + seed := []byte(RentPayerPDASeed) + rentPayerPda, _, err = solana.FindProgramAddress([][]byte{seed}, gatewayID) + + return rentPayerPda, err +} diff --git a/pkg/contracts/solana/gateway_message.go b/pkg/contracts/solana/gateway_message.go index 1c8abaca23..4979c69ac0 100644 --- a/pkg/contracts/solana/gateway_message.go +++ b/pkg/contracts/solana/gateway_message.go @@ -8,18 +8,18 @@ import ( "github.com/gagliardetto/solana-go" ) -// MsgWithdraw is the message for the Solana gateway withdraw/withdraw_spl instruction +// MsgWithdraw is the message for the Solana gateway withdraw instruction type MsgWithdraw struct { // chainID is the chain ID of Solana chain chainID uint64 - // Nonce is the nonce for the withdraw/withdraw_spl + // Nonce is the nonce for the withdraw nonce uint64 - // amount is the lamports amount for the withdraw/withdraw_spl + // amount is the lamports amount for the withdraw amount uint64 - // To is the recipient address for the withdraw/withdraw_spl + // To is the recipient address for the withdraw to solana.PublicKey // signature is the signature of the message @@ -108,6 +108,136 @@ func (msg *MsgWithdraw) Signer() (common.Address, error) { return RecoverSigner(msgHash[:], msgSig[:]) } +// MsgWithdrawSPL is the message for the Solana gateway withdraw_spl instruction +type MsgWithdrawSPL struct { + // chainID is the chain ID of Solana chain + chainID uint64 + + // Nonce is the nonce for the withdraw_spl + nonce uint64 + + // amount is the lamports amount for the withdraw_spl + amount uint64 + + // tokenAccount is the address for the spl token + tokenAccount solana.PublicKey + + // decimals of spl token + decimals uint8 + + // to is the recipient address for the withdraw_spl + to solana.PublicKey + + // recipientAta is the recipient address for the withdraw_spl + recipientAta solana.PublicKey + + // signature is the signature of the message + signature [65]byte +} + +// NewMsgWithdrawSPL returns a new withdraw spl message +func NewMsgWithdrawSPL( + chainID, nonce, amount uint64, + decimals uint8, + tokenAccount, to, toAta solana.PublicKey, +) *MsgWithdrawSPL { + return &MsgWithdrawSPL{ + chainID: chainID, + nonce: nonce, + amount: amount, + to: to, + recipientAta: toAta, + tokenAccount: tokenAccount, + decimals: decimals, + } +} + +// ChainID returns the chain ID of the message +func (msg *MsgWithdrawSPL) ChainID() uint64 { + return msg.chainID +} + +// Nonce returns the nonce of the message +func (msg *MsgWithdrawSPL) Nonce() uint64 { + return msg.nonce +} + +// Amount returns the amount of the message +func (msg *MsgWithdrawSPL) Amount() uint64 { + return msg.amount +} + +// To returns the recipient address of the message +func (msg *MsgWithdrawSPL) To() solana.PublicKey { + return msg.to +} + +func (msg *MsgWithdrawSPL) RecipientAta() solana.PublicKey { + return msg.recipientAta +} + +func (msg *MsgWithdrawSPL) TokenAccount() solana.PublicKey { + return msg.tokenAccount +} + +func (msg *MsgWithdrawSPL) Decimals() uint8 { + return msg.decimals +} + +// Hash packs the withdraw spl message and computes the hash +func (msg *MsgWithdrawSPL) Hash() [32]byte { + var message []byte + buff := make([]byte, 8) + + message = append(message, []byte("withdraw_spl_token")...) + + binary.BigEndian.PutUint64(buff, msg.chainID) + message = append(message, buff...) + + binary.BigEndian.PutUint64(buff, msg.nonce) + message = append(message, buff...) + + binary.BigEndian.PutUint64(buff, msg.amount) + message = append(message, buff...) + + message = append(message, msg.tokenAccount.Bytes()...) + + message = append(message, msg.recipientAta.Bytes()...) + + return crypto.Keccak256Hash(message) +} + +// SetSignature attaches the signature to the message +func (msg *MsgWithdrawSPL) SetSignature(signature [65]byte) *MsgWithdrawSPL { + msg.signature = signature + return msg +} + +// SigRSV returns the full 65-byte [R+S+V] signature +func (msg *MsgWithdrawSPL) SigRSV() [65]byte { + return msg.signature +} + +// SigRS returns the 64-byte [R+S] core part of the signature +func (msg *MsgWithdrawSPL) SigRS() [64]byte { + var sig [64]byte + copy(sig[:], msg.signature[:64]) + return sig +} + +// SigV returns the V part (recovery ID) of the signature +func (msg *MsgWithdrawSPL) SigV() uint8 { + return msg.signature[64] +} + +// Signer returns the signer of the message +func (msg *MsgWithdrawSPL) Signer() (common.Address, error) { + msgHash := msg.Hash() + msgSig := msg.SigRSV() + + return RecoverSigner(msgHash[:], msgSig[:]) +} + // MsgWhitelist is the message for the Solana gateway whitelist_spl_mint instruction type MsgWhitelist struct { // whitelistCandidate is the SPL token to be whitelisted in gateway program diff --git a/pkg/contracts/solana/instruction.go b/pkg/contracts/solana/instruction.go index 65b6e6e4c3..44aa80c16b 100644 --- a/pkg/contracts/solana/instruction.go +++ b/pkg/contracts/solana/instruction.go @@ -22,6 +22,12 @@ type InitializeParams struct { ChainID uint64 } +// InitializeRentPayerParams contains the parameters for a gateway initialize_rent_payer instruction +type InitializeRentPayerParams struct { + // Discriminator is the unique identifier for the initialize_rent_payer instruction + Discriminator [8]byte +} + // DepositInstructionParams contains the parameters for a gateway deposit instruction type DepositInstructionParams struct { // Discriminator is the unique identifier for the deposit instruction @@ -118,6 +124,65 @@ func ParseInstructionWithdraw(instruction solana.CompiledInstruction) (*Withdraw return inst, nil } +type WithdrawSPLInstructionParams struct { + // Discriminator is the unique identifier for the withdraw instruction + Discriminator [8]byte + + Decimals uint8 + + // Amount is the lamports amount for the withdraw + Amount uint64 + + // Signature is the ECDSA signature (by TSS) for the withdraw + Signature [64]byte + + // RecoveryID is the recovery ID used to recover the public key from ECDSA signature + RecoveryID uint8 + + // MessageHash is the hash of the message signed by TSS + MessageHash [32]byte + + // Nonce is the nonce for the withdraw + Nonce uint64 +} + +// Signer returns the signer of the signature contained +func (inst *WithdrawSPLInstructionParams) Signer() (signer common.Address, err error) { + var signature [65]byte + copy(signature[:], inst.Signature[:64]) + signature[64] = inst.RecoveryID + + return RecoverSigner(inst.MessageHash[:], signature[:]) +} + +// GatewayNonce returns the nonce of the instruction +func (inst *WithdrawSPLInstructionParams) GatewayNonce() uint64 { + return inst.Nonce +} + +// TokenAmount returns the amount of the instruction +func (inst *WithdrawSPLInstructionParams) TokenAmount() uint64 { + return inst.Amount +} + +// ParseInstructionWithdraw tries to parse the instruction as a 'withdraw'. +// It returns nil if the instruction can't be parsed as a 'withdraw'. +func ParseInstructionWithdrawSPL(instruction solana.CompiledInstruction) (*WithdrawSPLInstructionParams, error) { + // try deserializing instruction as a 'withdraw' + inst := &WithdrawSPLInstructionParams{} + err := borsh.Deserialize(inst, instruction.Data) + if err != nil { + return nil, errors.Wrap(err, "error deserializing instruction") + } + + // check the discriminator to ensure it's a 'withdraw' instruction + if inst.Discriminator != DiscriminatorWithdrawSPL { + return nil, fmt.Errorf("not a withdraw instruction: %v", inst.Discriminator) + } + + return inst, nil +} + // RecoverSigner recover the ECDSA signer from given message hash and signature func RecoverSigner(msgHash []byte, msgSig []byte) (signer common.Address, err error) { // recover the public key diff --git a/zetaclient/chains/solana/observer/outbound.go b/zetaclient/chains/solana/observer/outbound.go index 60bd70bec7..2ed98575c4 100644 --- a/zetaclient/chains/solana/observer/outbound.go +++ b/zetaclient/chains/solana/observer/outbound.go @@ -356,6 +356,8 @@ func ParseGatewayInstruction( return contracts.ParseInstructionWithdraw(instruction) case coin.CoinType_Cmd: return contracts.ParseInstructionWhitelist(instruction) + case coin.CoinType_ERC20: + return contracts.ParseInstructionWithdrawSPL(instruction) default: return nil, fmt.Errorf("unsupported outbound coin type %s", coinType) } diff --git a/zetaclient/chains/solana/signer/signer.go b/zetaclient/chains/solana/signer/signer.go index 8e180f8c7f..f70ddba677 100644 --- a/zetaclient/chains/solana/signer/signer.go +++ b/zetaclient/chains/solana/signer/signer.go @@ -6,8 +6,11 @@ import ( "strings" "cosmossdk.io/errors" + "github.com/davecgh/go-spew/spew" ethcommon "github.com/ethereum/go-ethereum/common" + bin "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/programs/token" "github.com/gagliardetto/solana-go/rpc" "github.com/rs/zerolog" @@ -43,6 +46,9 @@ type Signer struct { // pda is the program derived address of the gateway program pda solana.PublicKey + + // rent payer pda is the program derived address of the gateway program to pay rent for creating atas + rentPayerPda solana.PublicKey } // NewSigner creates a new Solana signer @@ -64,12 +70,18 @@ func NewSigner( return nil, errors.Wrapf(err, "cannot parse gateway address %s", chainParams.GatewayAddress) } + rentPayerPda, err := contracts.ParseRentPayerPda(chainParams.GatewayAddress) + if err != nil { + return nil, errors.Wrapf(err, "cannot parse gateway address %s", chainParams.GatewayAddress) + } + // create Solana signer signer := &Signer{ - Signer: baseSigner, - client: solClient, - gatewayID: gatewayID, - pda: pda, + Signer: baseSigner, + client: solClient, + gatewayID: gatewayID, + pda: pda, + rentPayerPda: rentPayerPda, } // construct Solana private key if present @@ -151,6 +163,15 @@ func (signer *Signer) TryProcessOutbound( } tx = withdrawTx + + case coin.CoinType_ERC20: + withdrawSPLTx, err := signer.prepareWithdrawSPLTx(ctx, cctx, height, logger) + if err != nil { + logger.Error().Err(err).Msgf("TryProcessOutbound: Fail to sign withdraw spl outbound") + return + } + + tx = withdrawSPLTx default: logger.Error(). Msgf("TryProcessOutbound: can only send SOL to the Solana network") @@ -219,6 +240,56 @@ func (signer *Signer) prepareWithdrawTx( return tx, nil } +func (signer *Signer) prepareWithdrawSPLTx( + ctx context.Context, + cctx *types.CrossChainTx, + height uint64, + logger zerolog.Logger, +) (*solana.Transaction, error) { + params := cctx.GetCurrentOutboundParam() + // compliance check + cancelTx := compliance.IsCctxRestricted(cctx) + if cancelTx { + compliance.PrintComplianceLog( + logger, + signer.Logger().Compliance, + true, + signer.Chain().ChainId, + cctx.Index, + cctx.InboundParams.Sender, + params.Receiver, + "SPL", + ) + } + + // get mint details to get decimals + mint, err := signer.decodeMintAccountDetails(ctx, cctx.InboundParams.Asset) + if err != nil { + return nil, err + } + + // sign gateway withdraw spl message by TSS + msg, err := signer.createAndSignMsgWithdrawSPL( + ctx, + params, + height, + cctx.InboundParams.Asset, + mint.Decimals, + cancelTx, + ) + if err != nil { + return nil, err + } + + // sign the withdraw transaction by relayer key + tx, err := signer.signWithdrawSPLTx(ctx, *msg) + if err != nil { + return nil, err + } + + return tx, nil +} + func (signer *Signer) prepareWhitelistTx( ctx context.Context, cctx *types.CrossChainTx, @@ -256,6 +327,24 @@ func (signer *Signer) prepareWhitelistTx( return tx, nil } +func (signer *Signer) decodeMintAccountDetails(ctx context.Context, asset string) (token.Mint, error) { + info, err := signer.client.GetAccountInfo(ctx, solana.MustPublicKeyFromBase58(asset)) + if err != nil { + return token.Mint{}, err + } + + var mint token.Mint + // Account{}.Data.GetBinary() returns the *decoded* binary data + // regardless the original encoding (it can handle them all). + err = bin.NewBinDecoder(info.Value.Data.GetBinary()).Decode(&mint) + if err != nil { + return token.Mint{}, err + } + spew.Dump(mint) + + return mint, nil +} + // SetGatewayAddress sets the gateway address func (signer *Signer) SetGatewayAddress(address string) { // parse gateway ID and PDA diff --git a/zetaclient/chains/solana/signer/withdraw.go b/zetaclient/chains/solana/signer/withdraw.go index 51f4cceeea..c9c94d8702 100644 --- a/zetaclient/chains/solana/signer/withdraw.go +++ b/zetaclient/chains/solana/signer/withdraw.go @@ -13,7 +13,7 @@ import ( "github.com/zeta-chain/node/x/crosschain/types" ) -// createAndSignMsgWithdraw creates and signs a withdraw message (for gateway withdraw/withdraw_spl instruction) with TSS. +// createAndSignMsgWithdraw creates and signs a withdraw message for gateway withdraw instruction with TSS. func (signer *Signer) createAndSignMsgWithdraw( ctx context.Context, params *types.OutboundParams, diff --git a/zetaclient/chains/solana/signer/withdraw_spl.go b/zetaclient/chains/solana/signer/withdraw_spl.go new file mode 100644 index 0000000000..ff0fd8a11e --- /dev/null +++ b/zetaclient/chains/solana/signer/withdraw_spl.go @@ -0,0 +1,178 @@ +package signer + +import ( + "context" + + "cosmossdk.io/errors" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + "github.com/near/borsh-go" + + "github.com/zeta-chain/node/pkg/chains" + contracts "github.com/zeta-chain/node/pkg/contracts/solana" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// createAndSignMsgWithdrawSPL creates and signs a withdraw spl message for gateway withdraw_spl instruction with TSS. +func (signer *Signer) createAndSignMsgWithdrawSPL( + ctx context.Context, + params *types.OutboundParams, + height uint64, + asset string, + decimals uint8, + cancelTx bool, +) (*contracts.MsgWithdrawSPL, error) { + chain := signer.Chain() + // #nosec G115 always positive + chainID := uint64(signer.Chain().ChainId) + nonce := params.TssNonce + amount := params.Amount.Uint64() + + // zero out the amount if cancelTx is set. It's legal to withdraw 0 spl thru the gateway. + if cancelTx { + amount = 0 + } + + // check receiver address + to, err := chains.DecodeSolanaWalletAddress(params.Receiver) + if err != nil { + return nil, errors.Wrapf(err, "cannot decode receiver address %s", params.Receiver) + } + + // parse token account + tokenAccount, err := solana.PublicKeyFromBase58(asset) + if err != nil { + return nil, err + } + + // get recipient ata + recipientAta, _, err := solana.FindAssociatedTokenAddress(to, tokenAccount) + if err != nil { + return nil, err + } + + // prepare withdraw spl msg and compute hash + msg := contracts.NewMsgWithdrawSPL(chainID, nonce, amount, decimals, tokenAccount, to, recipientAta) + msgHash := msg.Hash() + + // sign the message with TSS to get an ECDSA signature. + // the produced signature is in the [R || S || V] format where V is 0 or 1. + signature, err := signer.TSS().Sign(ctx, msgHash[:], height, nonce, chain.ChainId, "") + if err != nil { + return nil, errors.Wrap(err, "Key-sign failed") + } + signer.Logger().Std.Info().Msgf("Key-sign succeed for chain %d nonce %d", chainID, nonce) + + // attach the signature and return + return msg.SetSignature(signature), nil +} + +// signWithdrawSPLTx wraps the withdraw spl 'msg' into a Solana transaction and signs it with the relayer key. +func (signer *Signer) signWithdrawSPLTx( + ctx context.Context, + msg contracts.MsgWithdrawSPL, +) (*solana.Transaction, error) { + // create withdraw spl instruction with program call data + var err error + var inst solana.GenericInstruction + inst.DataBytes, err = borsh.Serialize(contracts.WithdrawSPLInstructionParams{ + Discriminator: contracts.DiscriminatorWithdrawSPL, + Decimals: msg.Decimals(), + Amount: msg.Amount(), + Signature: msg.SigRS(), + RecoveryID: msg.SigV(), + MessageHash: msg.Hash(), + Nonce: msg.Nonce(), + }) + if err != nil { + return nil, errors.Wrap(err, "cannot serialize withdraw instruction") + } + + // attach required accounts to the instruction + privkey := signer.relayerKey + err = attachWithdrawSPLAccounts( + &inst, + privkey.PublicKey(), + signer.pda, + signer.rentPayerPda, + msg.TokenAccount(), + msg.To(), + signer.gatewayID, + ) + if err != nil { + return nil, err + } + // get a recent blockhash + recent, err := signer.client.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) + if err != nil { + return nil, errors.Wrap(err, "GetLatestBlockhash error") + } + + // create a transaction that wraps the instruction + tx, err := solana.NewTransaction( + []solana.Instruction{ + // TODO: outbound now uses 5K lamports as the fixed fee, we could explore priority fee and compute budget + // https://github.com/zeta-chain/node/issues/2599 + // programs.ComputeBudgetSetComputeUnitLimit(computeUnitLimit), + // programs.ComputeBudgetSetComputeUnitPrice(computeUnitPrice), + &inst}, + recent.Value.Blockhash, + solana.TransactionPayer(privkey.PublicKey()), + ) + if err != nil { + return nil, errors.Wrap(err, "NewTransaction error") + } + + // relayer signs the transaction + _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey { + if key.Equals(privkey.PublicKey()) { + return privkey + } + return nil + }) + if err != nil { + return nil, errors.Wrap(err, "signer unable to sign transaction") + } + + return tx, nil +} + +// attachWithdrawSPLAccounts attaches the required accounts for the gateway withdraw spl instruction. +func attachWithdrawSPLAccounts( + inst *solana.GenericInstruction, + signer solana.PublicKey, + pda solana.PublicKey, + rentPayerPda solana.PublicKey, + tokenAccount solana.PublicKey, + to solana.PublicKey, + gatewayID solana.PublicKey, +) error { + // attach required accounts to the instruction + pdaAta, _, err := solana.FindAssociatedTokenAddress(pda, tokenAccount) + if err != nil { + return err + } + + recipientAta, _, err := solana.FindAssociatedTokenAddress(to, tokenAccount) + if err != nil { + return err + } + + var accountSlice []*solana.AccountMeta + accountSlice = append(accountSlice, solana.Meta(signer).WRITE().SIGNER()) + accountSlice = append(accountSlice, solana.Meta(pda).WRITE()) + accountSlice = append(accountSlice, solana.Meta(pdaAta).WRITE()) + accountSlice = append(accountSlice, solana.Meta(tokenAccount)) + accountSlice = append(accountSlice, solana.Meta(to)) + accountSlice = append(accountSlice, solana.Meta(recipientAta).WRITE()) + accountSlice = append(accountSlice, solana.Meta(rentPayerPda).WRITE()) + accountSlice = append(accountSlice, solana.Meta(solana.TokenProgramID)) + accountSlice = append(accountSlice, solana.Meta(solana.SPLAssociatedTokenAccountProgramID)) + accountSlice = append(accountSlice, solana.Meta(solana.SystemProgramID)) + + inst.ProgID = gatewayID + + inst.AccountValues = accountSlice + + return nil +} From 5eee39a8608d46f32a3abe44433eb39377285012 Mon Sep 17 00:00:00 2001 From: skosito Date: Fri, 8 Nov 2024 23:56:28 +0100 Subject: [PATCH 02/10] changelog --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index d71bded0a0..278f489c74 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ * [2984](https://github.com/zeta-chain/node/pull/2984) - add Whitelist message ability to whitelist SPL tokens on Solana * [3091](https://github.com/zeta-chain/node/pull/3091) - improve build reproducability. `make release{,-build-only}` checksums should now be stable. * [3124](https://github.com/zeta-chain/node/pull/3124) - integrate SPL deposits +* [3134](https://github.com/zeta-chain/node/pull/3134) - integrate withdraw SPL ### Tests * [3075](https://github.com/zeta-chain/node/pull/3075) - ton: withdraw concurrent, deposit & revert. From f852509697de66a46e325e63a7b07b10565e1e9b Mon Sep 17 00:00:00 2001 From: skosito Date: Sat, 9 Nov 2024 00:07:40 +0100 Subject: [PATCH 03/10] fix unit test --- zetaclient/chains/solana/observer/outbound_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zetaclient/chains/solana/observer/outbound_test.go b/zetaclient/chains/solana/observer/outbound_test.go index 73af8da573..bdda96c451 100644 --- a/zetaclient/chains/solana/observer/outbound_test.go +++ b/zetaclient/chains/solana/observer/outbound_test.go @@ -234,7 +234,7 @@ func Test_ParseGatewayInstruction(t *testing.T) { // load and unmarshal archived transaction txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) - inst, err := observer.ParseGatewayInstruction(txResult, gatewayID, coin.CoinType_ERC20) + inst, err := observer.ParseGatewayInstruction(txResult, gatewayID, coin.CoinType_Zeta) require.ErrorContains(t, err, "unsupported outbound coin type") require.Nil(t, inst) }) From b0f44ff6d2078857ac50c8ae84a80f2642ab422a Mon Sep 17 00:00:00 2001 From: skosito Date: Sun, 10 Nov 2024 21:14:36 +0100 Subject: [PATCH 04/10] e2e test that rent payer creates receiver ata --- cmd/zetae2e/local/local.go | 1 + contrib/localnet/solana/start-solana.sh | 3 +- e2e/e2etests/e2etests.go | 27 ++++--- ...st_spl_withdraw_and_create_receiver_ata.go | 72 +++++++++++++++++++ 4 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 e2e/e2etests/test_spl_withdraw_and_create_receiver_ata.go diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 0bc9ec916d..b42a52fef2 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -439,6 +439,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) { e2etests.TestSPLDepositName, e2etests.TestSPLDepositAndCallName, e2etests.TestSPLWithdrawName, + e2etests.TestSPLWithdrawAndCreateReceiverAtaName, e2etests.TestSolanaWhitelistSPLName, } eg.Go(solanaTestRoutine(conf, deployerRunner, verbose, solanaTests...)) diff --git a/contrib/localnet/solana/start-solana.sh b/contrib/localnet/solana/start-solana.sh index d87e9672ae..4c74bbce29 100644 --- a/contrib/localnet/solana/start-solana.sh +++ b/contrib/localnet/solana/start-solana.sh @@ -8,9 +8,10 @@ echo "starting solana test validator..." solana-test-validator & sleep 5 -# airdrop to e2e sol account +# airdrop to e2e sol account and rent payer solana airdrop 100 solana airdrop 100 37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ +solana airdrop 100 C6KPvGDYfNusoE4yfRP21F8wK35bxCBMT69xk4xo3X79 solana program deploy gateway.so diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index fc02bb7c5a..0c91eb73f3 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -55,15 +55,16 @@ const ( /* * Solana tests */ - TestSolanaDepositName = "solana_deposit" - TestSolanaWithdrawName = "solana_withdraw" - TestSolanaDepositAndCallName = "solana_deposit_and_call" - TestSolanaDepositAndCallRefundName = "solana_deposit_and_call_refund" - TestSolanaDepositRestrictedName = "solana_deposit_restricted" - TestSolanaWithdrawRestrictedName = "solana_withdraw_restricted" - TestSPLDepositName = "spl_deposit" - TestSPLDepositAndCallName = "spl_deposit_and_call" - TestSPLWithdrawName = "spl_withdraw" + TestSolanaDepositName = "solana_deposit" + TestSolanaWithdrawName = "solana_withdraw" + TestSolanaDepositAndCallName = "solana_deposit_and_call" + TestSolanaDepositAndCallRefundName = "solana_deposit_and_call_refund" + TestSolanaDepositRestrictedName = "solana_deposit_restricted" + TestSolanaWithdrawRestrictedName = "solana_withdraw_restricted" + TestSPLDepositName = "spl_deposit" + TestSPLDepositAndCallName = "spl_deposit_and_call" + TestSPLWithdrawName = "spl_withdraw" + TestSPLWithdrawAndCreateReceiverAtaName = "spl_withdraw_and_create_receiver_ata" /** * TON tests @@ -442,6 +443,14 @@ var AllE2ETests = []runner.E2ETest{ }, TestSPLWithdraw, ), + runner.NewE2ETest( + TestSPLWithdrawAndCreateReceiverAtaName, + "withdraw SPL from ZEVM and create receiver ata", + []runner.ArgDefinition{ + {Description: "amount in spl tokens", DefaultValue: "1000000"}, + }, + TestSPLWithdrawAndCreateReceiverAta, + ), runner.NewE2ETest( TestSolanaDepositAndCallRefundName, "deposit SOL into ZEVM and call a contract that reverts; should refund", diff --git a/e2e/e2etests/test_spl_withdraw_and_create_receiver_ata.go b/e2e/e2etests/test_spl_withdraw_and_create_receiver_ata.go new file mode 100644 index 0000000000..bcfdd44490 --- /dev/null +++ b/e2e/e2etests/test_spl_withdraw_and_create_receiver_ata.go @@ -0,0 +1,72 @@ +package e2etests + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/runner" +) + +// TestSPLWithdrawAndCreateReceiverAta withdraws spl, but letting gateway to create receiver ata using rent payer +// instead of providing receiver that has it already created +func TestSPLWithdrawAndCreateReceiverAta(r *runner.E2ERunner, args []string) { + require.Len(r, args, 1) + + withdrawAmount := parseBigInt(r, args[0]) + + // get SPL ZRC20 balance before withdraw + zrc20BalanceBefore, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) + require.NoError(r, err) + r.Logger.Info("runner balance of SPL before withdraw: %d", zrc20BalanceBefore) + + require.Equal(r, 1, zrc20BalanceBefore.Cmp(withdrawAmount), "Insufficient balance for withdrawal") + + // parse withdraw amount (in lamports), approve amount is 1 SOL + approvedAmount := new(big.Int).SetUint64(solana.LAMPORTS_PER_SOL) + require.Equal( + r, + -1, + withdrawAmount.Cmp(approvedAmount), + "Withdrawal amount must be less than the approved amount (1e9)", + ) + + // create new priv key, with empty ata + receiverPrivKey, err := solana.NewRandomPrivateKey() + require.NoError(r, err) + + // verify receiver ata account doesn't exist + receiverAta, _, err := solana.FindAssociatedTokenAddress(receiverPrivKey.PublicKey(), r.SPLAddr) + require.NoError(r, err) + + receiverAtaAcc, err := r.SolanaClient.GetAccountInfo(r.Ctx, receiverAta) + require.Error(r, err) + require.Nil(r, receiverAtaAcc) + + // withdraw + r.WithdrawSPLZRC20(receiverPrivKey.PublicKey(), withdrawAmount, approvedAmount) + + // get SPL ZRC20 balance after withdraw + zrc20BalanceAfter, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) + require.NoError(r, err) + r.Logger.Info("runner balance of SPL after withdraw: %d", zrc20BalanceAfter) + + // verify receiver ata was created + receiverAtaAcc, err = r.SolanaClient.GetAccountInfo(r.Ctx, receiverAta) + require.NoError(r, err) + require.NotNil(r, receiverAtaAcc) + + // verify balances are updated + receiverBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, receiverAta, rpc.CommitmentConfirmed) + require.NoError(r, err) + r.Logger.Info("receiver balance of SPL after withdraw: %s", receiverBalanceAfter.Value.Amount) + + // verify amount is added to receiver ata + require.Zero(r, withdrawAmount.Cmp(parseBigInt(r, receiverBalanceAfter.Value.Amount))) + + // verify amount is subtracted on zrc20 + require.Zero(r, new(big.Int).Sub(zrc20BalanceBefore, withdrawAmount).Cmp(zrc20BalanceAfter)) +} From 8bc512f42e8891cd25f52e7da38fd3b5718a9d99 Mon Sep 17 00:00:00 2001 From: skosito Date: Sun, 10 Nov 2024 21:29:01 +0100 Subject: [PATCH 05/10] comments fixes --- contrib/localnet/solana/start-solana.sh | 2 +- pkg/contracts/solana/gateway.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/localnet/solana/start-solana.sh b/contrib/localnet/solana/start-solana.sh index 4c74bbce29..73a4564f56 100644 --- a/contrib/localnet/solana/start-solana.sh +++ b/contrib/localnet/solana/start-solana.sh @@ -8,7 +8,7 @@ echo "starting solana test validator..." solana-test-validator & sleep 5 -# airdrop to e2e sol account and rent payer +# airdrop to e2e sol account and rent payer (used to generate atas for withdraw spl receivers if they don't exist) solana airdrop 100 solana airdrop 100 37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ solana airdrop 100 C6KPvGDYfNusoE4yfRP21F8wK35bxCBMT69xk4xo3X79 diff --git a/pkg/contracts/solana/gateway.go b/pkg/contracts/solana/gateway.go index d1cc628f93..34cbcb0261 100644 --- a/pkg/contracts/solana/gateway.go +++ b/pkg/contracts/solana/gateway.go @@ -60,7 +60,7 @@ func ParseGatewayIDAndPda(address string) (solana.PublicKey, solana.PublicKey, e return gatewayID, pda, err } -// ParseGatewayAddressAndPda parses the rent payer program derived address from the given string +// ParseRentPayerPda parses the rent payer program derived address from the given string func ParseRentPayerPda(address string) (solana.PublicKey, error) { var rentPayerPda solana.PublicKey From 79493b1ca3b8f5db57eeae2a2ecc4a920e39b045 Mon Sep 17 00:00:00 2001 From: skosito Date: Tue, 12 Nov 2024 14:22:03 +0100 Subject: [PATCH 06/10] PR comments fixes --- changelog.md | 2 +- e2e/e2etests/test_solana_whitelist_spl.go | 3 +- e2e/e2etests/test_solana_withdraw.go | 14 ++++++--- ...test_solana_withdraw_restricted_address.go | 8 ++++- e2e/e2etests/test_spl_deposit.go | 8 ++--- e2e/e2etests/test_spl_deposit_and_call.go | 8 ++--- e2e/e2etests/test_spl_withdraw.go | 26 ++++++++-------- ...st_spl_withdraw_and_create_receiver_ata.go | 15 ++++++--- e2e/runner/runner.go | 7 +++++ e2e/runner/solana.go | 31 +++++++------------ pkg/contracts/solana/gateway.go | 8 ++--- pkg/contracts/solana/gateway_message.go | 2 +- zetaclient/chains/solana/signer/signer.go | 3 +- zetaclient/chains/solana/signer/withdraw.go | 2 +- .../chains/solana/signer/withdraw_spl.go | 2 +- 15 files changed, 75 insertions(+), 64 deletions(-) diff --git a/changelog.md b/changelog.md index aa5502c43b..b6b27f5395 100644 --- a/changelog.md +++ b/changelog.md @@ -6,7 +6,7 @@ * [2984](https://github.com/zeta-chain/node/pull/2984) - add Whitelist message ability to whitelist SPL tokens on Solana * [3091](https://github.com/zeta-chain/node/pull/3091) - improve build reproducability. `make release{,-build-only}` checksums should now be stable. * [3124](https://github.com/zeta-chain/node/pull/3124) - integrate SPL deposits -* [3134](https://github.com/zeta-chain/node/pull/3134) - integrate withdraw SPL +* [3134](https://github.com/zeta-chain/node/pull/3134) - integrate SPL tokens withdraw to Solana ### Tests diff --git a/e2e/e2etests/test_solana_whitelist_spl.go b/e2e/e2etests/test_solana_whitelist_spl.go index fadff22805..c07bdabb12 100644 --- a/e2e/e2etests/test_solana_whitelist_spl.go +++ b/e2e/e2etests/test_solana_whitelist_spl.go @@ -16,8 +16,7 @@ func TestSolanaWhitelistSPL(r *runner.E2ERunner, _ []string) { r.Logger.Info("Deploying new SPL") // load deployer private key - privkey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String()) - require.NoError(r, err) + privkey := r.GetSolanaPrivKey() // deploy SPL token, but don't whitelist in gateway spl := r.DeploySPL(&privkey, false) diff --git a/e2e/e2etests/test_solana_withdraw.go b/e2e/e2etests/test_solana_withdraw.go index c7f6ccc58b..d8e427a66c 100644 --- a/e2e/e2etests/test_solana_withdraw.go +++ b/e2e/e2etests/test_solana_withdraw.go @@ -8,6 +8,8 @@ import ( "github.com/stretchr/testify/require" "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) func TestSolanaWithdraw(r *runner.E2ERunner, args []string) { @@ -28,15 +30,19 @@ func TestSolanaWithdraw(r *runner.E2ERunner, args []string) { r, -1, withdrawAmount.Cmp(approvedAmount), - "Withdrawal amount must be less than the approved amount (1e9)", + "Withdrawal amount must be less than the approved amount: %v", + approvedAmount, ) // load deployer private key - privkey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String()) - require.NoError(r, err) + privkey := r.GetSolanaPrivKey() // withdraw - r.WithdrawSOLZRC20(privkey.PublicKey(), withdrawAmount, approvedAmount) + tx := r.WithdrawSOLZRC20(privkey.PublicKey(), withdrawAmount, approvedAmount) + + // wait for the cctx to be mined + cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) // get ERC20 SOL balance after withdraw balanceAfter, err := r.SOLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) diff --git a/e2e/e2etests/test_solana_withdraw_restricted_address.go b/e2e/e2etests/test_solana_withdraw_restricted_address.go index e7964f3702..af2027ac98 100644 --- a/e2e/e2etests/test_solana_withdraw_restricted_address.go +++ b/e2e/e2etests/test_solana_withdraw_restricted_address.go @@ -8,7 +8,9 @@ import ( "github.com/stretchr/testify/require" "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/pkg/chains" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) func TestSolanaWithdrawRestricted(r *runner.E2ERunner, args []string) { @@ -29,7 +31,11 @@ func TestSolanaWithdrawRestricted(r *runner.E2ERunner, args []string) { ) // withdraw - cctx := r.WithdrawSOLZRC20(receiverRestricted, withdrawAmount, approvedAmount) + tx := r.WithdrawSOLZRC20(receiverRestricted, withdrawAmount, approvedAmount) + + // wait for the cctx to be mined + cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) // the cctx should be cancelled with zero value verifySolanaWithdrawalAmountFromCCTX(r, cctx, 0) diff --git a/e2e/e2etests/test_spl_deposit.go b/e2e/e2etests/test_spl_deposit.go index ee5013d16c..3d969fa8b2 100644 --- a/e2e/e2etests/test_spl_deposit.go +++ b/e2e/e2etests/test_spl_deposit.go @@ -4,7 +4,6 @@ import ( "math/big" "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" "github.com/stretchr/testify/require" @@ -18,17 +17,16 @@ func TestSPLDeposit(r *runner.E2ERunner, args []string) { amount := parseInt(r, args[0]) // load deployer private key - privKey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String()) - require.NoError(r, err) + privKey := r.GetSolanaPrivKey() // get SPL balance for pda and sender atas pda := r.ComputePdaAddress() - pdaAta := r.FindOrCreateAssociatedTokenAccount(privKey, pda, r.SPLAddr) + pdaAta := r.FindOrCreateAta(privKey, pda, r.SPLAddr) pdaBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentConfirmed) require.NoError(r, err) - senderAta := r.FindOrCreateAssociatedTokenAccount(privKey, privKey.PublicKey(), r.SPLAddr) + senderAta := r.FindOrCreateAta(privKey, privKey.PublicKey(), r.SPLAddr) senderBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, senderAta, rpc.CommitmentConfirmed) require.NoError(r, err) diff --git a/e2e/e2etests/test_spl_deposit_and_call.go b/e2e/e2etests/test_spl_deposit_and_call.go index cdfc94daa1..078a2ee210 100644 --- a/e2e/e2etests/test_spl_deposit_and_call.go +++ b/e2e/e2etests/test_spl_deposit_and_call.go @@ -4,7 +4,6 @@ import ( "math/big" "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" "github.com/stretchr/testify/require" @@ -24,17 +23,16 @@ func TestSPLDepositAndCall(r *runner.E2ERunner, args []string) { r.Logger.Info("Example contract deployed at: %s", contractAddr.String()) // load deployer private key - privKey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String()) - require.NoError(r, err) + privKey := r.GetSolanaPrivKey() // get SPL balance for pda and sender atas pda := r.ComputePdaAddress() - pdaAta := r.FindOrCreateAssociatedTokenAccount(privKey, pda, r.SPLAddr) + pdaAta := r.FindOrCreateAta(privKey, pda, r.SPLAddr) pdaBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentConfirmed) require.NoError(r, err) - senderAta := r.FindOrCreateAssociatedTokenAccount(privKey, privKey.PublicKey(), r.SPLAddr) + senderAta := r.FindOrCreateAta(privKey, privKey.PublicKey(), r.SPLAddr) senderBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, senderAta, rpc.CommitmentConfirmed) require.NoError(r, err) diff --git a/e2e/e2etests/test_spl_withdraw.go b/e2e/e2etests/test_spl_withdraw.go index c0e94e926c..8074b98279 100644 --- a/e2e/e2etests/test_spl_withdraw.go +++ b/e2e/e2etests/test_spl_withdraw.go @@ -9,6 +9,8 @@ import ( "github.com/stretchr/testify/require" "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) func TestSPLWithdraw(r *runner.E2ERunner, args []string) { @@ -29,21 +31,25 @@ func TestSPLWithdraw(r *runner.E2ERunner, args []string) { r, -1, withdrawAmount.Cmp(approvedAmount), - "Withdrawal amount must be less than the approved amount (1e9)", + "Withdrawal amount must be less than the %v", + approvedAmount, ) // load deployer private key - privkey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String()) - require.NoError(r, err) + privkey := r.GetSolanaPrivKey() // get receiver ata balance before withdraw - receiverAta := r.FindOrCreateAssociatedTokenAccount(privkey, privkey.PublicKey(), r.SPLAddr) + receiverAta := r.FindOrCreateAta(privkey, privkey.PublicKey(), r.SPLAddr) receiverBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, receiverAta, rpc.CommitmentConfirmed) require.NoError(r, err) r.Logger.Info("receiver balance of SPL before withdraw: %s", receiverBalanceBefore.Value.Amount) // withdraw - r.WithdrawSPLZRC20(privkey.PublicKey(), withdrawAmount, approvedAmount) + tx := r.WithdrawSPLZRC20(privkey.PublicKey(), withdrawAmount, approvedAmount) + + // wait for the cctx to be mined + cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) // get SPL ZRC20 balance after withdraw zrc20BalanceAfter, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) @@ -56,14 +62,8 @@ func TestSPLWithdraw(r *runner.E2ERunner, args []string) { r.Logger.Info("receiver balance of SPL after withdraw: %s", receiverBalanceAfter.Value.Amount) // verify amount is added to receiver ata - require.Zero( - r, - new( - big.Int, - ).Add(withdrawAmount, parseBigInt(r, receiverBalanceBefore.Value.Amount)). - Cmp(parseBigInt(r, receiverBalanceAfter.Value.Amount)), - ) + require.EqualValues(r, new(big.Int).Add(withdrawAmount, parseBigInt(r, receiverBalanceBefore.Value.Amount)).String(), parseBigInt(r, receiverBalanceAfter.Value.Amount).String()) // verify amount is subtracted on zrc20 - require.Zero(r, new(big.Int).Sub(zrc20BalanceBefore, withdrawAmount).Cmp(zrc20BalanceAfter)) + require.EqualValues(r, new(big.Int).Sub(zrc20BalanceBefore, withdrawAmount).String(), zrc20BalanceAfter.String()) } diff --git a/e2e/e2etests/test_spl_withdraw_and_create_receiver_ata.go b/e2e/e2etests/test_spl_withdraw_and_create_receiver_ata.go index bcfdd44490..e3de483380 100644 --- a/e2e/e2etests/test_spl_withdraw_and_create_receiver_ata.go +++ b/e2e/e2etests/test_spl_withdraw_and_create_receiver_ata.go @@ -9,6 +9,8 @@ import ( "github.com/stretchr/testify/require" "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) // TestSPLWithdrawAndCreateReceiverAta withdraws spl, but letting gateway to create receiver ata using rent payer @@ -31,7 +33,8 @@ func TestSPLWithdrawAndCreateReceiverAta(r *runner.E2ERunner, args []string) { r, -1, withdrawAmount.Cmp(approvedAmount), - "Withdrawal amount must be less than the approved amount (1e9)", + "Withdrawal amount must be less than the %v", + approvedAmount, ) // create new priv key, with empty ata @@ -47,7 +50,11 @@ func TestSPLWithdrawAndCreateReceiverAta(r *runner.E2ERunner, args []string) { require.Nil(r, receiverAtaAcc) // withdraw - r.WithdrawSPLZRC20(receiverPrivKey.PublicKey(), withdrawAmount, approvedAmount) + tx := r.WithdrawSPLZRC20(receiverPrivKey.PublicKey(), withdrawAmount, approvedAmount) + + // wait for the cctx to be mined + cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) // get SPL ZRC20 balance after withdraw zrc20BalanceAfter, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) @@ -65,8 +72,8 @@ func TestSPLWithdrawAndCreateReceiverAta(r *runner.E2ERunner, args []string) { r.Logger.Info("receiver balance of SPL after withdraw: %s", receiverBalanceAfter.Value.Amount) // verify amount is added to receiver ata - require.Zero(r, withdrawAmount.Cmp(parseBigInt(r, receiverBalanceAfter.Value.Amount))) + require.EqualValues(r, withdrawAmount.String(), parseBigInt(r, receiverBalanceAfter.Value.Amount).String()) // verify amount is subtracted on zrc20 - require.Zero(r, new(big.Int).Sub(zrc20BalanceBefore, withdrawAmount).Cmp(zrc20BalanceAfter)) + require.EqualValues(r, new(big.Int).Sub(zrc20BalanceBefore, withdrawAmount).String(), zrc20BalanceAfter.String()) } diff --git a/e2e/runner/runner.go b/e2e/runner/runner.go index ef68abe6a8..f117758c28 100644 --- a/e2e/runner/runner.go +++ b/e2e/runner/runner.go @@ -20,6 +20,7 @@ import ( "github.com/ethereum/go-ethereum/ethclient" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" + "github.com/stretchr/testify/require" "github.com/zeta-chain/protocol-contracts/v1/pkg/contracts/evm/erc20custody.sol" zetaeth "github.com/zeta-chain/protocol-contracts/v1/pkg/contracts/evm/zeta.eth.sol" zetaconnectoreth "github.com/zeta-chain/protocol-contracts/v1/pkg/contracts/evm/zetaconnector.eth.sol" @@ -434,3 +435,9 @@ func (r *E2ERunner) requireTxSuccessful(receipt *ethtypes.Receipt, msgAndArgs .. func (r *E2ERunner) EVMAddress() ethcommon.Address { return r.Account.EVMAddress() } + +func (r *E2ERunner) GetSolanaPrivKey() solana.PrivateKey { + privkey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String()) + require.NoError(r, err) + return privkey +} diff --git a/e2e/runner/solana.go b/e2e/runner/solana.go index 3d2f6c4776..22bf6482cc 100644 --- a/e2e/runner/solana.go +++ b/e2e/runner/solana.go @@ -13,9 +13,9 @@ import ( "github.com/near/borsh-go" "github.com/stretchr/testify/require" + ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/zeta-chain/node/e2e/utils" solanacontract "github.com/zeta-chain/node/pkg/contracts/solana" - crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) // ComputePdaAddress computes the PDA address for the gateway program @@ -159,8 +159,8 @@ func (r *E2ERunner) CreateSignedTransaction( return tx } -// FindOrCreateAssociatedTokenAccount checks if ata exists, and if not creates it -func (r *E2ERunner) FindOrCreateAssociatedTokenAccount( +// FindOrCreateAta checks if ata exists, and if not creates it +func (r *E2ERunner) FindOrCreateAta( payer solana.PrivateKey, owner solana.PublicKey, tokenAccount solana.PublicKey, @@ -196,10 +196,10 @@ func (r *E2ERunner) SPLDepositAndCall( ) solana.Signature { // ata for pda pda := r.ComputePdaAddress() - pdaAta := r.FindOrCreateAssociatedTokenAccount(*privateKey, pda, tokenAccount) + pdaAta := r.FindOrCreateAta(*privateKey, pda, tokenAccount) // deployer ata - ata := r.FindOrCreateAssociatedTokenAccount(*privateKey, privateKey.PublicKey(), tokenAccount) + ata := r.FindOrCreateAta(*privateKey, privateKey.PublicKey(), tokenAccount) // deposit spl seed := [][]byte{[]byte("whitelist"), tokenAccount.Bytes()} @@ -260,7 +260,7 @@ func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey, whitelist bool) *so r.Logger.Info("create spl logs: %v", out.Meta.LogMessages) // minting some tokens to deployer for testing - ata := r.FindOrCreateAssociatedTokenAccount(*privateKey, privateKey.PublicKey(), tokenAccount.PublicKey()) + ata := r.FindOrCreateAta(*privateKey, privateKey.PublicKey(), tokenAccount.PublicKey()) mintToInstruction := token.NewMintToInstruction(uint64(1_000_000_000), tokenAccount.PublicKey(), ata, privateKey.PublicKey(), []solana.PublicKey{}). Build() @@ -345,8 +345,7 @@ func (r *E2ERunner) SOLDepositAndCall( ) solana.Signature { // if signer is not provided, use the runner account as default if signerPrivKey == nil { - privkey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String()) - require.NoError(r, err) + privkey := r.GetSolanaPrivKey() signerPrivKey = &privkey } @@ -368,7 +367,7 @@ func (r *E2ERunner) WithdrawSOLZRC20( to solana.PublicKey, amount *big.Int, approveAmount *big.Int, -) *crosschaintypes.CrossChainTx { +) *ethtypes.Transaction { // approve tx, err := r.SOLZRC20.Approve(r.ZEVMAuth, r.SOLZRC20Addr, approveAmount) require.NoError(r, err) @@ -385,11 +384,7 @@ func (r *E2ERunner) WithdrawSOLZRC20( utils.RequireTxSuccessful(r, receipt, "withdraw") r.Logger.Info("Receipt txhash %s status %d", receipt.TxHash, receipt.Status) - // wait for the cctx to be mined - cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout) - utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) - - return cctx + return tx } // WithdrawSPLZRC20 withdraws an amount of ZRC20 SPL tokens @@ -397,7 +392,7 @@ func (r *E2ERunner) WithdrawSPLZRC20( to solana.PublicKey, amount *big.Int, approveAmount *big.Int, -) *crosschaintypes.CrossChainTx { +) *ethtypes.Transaction { // approve splzrc20 to spend gas tokens to pay gas fee tx, err := r.SOLZRC20.Approve(r.ZEVMAuth, r.SPLZRC20Addr, approveAmount) require.NoError(r, err) @@ -414,9 +409,5 @@ func (r *E2ERunner) WithdrawSPLZRC20( utils.RequireTxSuccessful(r, receipt, "withdraw") r.Logger.Info("Receipt txhash %s status %d", receipt.TxHash, receipt.Status) - // wait for the cctx to be mined - cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout) - utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) - - return cctx + return tx } diff --git a/pkg/contracts/solana/gateway.go b/pkg/contracts/solana/gateway.go index 34cbcb0261..a417abe5db 100644 --- a/pkg/contracts/solana/gateway.go +++ b/pkg/contracts/solana/gateway.go @@ -44,11 +44,11 @@ var ( ) // ParseGatewayAddressAndPda parses the gateway id and program derived address from the given string -func ParseGatewayIDAndPda(address string) (solana.PublicKey, solana.PublicKey, error) { +func ParseGatewayIDAndPda(gatewayAddress string) (solana.PublicKey, solana.PublicKey, error) { var gatewayID, pda solana.PublicKey // decode gateway address - gatewayID, err := solana.PublicKeyFromBase58(address) + gatewayID, err := solana.PublicKeyFromBase58(gatewayAddress) if err != nil { return gatewayID, pda, errors.Wrap(err, "unable to decode address") } @@ -61,11 +61,11 @@ func ParseGatewayIDAndPda(address string) (solana.PublicKey, solana.PublicKey, e } // ParseRentPayerPda parses the rent payer program derived address from the given string -func ParseRentPayerPda(address string) (solana.PublicKey, error) { +func ParseRentPayerPda(gatewayAddress string) (solana.PublicKey, error) { var rentPayerPda solana.PublicKey // decode gateway address - gatewayID, err := solana.PublicKeyFromBase58(address) + gatewayID, err := solana.PublicKeyFromBase58(gatewayAddress) if err != nil { return rentPayerPda, errors.Wrap(err, "unable to decode address") } diff --git a/pkg/contracts/solana/gateway_message.go b/pkg/contracts/solana/gateway_message.go index 4979c69ac0..bd8cbb51af 100644 --- a/pkg/contracts/solana/gateway_message.go +++ b/pkg/contracts/solana/gateway_message.go @@ -128,7 +128,7 @@ type MsgWithdrawSPL struct { // to is the recipient address for the withdraw_spl to solana.PublicKey - // recipientAta is the recipient address for the withdraw_spl + // recipientAta is the recipient associated token account for the withdraw_spl recipientAta solana.PublicKey // signature is the signature of the message diff --git a/zetaclient/chains/solana/signer/signer.go b/zetaclient/chains/solana/signer/signer.go index f70ddba677..70ad743662 100644 --- a/zetaclient/chains/solana/signer/signer.go +++ b/zetaclient/chains/solana/signer/signer.go @@ -6,7 +6,6 @@ import ( "strings" "cosmossdk.io/errors" - "github.com/davecgh/go-spew/spew" ethcommon "github.com/ethereum/go-ethereum/common" bin "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" @@ -70,6 +69,7 @@ func NewSigner( return nil, errors.Wrapf(err, "cannot parse gateway address %s", chainParams.GatewayAddress) } + // parse rent payer PDA, used in case receiver ATA should be created in gateway rentPayerPda, err := contracts.ParseRentPayerPda(chainParams.GatewayAddress) if err != nil { return nil, errors.Wrapf(err, "cannot parse gateway address %s", chainParams.GatewayAddress) @@ -340,7 +340,6 @@ func (signer *Signer) decodeMintAccountDetails(ctx context.Context, asset string if err != nil { return token.Mint{}, err } - spew.Dump(mint) return mint, nil } diff --git a/zetaclient/chains/solana/signer/withdraw.go b/zetaclient/chains/solana/signer/withdraw.go index c9c94d8702..e0ffdc14e2 100644 --- a/zetaclient/chains/solana/signer/withdraw.go +++ b/zetaclient/chains/solana/signer/withdraw.go @@ -26,7 +26,7 @@ func (signer *Signer) createAndSignMsgWithdraw( nonce := params.TssNonce amount := params.Amount.Uint64() - // zero out the amount if cancelTx is set. It's legal to withdraw 0 lamports thru the gateway. + // zero out the amount if cancelTx is set. It's legal to withdraw 0 lamports through the gateway. if cancelTx { amount = 0 } diff --git a/zetaclient/chains/solana/signer/withdraw_spl.go b/zetaclient/chains/solana/signer/withdraw_spl.go index ff0fd8a11e..fd95f1e7bc 100644 --- a/zetaclient/chains/solana/signer/withdraw_spl.go +++ b/zetaclient/chains/solana/signer/withdraw_spl.go @@ -28,7 +28,7 @@ func (signer *Signer) createAndSignMsgWithdrawSPL( nonce := params.TssNonce amount := params.Amount.Uint64() - // zero out the amount if cancelTx is set. It's legal to withdraw 0 spl thru the gateway. + // zero out the amount if cancelTx is set. It's legal to withdraw 0 spl through the gateway. if cancelTx { amount = 0 } From a80a7848fea1e2bd40ea3a22c9bffa391f011642 Mon Sep 17 00:00:00 2001 From: skosito Date: Tue, 12 Nov 2024 15:39:39 +0100 Subject: [PATCH 07/10] fmt --- e2e/e2etests/test_spl_withdraw.go | 6 +++++- e2e/runner/solana.go | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/e2e/e2etests/test_spl_withdraw.go b/e2e/e2etests/test_spl_withdraw.go index 8074b98279..639cbaa16e 100644 --- a/e2e/e2etests/test_spl_withdraw.go +++ b/e2e/e2etests/test_spl_withdraw.go @@ -62,7 +62,11 @@ func TestSPLWithdraw(r *runner.E2ERunner, args []string) { r.Logger.Info("receiver balance of SPL after withdraw: %s", receiverBalanceAfter.Value.Amount) // verify amount is added to receiver ata - require.EqualValues(r, new(big.Int).Add(withdrawAmount, parseBigInt(r, receiverBalanceBefore.Value.Amount)).String(), parseBigInt(r, receiverBalanceAfter.Value.Amount).String()) + require.EqualValues( + r, + new(big.Int).Add(withdrawAmount, parseBigInt(r, receiverBalanceBefore.Value.Amount)).String(), + parseBigInt(r, receiverBalanceAfter.Value.Amount).String(), + ) // verify amount is subtracted on zrc20 require.EqualValues(r, new(big.Int).Sub(zrc20BalanceBefore, withdrawAmount).String(), zrc20BalanceAfter.String()) diff --git a/e2e/runner/solana.go b/e2e/runner/solana.go index 22bf6482cc..9d5e68fd27 100644 --- a/e2e/runner/solana.go +++ b/e2e/runner/solana.go @@ -5,6 +5,7 @@ import ( "time" ethcommon "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/gagliardetto/solana-go" associatedtokenaccount "github.com/gagliardetto/solana-go/programs/associated-token-account" "github.com/gagliardetto/solana-go/programs/system" @@ -13,7 +14,6 @@ import ( "github.com/near/borsh-go" "github.com/stretchr/testify/require" - ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/zeta-chain/node/e2e/utils" solanacontract "github.com/zeta-chain/node/pkg/contracts/solana" ) From 0011b801fe5524af13823b59423b3c328c03eb62 Mon Sep 17 00:00:00 2001 From: skosito Date: Tue, 12 Nov 2024 17:08:53 +0100 Subject: [PATCH 08/10] PR comments --- e2e/e2etests/test_spl_deposit.go | 4 +- e2e/e2etests/test_spl_deposit_and_call.go | 4 +- e2e/e2etests/test_spl_withdraw.go | 2 +- e2e/runner/setup_solana.go | 2 +- e2e/runner/solana.go | 14 +-- pkg/contracts/solana/gateway.go | 16 +--- zetaclient/chains/solana/observer/observer.go | 2 +- zetaclient/chains/solana/signer/signer.go | 6 +- zetaclient/chains/solana/signer/whitelist.go | 53 +++-------- zetaclient/chains/solana/signer/withdraw.go | 41 +++------ .../chains/solana/signer/withdraw_spl.go | 91 ++++++------------- 11 files changed, 79 insertions(+), 156 deletions(-) diff --git a/e2e/e2etests/test_spl_deposit.go b/e2e/e2etests/test_spl_deposit.go index 3d969fa8b2..3c9862f57d 100644 --- a/e2e/e2etests/test_spl_deposit.go +++ b/e2e/e2etests/test_spl_deposit.go @@ -21,12 +21,12 @@ func TestSPLDeposit(r *runner.E2ERunner, args []string) { // get SPL balance for pda and sender atas pda := r.ComputePdaAddress() - pdaAta := r.FindOrCreateAta(privKey, pda, r.SPLAddr) + pdaAta := r.ResolveSolanaATA(privKey, pda, r.SPLAddr) pdaBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentConfirmed) require.NoError(r, err) - senderAta := r.FindOrCreateAta(privKey, privKey.PublicKey(), r.SPLAddr) + senderAta := r.ResolveSolanaATA(privKey, privKey.PublicKey(), r.SPLAddr) senderBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, senderAta, rpc.CommitmentConfirmed) require.NoError(r, err) diff --git a/e2e/e2etests/test_spl_deposit_and_call.go b/e2e/e2etests/test_spl_deposit_and_call.go index 078a2ee210..f136866cc6 100644 --- a/e2e/e2etests/test_spl_deposit_and_call.go +++ b/e2e/e2etests/test_spl_deposit_and_call.go @@ -27,12 +27,12 @@ func TestSPLDepositAndCall(r *runner.E2ERunner, args []string) { // get SPL balance for pda and sender atas pda := r.ComputePdaAddress() - pdaAta := r.FindOrCreateAta(privKey, pda, r.SPLAddr) + pdaAta := r.ResolveSolanaATA(privKey, pda, r.SPLAddr) pdaBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentConfirmed) require.NoError(r, err) - senderAta := r.FindOrCreateAta(privKey, privKey.PublicKey(), r.SPLAddr) + senderAta := r.ResolveSolanaATA(privKey, privKey.PublicKey(), r.SPLAddr) senderBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, senderAta, rpc.CommitmentConfirmed) require.NoError(r, err) diff --git a/e2e/e2etests/test_spl_withdraw.go b/e2e/e2etests/test_spl_withdraw.go index 639cbaa16e..7ee87b68d3 100644 --- a/e2e/e2etests/test_spl_withdraw.go +++ b/e2e/e2etests/test_spl_withdraw.go @@ -39,7 +39,7 @@ func TestSPLWithdraw(r *runner.E2ERunner, args []string) { privkey := r.GetSolanaPrivKey() // get receiver ata balance before withdraw - receiverAta := r.FindOrCreateAta(privkey, privkey.PublicKey(), r.SPLAddr) + receiverAta := r.ResolveSolanaATA(privkey, privkey.PublicKey(), r.SPLAddr) receiverBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, receiverAta, rpc.CommitmentConfirmed) require.NoError(r, err) r.Logger.Info("receiver balance of SPL before withdraw: %s", receiverBalanceBefore.Value.Amount) diff --git a/e2e/runner/setup_solana.go b/e2e/runner/setup_solana.go index c6738c4e3d..1a46326d02 100644 --- a/e2e/runner/setup_solana.go +++ b/e2e/runner/setup_solana.go @@ -87,7 +87,7 @@ func (r *E2ERunner) SetupSolana(deployerPrivateKey string) { // initialize rent payer var instRentPayer solana.GenericInstruction - rentPayerPdaComputed := r.ComputeRentPayerPdaAddress() + rentPayerPdaComputed := r.SolanaRentPayerPDA() // create 'initialize_rent_payer' instruction accountSlice = []*solana.AccountMeta{} diff --git a/e2e/runner/solana.go b/e2e/runner/solana.go index 9d5e68fd27..d395c60d76 100644 --- a/e2e/runner/solana.go +++ b/e2e/runner/solana.go @@ -30,8 +30,8 @@ func (r *E2ERunner) ComputePdaAddress() solana.PublicKey { return pdaComputed } -// ComputePdaAddress computes the rent payer PDA address for the gateway program -func (r *E2ERunner) ComputeRentPayerPdaAddress() solana.PublicKey { +// SolanaRentPayerPDA computes the rent payer PDA (Program Derived Address) address for the gateway program +func (r *E2ERunner) SolanaRentPayerPDA() solana.PublicKey { seed := []byte(solanacontract.RentPayerPDASeed) GatewayProgramID := solana.MustPublicKeyFromBase58(solanacontract.SolanaGatewayProgramID) pdaComputed, bump, err := solana.FindProgramAddress([][]byte{seed}, GatewayProgramID) @@ -159,8 +159,8 @@ func (r *E2ERunner) CreateSignedTransaction( return tx } -// FindOrCreateAta checks if ata exists, and if not creates it -func (r *E2ERunner) FindOrCreateAta( +// ResolveSolanaATA finds or creates SOL associated token account +func (r *E2ERunner) ResolveSolanaATA( payer solana.PrivateKey, owner solana.PublicKey, tokenAccount solana.PublicKey, @@ -196,10 +196,10 @@ func (r *E2ERunner) SPLDepositAndCall( ) solana.Signature { // ata for pda pda := r.ComputePdaAddress() - pdaAta := r.FindOrCreateAta(*privateKey, pda, tokenAccount) + pdaAta := r.ResolveSolanaATA(*privateKey, pda, tokenAccount) // deployer ata - ata := r.FindOrCreateAta(*privateKey, privateKey.PublicKey(), tokenAccount) + ata := r.ResolveSolanaATA(*privateKey, privateKey.PublicKey(), tokenAccount) // deposit spl seed := [][]byte{[]byte("whitelist"), tokenAccount.Bytes()} @@ -260,7 +260,7 @@ func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey, whitelist bool) *so r.Logger.Info("create spl logs: %v", out.Meta.LogMessages) // minting some tokens to deployer for testing - ata := r.FindOrCreateAta(*privateKey, privateKey.PublicKey(), tokenAccount.PublicKey()) + ata := r.ResolveSolanaATA(*privateKey, privateKey.PublicKey(), tokenAccount.PublicKey()) mintToInstruction := token.NewMintToInstruction(uint64(1_000_000_000), tokenAccount.PublicKey(), ata, privateKey.PublicKey(), []solana.PublicKey{}). Build() diff --git a/pkg/contracts/solana/gateway.go b/pkg/contracts/solana/gateway.go index a417abe5db..bf674aa081 100644 --- a/pkg/contracts/solana/gateway.go +++ b/pkg/contracts/solana/gateway.go @@ -43,8 +43,8 @@ var ( DiscriminatorWhitelistSplMint = idlgateway.IDLGateway.GetDiscriminator("whitelist_spl_mint") ) -// ParseGatewayAddressAndPda parses the gateway id and program derived address from the given string -func ParseGatewayIDAndPda(gatewayAddress string) (solana.PublicKey, solana.PublicKey, error) { +// ParseGatewayWithPDA parses the gateway id and program derived address from the given string +func ParseGatewayWithPDA(gatewayAddress string) (solana.PublicKey, solana.PublicKey, error) { var gatewayID, pda solana.PublicKey // decode gateway address @@ -61,18 +61,10 @@ func ParseGatewayIDAndPda(gatewayAddress string) (solana.PublicKey, solana.Publi } // ParseRentPayerPda parses the rent payer program derived address from the given string -func ParseRentPayerPda(gatewayAddress string) (solana.PublicKey, error) { +func RentPayerPDA(gateway solana.PublicKey) (solana.PublicKey, error) { var rentPayerPda solana.PublicKey - - // decode gateway address - gatewayID, err := solana.PublicKeyFromBase58(gatewayAddress) - if err != nil { - return rentPayerPda, errors.Wrap(err, "unable to decode address") - } - - // compute gateway PDA seed := []byte(RentPayerPDASeed) - rentPayerPda, _, err = solana.FindProgramAddress([][]byte{seed}, gatewayID) + rentPayerPda, _, err := solana.FindProgramAddress([][]byte{seed}, gateway) return rentPayerPda, err } diff --git a/zetaclient/chains/solana/observer/observer.go b/zetaclient/chains/solana/observer/observer.go index 0548fcd6d3..6187ca2c39 100644 --- a/zetaclient/chains/solana/observer/observer.go +++ b/zetaclient/chains/solana/observer/observer.go @@ -67,7 +67,7 @@ func NewObserver( } // parse gateway ID and PDA - gatewayID, pda, err := contracts.ParseGatewayIDAndPda(chainParams.GatewayAddress) + gatewayID, pda, err := contracts.ParseGatewayWithPDA(chainParams.GatewayAddress) if err != nil { return nil, errors.Wrapf(err, "cannot parse gateway address %s", chainParams.GatewayAddress) } diff --git a/zetaclient/chains/solana/signer/signer.go b/zetaclient/chains/solana/signer/signer.go index 70ad743662..c3a723ec87 100644 --- a/zetaclient/chains/solana/signer/signer.go +++ b/zetaclient/chains/solana/signer/signer.go @@ -64,13 +64,13 @@ func NewSigner( baseSigner := base.NewSigner(chain, tss, ts, logger) // parse gateway ID and PDA - gatewayID, pda, err := contracts.ParseGatewayIDAndPda(chainParams.GatewayAddress) + gatewayID, pda, err := contracts.ParseGatewayWithPDA(chainParams.GatewayAddress) if err != nil { return nil, errors.Wrapf(err, "cannot parse gateway address %s", chainParams.GatewayAddress) } // parse rent payer PDA, used in case receiver ATA should be created in gateway - rentPayerPda, err := contracts.ParseRentPayerPda(chainParams.GatewayAddress) + rentPayerPda, err := contracts.RentPayerPDA(gatewayID) if err != nil { return nil, errors.Wrapf(err, "cannot parse gateway address %s", chainParams.GatewayAddress) } @@ -347,7 +347,7 @@ func (signer *Signer) decodeMintAccountDetails(ctx context.Context, asset string // SetGatewayAddress sets the gateway address func (signer *Signer) SetGatewayAddress(address string) { // parse gateway ID and PDA - gatewayID, pda, err := contracts.ParseGatewayIDAndPda(address) + gatewayID, pda, err := contracts.ParseGatewayWithPDA(address) if err != nil { signer.Logger().Std.Error().Err(err).Msgf("cannot parse gateway address: %s", address) return diff --git a/zetaclient/chains/solana/signer/whitelist.go b/zetaclient/chains/solana/signer/whitelist.go index 73ee769039..6d9055adc7 100644 --- a/zetaclient/chains/solana/signer/whitelist.go +++ b/zetaclient/chains/solana/signer/whitelist.go @@ -35,7 +35,6 @@ func (signer *Signer) createAndSignMsgWhitelist( if err != nil { return nil, errors.Wrap(err, "Key-sign failed") } - signer.Logger().Std.Info().Msgf("Key-sign succeed for chain %d nonce %d", chainID, nonce) // attach the signature and return return msg.SetSignature(signature), nil @@ -44,9 +43,7 @@ func (signer *Signer) createAndSignMsgWhitelist( // signWhitelistTx wraps the whitelist 'msg' into a Solana transaction and signs it with the relayer key. func (signer *Signer) signWhitelistTx(ctx context.Context, msg *contracts.MsgWhitelist) (*solana.Transaction, error) { // create whitelist_spl_mint instruction with program call data - var err error - var inst solana.GenericInstruction - inst.DataBytes, err = borsh.Serialize(contracts.WhitelistInstructionParams{ + dataBytes, err := borsh.Serialize(contracts.WhitelistInstructionParams{ Discriminator: contracts.DiscriminatorWhitelistSplMint, Signature: msg.SigRS(), RecoveryID: msg.SigV(), @@ -57,16 +54,17 @@ func (signer *Signer) signWhitelistTx(ctx context.Context, msg *contracts.MsgWhi return nil, errors.Wrap(err, "cannot serialize whitelist_spl_mint instruction") } - // attach required accounts to the instruction - privkey := signer.relayerKey - attachWhitelistAccounts( - &inst, - privkey.PublicKey(), - signer.pda, - msg.WhitelistCandidate(), - msg.WhitelistEntry(), - signer.gatewayID, - ) + inst := solana.GenericInstruction{ + ProgID: signer.gatewayID, + DataBytes: dataBytes, + AccountValues: []*solana.AccountMeta{ + solana.Meta(msg.WhitelistEntry()).WRITE(), + solana.Meta(msg.WhitelistCandidate()), + solana.Meta(signer.pda).WRITE(), + solana.Meta(signer.relayerKey.PublicKey()).WRITE().SIGNER(), + solana.Meta(solana.SystemProgramID), + }, + } // get a recent blockhash recent, err := signer.client.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) @@ -83,7 +81,7 @@ func (signer *Signer) signWhitelistTx(ctx context.Context, msg *contracts.MsgWhi // programs.ComputeBudgetSetComputeUnitPrice(computeUnitPrice), &inst}, recent.Value.Blockhash, - solana.TransactionPayer(privkey.PublicKey()), + solana.TransactionPayer(signer.relayerKey.PublicKey()), ) if err != nil { return nil, errors.Wrap(err, "NewTransaction error") @@ -91,8 +89,8 @@ func (signer *Signer) signWhitelistTx(ctx context.Context, msg *contracts.MsgWhi // relayer signs the transaction _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey { - if key.Equals(privkey.PublicKey()) { - return privkey + if key.Equals(signer.relayerKey.PublicKey()) { + return signer.relayerKey } return nil }) @@ -102,24 +100,3 @@ func (signer *Signer) signWhitelistTx(ctx context.Context, msg *contracts.MsgWhi return tx, nil } - -// attachWhitelistAccounts attaches the required accounts for the gateway whitelist instruction. -func attachWhitelistAccounts( - inst *solana.GenericInstruction, - signer solana.PublicKey, - pda solana.PublicKey, - whitelistCandidate solana.PublicKey, - whitelistEntry solana.PublicKey, - gatewayID solana.PublicKey, -) { - // attach required accounts to the instruction - var accountSlice []*solana.AccountMeta - accountSlice = append(accountSlice, solana.Meta(whitelistEntry).WRITE()) - accountSlice = append(accountSlice, solana.Meta(whitelistCandidate)) - accountSlice = append(accountSlice, solana.Meta(pda).WRITE()) - accountSlice = append(accountSlice, solana.Meta(signer).WRITE().SIGNER()) - accountSlice = append(accountSlice, solana.Meta(solana.SystemProgramID)) - inst.ProgID = gatewayID - - inst.AccountValues = accountSlice -} diff --git a/zetaclient/chains/solana/signer/withdraw.go b/zetaclient/chains/solana/signer/withdraw.go index e0ffdc14e2..5a27095f6f 100644 --- a/zetaclient/chains/solana/signer/withdraw.go +++ b/zetaclient/chains/solana/signer/withdraw.go @@ -47,7 +47,6 @@ func (signer *Signer) createAndSignMsgWithdraw( if err != nil { return nil, errors.Wrap(err, "Key-sign failed") } - signer.Logger().Std.Info().Msgf("Key-sign succeed for chain %d nonce %d", chainID, nonce) // attach the signature and return return msg.SetSignature(signature), nil @@ -56,9 +55,7 @@ func (signer *Signer) createAndSignMsgWithdraw( // signWithdrawTx wraps the withdraw 'msg' into a Solana transaction and signs it with the relayer key. func (signer *Signer) signWithdrawTx(ctx context.Context, msg contracts.MsgWithdraw) (*solana.Transaction, error) { // create withdraw instruction with program call data - var err error - var inst solana.GenericInstruction - inst.DataBytes, err = borsh.Serialize(contracts.WithdrawInstructionParams{ + dataBytes, err := borsh.Serialize(contracts.WithdrawInstructionParams{ Discriminator: contracts.DiscriminatorWithdraw, Amount: msg.Amount(), Signature: msg.SigRS(), @@ -70,9 +67,15 @@ func (signer *Signer) signWithdrawTx(ctx context.Context, msg contracts.MsgWithd return nil, errors.Wrap(err, "cannot serialize withdraw instruction") } - // attach required accounts to the instruction - privkey := signer.relayerKey - attachWithdrawAccounts(&inst, privkey.PublicKey(), signer.pda, msg.To(), signer.gatewayID) + inst := solana.GenericInstruction{ + ProgID: signer.gatewayID, + DataBytes: dataBytes, + AccountValues: []*solana.AccountMeta{ + solana.Meta(signer.relayerKey.PublicKey()).WRITE().SIGNER(), + solana.Meta(signer.pda).WRITE(), + solana.Meta(msg.To()).WRITE(), + }, + } // get a recent blockhash recent, err := signer.client.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) @@ -89,7 +92,7 @@ func (signer *Signer) signWithdrawTx(ctx context.Context, msg contracts.MsgWithd // programs.ComputeBudgetSetComputeUnitPrice(computeUnitPrice), &inst}, recent.Value.Blockhash, - solana.TransactionPayer(privkey.PublicKey()), + solana.TransactionPayer(signer.relayerKey.PublicKey()), ) if err != nil { return nil, errors.Wrap(err, "NewTransaction error") @@ -97,8 +100,8 @@ func (signer *Signer) signWithdrawTx(ctx context.Context, msg contracts.MsgWithd // relayer signs the transaction _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey { - if key.Equals(privkey.PublicKey()) { - return privkey + if key.Equals(signer.relayerKey.PublicKey()) { + return signer.relayerKey } return nil }) @@ -108,21 +111,3 @@ func (signer *Signer) signWithdrawTx(ctx context.Context, msg contracts.MsgWithd return tx, nil } - -// attachWithdrawAccounts attaches the required accounts for the gateway withdraw instruction. -func attachWithdrawAccounts( - inst *solana.GenericInstruction, - signer solana.PublicKey, - pda solana.PublicKey, - to solana.PublicKey, - gatewayID solana.PublicKey, -) { - // attach required accounts to the instruction - var accountSlice []*solana.AccountMeta - accountSlice = append(accountSlice, solana.Meta(signer).WRITE().SIGNER()) - accountSlice = append(accountSlice, solana.Meta(pda).WRITE()) - accountSlice = append(accountSlice, solana.Meta(to).WRITE()) - inst.ProgID = gatewayID - - inst.AccountValues = accountSlice -} diff --git a/zetaclient/chains/solana/signer/withdraw_spl.go b/zetaclient/chains/solana/signer/withdraw_spl.go index fd95f1e7bc..bf03260eca 100644 --- a/zetaclient/chains/solana/signer/withdraw_spl.go +++ b/zetaclient/chains/solana/signer/withdraw_spl.go @@ -42,13 +42,13 @@ func (signer *Signer) createAndSignMsgWithdrawSPL( // parse token account tokenAccount, err := solana.PublicKeyFromBase58(asset) if err != nil { - return nil, err + return nil, errors.Wrapf(err, "cannot parse asset public key %s", asset) } // get recipient ata recipientAta, _, err := solana.FindAssociatedTokenAddress(to, tokenAccount) if err != nil { - return nil, err + return nil, errors.Wrapf(err, "cannot find ATA for %s and token account %s", to, tokenAccount) } // prepare withdraw spl msg and compute hash @@ -61,7 +61,6 @@ func (signer *Signer) createAndSignMsgWithdrawSPL( if err != nil { return nil, errors.Wrap(err, "Key-sign failed") } - signer.Logger().Std.Info().Msgf("Key-sign succeed for chain %d nonce %d", chainID, nonce) // attach the signature and return return msg.SetSignature(signature), nil @@ -73,9 +72,7 @@ func (signer *Signer) signWithdrawSPLTx( msg contracts.MsgWithdrawSPL, ) (*solana.Transaction, error) { // create withdraw spl instruction with program call data - var err error - var inst solana.GenericInstruction - inst.DataBytes, err = borsh.Serialize(contracts.WithdrawSPLInstructionParams{ + dataBytes, err := borsh.Serialize(contracts.WithdrawSPLInstructionParams{ Discriminator: contracts.DiscriminatorWithdrawSPL, Decimals: msg.Decimals(), Amount: msg.Amount(), @@ -88,19 +85,31 @@ func (signer *Signer) signWithdrawSPLTx( return nil, errors.Wrap(err, "cannot serialize withdraw instruction") } - // attach required accounts to the instruction - privkey := signer.relayerKey - err = attachWithdrawSPLAccounts( - &inst, - privkey.PublicKey(), - signer.pda, - signer.rentPayerPda, - msg.TokenAccount(), - msg.To(), - signer.gatewayID, - ) + pdaAta, _, err := solana.FindAssociatedTokenAddress(signer.pda, msg.TokenAccount()) if err != nil { - return nil, err + return nil, errors.Wrapf(err, "cannot find ATA for %s and token account %s", signer.pda, msg.TokenAccount()) + } + + recipientAta, _, err := solana.FindAssociatedTokenAddress(msg.To(), msg.TokenAccount()) + if err != nil { + return nil, errors.Wrapf(err, "cannot find ATA for %s and token account %s", msg.To(), msg.TokenAccount()) + } + + inst := solana.GenericInstruction{ + ProgID: signer.gatewayID, + DataBytes: dataBytes, + AccountValues: []*solana.AccountMeta{ + solana.Meta(signer.relayerKey.PublicKey()).WRITE().SIGNER(), + solana.Meta(signer.pda).WRITE(), + solana.Meta(pdaAta).WRITE(), + solana.Meta(msg.TokenAccount()), + solana.Meta(msg.To()), + solana.Meta(recipientAta).WRITE(), + solana.Meta(signer.rentPayerPda).WRITE(), + solana.Meta(solana.TokenProgramID), + solana.Meta(solana.SPLAssociatedTokenAccountProgramID), + solana.Meta(solana.SystemProgramID), + }, } // get a recent blockhash recent, err := signer.client.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) @@ -117,7 +126,7 @@ func (signer *Signer) signWithdrawSPLTx( // programs.ComputeBudgetSetComputeUnitPrice(computeUnitPrice), &inst}, recent.Value.Blockhash, - solana.TransactionPayer(privkey.PublicKey()), + solana.TransactionPayer(signer.relayerKey.PublicKey()), ) if err != nil { return nil, errors.Wrap(err, "NewTransaction error") @@ -125,8 +134,8 @@ func (signer *Signer) signWithdrawSPLTx( // relayer signs the transaction _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey { - if key.Equals(privkey.PublicKey()) { - return privkey + if key.Equals(signer.relayerKey.PublicKey()) { + return signer.relayerKey } return nil }) @@ -136,43 +145,3 @@ func (signer *Signer) signWithdrawSPLTx( return tx, nil } - -// attachWithdrawSPLAccounts attaches the required accounts for the gateway withdraw spl instruction. -func attachWithdrawSPLAccounts( - inst *solana.GenericInstruction, - signer solana.PublicKey, - pda solana.PublicKey, - rentPayerPda solana.PublicKey, - tokenAccount solana.PublicKey, - to solana.PublicKey, - gatewayID solana.PublicKey, -) error { - // attach required accounts to the instruction - pdaAta, _, err := solana.FindAssociatedTokenAddress(pda, tokenAccount) - if err != nil { - return err - } - - recipientAta, _, err := solana.FindAssociatedTokenAddress(to, tokenAccount) - if err != nil { - return err - } - - var accountSlice []*solana.AccountMeta - accountSlice = append(accountSlice, solana.Meta(signer).WRITE().SIGNER()) - accountSlice = append(accountSlice, solana.Meta(pda).WRITE()) - accountSlice = append(accountSlice, solana.Meta(pdaAta).WRITE()) - accountSlice = append(accountSlice, solana.Meta(tokenAccount)) - accountSlice = append(accountSlice, solana.Meta(to)) - accountSlice = append(accountSlice, solana.Meta(recipientAta).WRITE()) - accountSlice = append(accountSlice, solana.Meta(rentPayerPda).WRITE()) - accountSlice = append(accountSlice, solana.Meta(solana.TokenProgramID)) - accountSlice = append(accountSlice, solana.Meta(solana.SPLAssociatedTokenAccountProgramID)) - accountSlice = append(accountSlice, solana.Meta(solana.SystemProgramID)) - - inst.ProgID = gatewayID - - inst.AccountValues = accountSlice - - return nil -} From bf24380cc932bce71353dc77d98f31fafb7948df Mon Sep 17 00:00:00 2001 From: skosito Date: Tue, 12 Nov 2024 17:35:21 +0100 Subject: [PATCH 09/10] PR comments --- e2e/e2etests/test_spl_deposit.go | 8 ++++---- e2e/e2etests/test_spl_deposit_and_call.go | 8 ++++---- e2e/e2etests/test_spl_withdraw.go | 4 ++-- .../test_spl_withdraw_and_create_receiver_ata.go | 2 +- zetaclient/chains/solana/signer/signer.go | 11 ----------- 5 files changed, 11 insertions(+), 22 deletions(-) diff --git a/e2e/e2etests/test_spl_deposit.go b/e2e/e2etests/test_spl_deposit.go index 3c9862f57d..e20ff5879a 100644 --- a/e2e/e2etests/test_spl_deposit.go +++ b/e2e/e2etests/test_spl_deposit.go @@ -23,11 +23,11 @@ func TestSPLDeposit(r *runner.E2ERunner, args []string) { pda := r.ComputePdaAddress() pdaAta := r.ResolveSolanaATA(privKey, pda, r.SPLAddr) - pdaBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentConfirmed) + pdaBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentFinalized) require.NoError(r, err) senderAta := r.ResolveSolanaATA(privKey, privKey.PublicKey(), r.SPLAddr) - senderBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, senderAta, rpc.CommitmentConfirmed) + senderBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, senderAta, rpc.CommitmentFinalized) require.NoError(r, err) // get zrc20 balance for recipient @@ -44,10 +44,10 @@ func TestSPLDeposit(r *runner.E2ERunner, args []string) { utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) // verify balances are updated - pdaBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentConfirmed) + pdaBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentFinalized) require.NoError(r, err) - senderBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, senderAta, rpc.CommitmentConfirmed) + senderBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, senderAta, rpc.CommitmentFinalized) require.NoError(r, err) zrc20BalanceAfter, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) diff --git a/e2e/e2etests/test_spl_deposit_and_call.go b/e2e/e2etests/test_spl_deposit_and_call.go index f136866cc6..d7e11cd999 100644 --- a/e2e/e2etests/test_spl_deposit_and_call.go +++ b/e2e/e2etests/test_spl_deposit_and_call.go @@ -29,11 +29,11 @@ func TestSPLDepositAndCall(r *runner.E2ERunner, args []string) { pda := r.ComputePdaAddress() pdaAta := r.ResolveSolanaATA(privKey, pda, r.SPLAddr) - pdaBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentConfirmed) + pdaBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentFinalized) require.NoError(r, err) senderAta := r.ResolveSolanaATA(privKey, privKey.PublicKey(), r.SPLAddr) - senderBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, senderAta, rpc.CommitmentConfirmed) + senderBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, senderAta, rpc.CommitmentFinalized) require.NoError(r, err) // get zrc20 balance for recipient @@ -54,10 +54,10 @@ func TestSPLDepositAndCall(r *runner.E2ERunner, args []string) { utils.MustHaveCalledExampleContract(r, contract, big.NewInt(int64(amount))) // verify balances are updated - pdaBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentConfirmed) + pdaBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentFinalized) require.NoError(r, err) - senderBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, senderAta, rpc.CommitmentConfirmed) + senderBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, senderAta, rpc.CommitmentFinalized) require.NoError(r, err) zrc20BalanceAfter, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, contractAddr) diff --git a/e2e/e2etests/test_spl_withdraw.go b/e2e/e2etests/test_spl_withdraw.go index 7ee87b68d3..2af4ddfd94 100644 --- a/e2e/e2etests/test_spl_withdraw.go +++ b/e2e/e2etests/test_spl_withdraw.go @@ -40,7 +40,7 @@ func TestSPLWithdraw(r *runner.E2ERunner, args []string) { // get receiver ata balance before withdraw receiverAta := r.ResolveSolanaATA(privkey, privkey.PublicKey(), r.SPLAddr) - receiverBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, receiverAta, rpc.CommitmentConfirmed) + receiverBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, receiverAta, rpc.CommitmentFinalized) require.NoError(r, err) r.Logger.Info("receiver balance of SPL before withdraw: %s", receiverBalanceBefore.Value.Amount) @@ -57,7 +57,7 @@ func TestSPLWithdraw(r *runner.E2ERunner, args []string) { r.Logger.Info("runner balance of SPL after withdraw: %d", zrc20BalanceAfter) // verify balances are updated - receiverBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, receiverAta, rpc.CommitmentConfirmed) + receiverBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, receiverAta, rpc.CommitmentFinalized) require.NoError(r, err) r.Logger.Info("receiver balance of SPL after withdraw: %s", receiverBalanceAfter.Value.Amount) diff --git a/e2e/e2etests/test_spl_withdraw_and_create_receiver_ata.go b/e2e/e2etests/test_spl_withdraw_and_create_receiver_ata.go index e3de483380..51196c8a46 100644 --- a/e2e/e2etests/test_spl_withdraw_and_create_receiver_ata.go +++ b/e2e/e2etests/test_spl_withdraw_and_create_receiver_ata.go @@ -67,7 +67,7 @@ func TestSPLWithdrawAndCreateReceiverAta(r *runner.E2ERunner, args []string) { require.NotNil(r, receiverAtaAcc) // verify balances are updated - receiverBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, receiverAta, rpc.CommitmentConfirmed) + receiverBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, receiverAta, rpc.CommitmentFinalized) require.NoError(r, err) r.Logger.Info("receiver balance of SPL after withdraw: %s", receiverBalanceAfter.Value.Amount) diff --git a/zetaclient/chains/solana/signer/signer.go b/zetaclient/chains/solana/signer/signer.go index 05c27d737b..627b47ecf0 100644 --- a/zetaclient/chains/solana/signer/signer.go +++ b/zetaclient/chains/solana/signer/signer.go @@ -355,17 +355,6 @@ func (signer *Signer) SetGatewayAddress(address string) { gatewayID, pda, err := contracts.ParseGatewayWithPDA(address) if err != nil { signer.Logger().Std.Error().Err(err).Msgf("cannot parse gateway address: %s", address) - - // parse gateway ID and PDA - gatewayID, pda, err := contracts.ParseGatewayIDAndPda(address) - if err != nil { - signer.Logger().Std.Error().Err(err).Str("address", address).Msgf("Unable to parse gateway address") - return - } - - // noop - if signer.gatewayID.Equals(gatewayID) { - return } signer.Logger().Std.Info(). From e58778fa1ebc1e768fdc97676613541d1ade8644 Mon Sep 17 00:00:00 2001 From: skosito Date: Tue, 12 Nov 2024 17:53:26 +0100 Subject: [PATCH 10/10] fix unit test --- zetaclient/chains/solana/signer/signer.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/zetaclient/chains/solana/signer/signer.go b/zetaclient/chains/solana/signer/signer.go index 627b47ecf0..7405ddaf87 100644 --- a/zetaclient/chains/solana/signer/signer.go +++ b/zetaclient/chains/solana/signer/signer.go @@ -355,6 +355,12 @@ func (signer *Signer) SetGatewayAddress(address string) { gatewayID, pda, err := contracts.ParseGatewayWithPDA(address) if err != nil { signer.Logger().Std.Error().Err(err).Msgf("cannot parse gateway address: %s", address) + return + } + + // noop + if signer.gatewayID.Equals(gatewayID) { + return } signer.Logger().Std.Info().