From 6fd03f282c497a68c0f919584850c6498afaab9c Mon Sep 17 00:00:00 2001 From: skosito Date: Wed, 6 Nov 2024 05:07:56 +0100 Subject: [PATCH 01/13] deposit spl integration and start with e2e test --- cmd/zetae2e/config/config.go | 4 + cmd/zetae2e/config/contracts.go | 19 ++ cmd/zetae2e/local/local.go | 27 +-- e2e/config/config.go | 5 +- e2e/e2etests/e2etests.go | 9 + e2e/e2etests/test_solana_deposit_spl.go | 20 ++ e2e/e2etests/test_solana_whitelist_spl.go | 2 +- e2e/runner/runner.go | 7 + e2e/runner/setup_solana.go | 5 + e2e/runner/setup_zeta.go | 8 +- e2e/runner/solana.go | 195 ++++++++++++++++++- e2e/txserver/zeta_tx_server.go | 44 +++-- pkg/contracts/solana/gateway.go | 2 + pkg/contracts/solana/instruction.go | 12 ++ x/fungible/keeper/foreign_coins.go | 13 +- zetaclient/chains/solana/observer/inbound.go | 61 +++++- 16 files changed, 391 insertions(+), 42 deletions(-) create mode 100644 e2e/e2etests/test_solana_deposit_spl.go diff --git a/cmd/zetae2e/config/config.go b/cmd/zetae2e/config/config.go index 0cea78af39..32446a4799 100644 --- a/cmd/zetae2e/config/config.go +++ b/cmd/zetae2e/config/config.go @@ -63,11 +63,15 @@ func ExportContractsFromRunner(r *runner.E2ERunner, conf config.Config) config.C conf.Contracts.EVM.ERC20 = config.DoubleQuotedString(r.ERC20Addr.Hex()) conf.Contracts.EVM.TestDappAddr = config.DoubleQuotedString(r.EvmTestDAppAddr.Hex()) + conf.Contracts.Solana.SPL = config.DoubleQuotedString(r.SPLAddr.String()) + conf.Contracts.Solana.SPLPrivateKey = r.SPLPrivateKey + conf.Contracts.ZEVM.SystemContractAddr = config.DoubleQuotedString(r.SystemContractAddr.Hex()) conf.Contracts.ZEVM.ETHZRC20Addr = config.DoubleQuotedString(r.ETHZRC20Addr.Hex()) conf.Contracts.ZEVM.ERC20ZRC20Addr = config.DoubleQuotedString(r.ERC20ZRC20Addr.Hex()) conf.Contracts.ZEVM.BTCZRC20Addr = config.DoubleQuotedString(r.BTCZRC20Addr.Hex()) conf.Contracts.ZEVM.SOLZRC20Addr = config.DoubleQuotedString(r.SOLZRC20Addr.Hex()) + conf.Contracts.ZEVM.SPLZRC20Addr = config.DoubleQuotedString(r.SPLZRC20Addr.Hex()) conf.Contracts.ZEVM.TONZRC20Addr = config.DoubleQuotedString(r.TONZRC20Addr.Hex()) conf.Contracts.ZEVM.UniswapFactoryAddr = config.DoubleQuotedString(r.UniswapV2FactoryAddr.Hex()) conf.Contracts.ZEVM.UniswapRouterAddr = config.DoubleQuotedString(r.UniswapV2RouterAddr.Hex()) diff --git a/cmd/zetae2e/config/contracts.go b/cmd/zetae2e/config/contracts.go index 6f2dff72c6..5fa5d09a4e 100644 --- a/cmd/zetae2e/config/contracts.go +++ b/cmd/zetae2e/config/contracts.go @@ -34,6 +34,14 @@ func setContractsFromConfig(r *runner.E2ERunner, conf config.Config) error { r.GatewayProgram = solana.MustPublicKeyFromBase58(c) } + if c := conf.Contracts.Solana.SPLPrivateKey; c != nil { + r.SPLPrivateKey = solana.PrivateKey(c) + } + + if c := conf.Contracts.Solana.SPL; c != "" { + r.SPLAddr = solana.MustPublicKeyFromBase58(c.String()) + } + // set EVM contracts if c := conf.Contracts.EVM.ZetaEthAddr; c != "" { r.ZetaEthAddr, err = c.AsEVMAddress() @@ -135,6 +143,17 @@ func setContractsFromConfig(r *runner.E2ERunner, conf config.Config) error { } } + if c := conf.Contracts.ZEVM.SPLZRC20Addr; c != "" { + r.SPLZRC20Addr, err = c.AsEVMAddress() + if err != nil { + return fmt.Errorf("invalid SPLZRC20Addr: %w", err) + } + r.SPLZRC20, err = zrc20.NewZRC20(r.SPLZRC20Addr, r.ZEVMClient) + if err != nil { + return err + } + } + if c := conf.Contracts.ZEVM.TONZRC20Addr; c != "" { r.TONZRC20Addr, err = c.AsEVMAddress() if err != nil { diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 986e9e4689..0b182263ed 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -218,6 +218,10 @@ func localE2ETest(cmd *cobra.Command, _ []string) { deployerRunner.SetupEVMV2() + if testSolana { + deployerRunner.SetupSolana(conf.AdditionalAccounts.UserSolana.SolanaPrivateKey.String()) + } + deployerRunner.SetZEVMSystemContracts() // NOTE: v2 (gateway) setup called here because system contract needs to be set first, then gateway, then zrc20 @@ -231,10 +235,6 @@ func localE2ETest(cmd *cobra.Command, _ []string) { deployerRunner.UpdateChainParamsV2Contracts() deployerRunner.ERC20CustodyAddr = deployerRunner.ERC20CustodyV2Addr - if testSolana { - deployerRunner.SetupSolana(conf.AdditionalAccounts.UserSolana.SolanaPrivateKey.String()) - } - deployerRunner.MintERC20OnEvm(1000000) logger.Print("✅ setup completed in %s", time.Since(startTime)) @@ -404,15 +404,16 @@ func localE2ETest(cmd *cobra.Command, _ []string) { os.Exit(1) } solanaTests := []string{ - e2etests.TestSolanaDepositName, - e2etests.TestSolanaWithdrawName, - e2etests.TestSolanaDepositAndCallName, - e2etests.TestSolanaDepositAndCallRefundName, - e2etests.TestSolanaDepositRestrictedName, - e2etests.TestSolanaWithdrawRestrictedName, - // TODO move under admin tests - // https://github.com/zeta-chain/node/issues/3085 - e2etests.TestSolanaWhitelistSPLName, + // e2etests.TestSolanaDepositName, + // e2etests.TestSolanaWithdrawName, + // e2etests.TestSolanaDepositAndCallName, + // e2etests.TestSolanaDepositAndCallRefundName, + // e2etests.TestSolanaDepositRestrictedName, + // e2etests.TestSolanaWithdrawRestrictedName, + // // TODO move under admin tests + // // https://github.com/zeta-chain/node/issues/3085 + // e2etests.TestSolanaWhitelistSPLName, + e2etests.TestSolanaDepositSPLName, } eg.Go(solanaTestRoutine(conf, deployerRunner, verbose, solanaTests...)) } diff --git a/e2e/config/config.go b/e2e/config/config.go index 67685b0dd3..9b9065896e 100644 --- a/e2e/config/config.go +++ b/e2e/config/config.go @@ -119,7 +119,9 @@ type Contracts struct { // Solana contains the addresses of predeployed contracts on the Solana chain type Solana struct { - GatewayProgramID string `yaml:"gateway_program_id"` + GatewayProgramID string `yaml:"gateway_program_id"` + SPL DoubleQuotedString `yaml:"spl"` + SPLPrivateKey []byte `yaml:"spl_private_key"` } // EVM contains the addresses of predeployed contracts on the EVM chain @@ -141,6 +143,7 @@ type ZEVM struct { ERC20ZRC20Addr DoubleQuotedString `yaml:"erc20_zrc20"` BTCZRC20Addr DoubleQuotedString `yaml:"btc_zrc20"` SOLZRC20Addr DoubleQuotedString `yaml:"sol_zrc20"` + SPLZRC20Addr DoubleQuotedString `yaml:"spl_zrc20"` TONZRC20Addr DoubleQuotedString `yaml:"ton_zrc20"` UniswapFactoryAddr DoubleQuotedString `yaml:"uniswap_factory"` UniswapRouterAddr DoubleQuotedString `yaml:"uniswap_router"` diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index 9f86120bb0..4f40c31284 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -61,6 +61,7 @@ const ( TestSolanaDepositAndCallRefundName = "solana_deposit_and_call_refund" TestSolanaDepositRestrictedName = "solana_deposit_restricted" TestSolanaWithdrawRestrictedName = "solana_withdraw_restricted" + TestSolanaDepositSPLName = "solana_deposit_spl" /** * TON tests @@ -460,6 +461,14 @@ var AllE2ETests = []runner.E2ETest{ []runner.ArgDefinition{}, TestSolanaWhitelistSPL, ), + runner.NewE2ETest( + TestSolanaDepositSPLName, + "deposit SPL into ZEVM", + []runner.ArgDefinition{ + // {Description: "amount in lamport", DefaultValue: "12000000"}, + }, + TestSolanaDepositSPL, + ), /* TON tests */ diff --git a/e2e/e2etests/test_solana_deposit_spl.go b/e2e/e2etests/test_solana_deposit_spl.go new file mode 100644 index 0000000000..896b741ca0 --- /dev/null +++ b/e2e/e2etests/test_solana_deposit_spl.go @@ -0,0 +1,20 @@ +package e2etests + +import ( + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/runner" +) + +func TestSolanaDepositSPL(r *runner.E2ERunner, _ []string) { + // require.Len(r, args, 1) + + // load deployer private key + privKey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String()) + require.NoError(r, err) + + wall, err := solana.WalletFromPrivateKeyBase58(r.SPLPrivateKey.String()) + require.NoError(r, err) + r.DepositSPL(&privKey, *wall) +} diff --git a/e2e/e2etests/test_solana_whitelist_spl.go b/e2e/e2etests/test_solana_whitelist_spl.go index 259657f72b..0827afd034 100644 --- a/e2e/e2etests/test_solana_whitelist_spl.go +++ b/e2e/e2etests/test_solana_whitelist_spl.go @@ -19,7 +19,7 @@ func TestSolanaWhitelistSPL(r *runner.E2ERunner, _ []string) { privkey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String()) require.NoError(r, err) - spl := r.DeploySPL(&privkey) + spl := r.DeploySPL(&privkey, false) // check that whitelist entry doesn't exist for this spl seed := [][]byte{[]byte("whitelist"), spl.PublicKey().Bytes()} diff --git a/e2e/runner/runner.go b/e2e/runner/runner.go index cf7a8e6f02..8a7d02f305 100644 --- a/e2e/runner/runner.go +++ b/e2e/runner/runner.go @@ -110,6 +110,8 @@ type E2ERunner struct { // programs on Solana GatewayProgram solana.PublicKey + SPLAddr solana.PublicKey + SPLPrivateKey solana.PrivateKey // private key for token account is needed for instruction accounts // contracts evm ZetaEthAddr ethcommon.Address @@ -125,6 +127,8 @@ type E2ERunner struct { // contracts zevm ERC20ZRC20Addr ethcommon.Address ERC20ZRC20 *zrc20.ZRC20 + SPLZRC20Addr ethcommon.Address + SPLZRC20 *zrc20.ZRC20 ETHZRC20Addr ethcommon.Address ETHZRC20 *zrc20.ZRC20 BTCZRC20Addr ethcommon.Address @@ -366,6 +370,8 @@ func (r *E2ERunner) Unlock() { func (r *E2ERunner) PrintContractAddresses() { r.Logger.Print(" --- 📜Solana addresses ---") r.Logger.Print("GatewayProgram: %s", r.GatewayProgram.String()) + r.Logger.Print("SPL: %s", r.SPLAddr.String()) + // zevm contracts r.Logger.Print(" --- 📜zEVM contracts ---") r.Logger.Print("SystemContract: %s", r.SystemContractAddr.Hex()) @@ -373,6 +379,7 @@ func (r *E2ERunner) PrintContractAddresses() { r.Logger.Print("ERC20ZRC20: %s", r.ERC20ZRC20Addr.Hex()) r.Logger.Print("BTCZRC20: %s", r.BTCZRC20Addr.Hex()) r.Logger.Print("SOLZRC20: %s", r.SOLZRC20Addr.Hex()) + r.Logger.Print("SPLZRC20: %s", r.SPLZRC20Addr.Hex()) r.Logger.Print("TONZRC20: %s", r.TONZRC20Addr.Hex()) r.Logger.Print("UniswapFactory: %s", r.UniswapV2FactoryAddr.Hex()) r.Logger.Print("UniswapRouter: %s", r.UniswapV2RouterAddr.Hex()) diff --git a/e2e/runner/setup_solana.go b/e2e/runner/setup_solana.go index 73a571b2be..907f80c4f8 100644 --- a/e2e/runner/setup_solana.go +++ b/e2e/runner/setup_solana.go @@ -87,6 +87,11 @@ func (r *E2ERunner) SetupSolana(deployerPrivateKey string) { err = r.ensureSolanaChainParams() require.NoError(r, err) + + // deploy test spl + tokenAccount := r.DeploySPL(&privkey, true) + r.SPLAddr = tokenAccount.PublicKey() + r.SPLPrivateKey = tokenAccount.PrivateKey } func (r *E2ERunner) ensureSolanaChainParams() error { diff --git a/e2e/runner/setup_zeta.go b/e2e/runner/setup_zeta.go index 1d1db47108..7aa9d8d0c2 100644 --- a/e2e/runner/setup_zeta.go +++ b/e2e/runner/setup_zeta.go @@ -175,10 +175,11 @@ func (r *E2ERunner) SetZEVMZRC20s() { }() // deploy system contracts and ZRC20 contracts on ZetaChain - erc20zrc20Addr, err := r.ZetaTxServer.DeployZRC20s( + erc20zrc20Addr, splzrc20Addr, err := r.ZetaTxServer.DeployZRC20s( e2eutils.OperationalPolicyName, e2eutils.AdminPolicyName, r.ERC20Addr.Hex(), + r.SPLAddr.String(), r.skipChainOperations, ) require.NoError(r, err) @@ -188,6 +189,11 @@ func (r *E2ERunner) SetZEVMZRC20s() { r.ERC20ZRC20, err = zrc20.NewZRC20(r.ERC20ZRC20Addr, r.ZEVMClient) require.NoError(r, err) + // Set SPLZRC20Addr + r.SPLZRC20Addr = ethcommon.HexToAddress(splzrc20Addr) + r.SPLZRC20, err = zrc20.NewZRC20(r.SPLZRC20Addr, r.ZEVMClient) + require.NoError(r, err) + // set ZRC20 contracts r.SetupETHZRC20() r.SetupBTCZRC20() diff --git a/e2e/runner/solana.go b/e2e/runner/solana.go index 24ea3c3b2f..98b253237d 100644 --- a/e2e/runner/solana.go +++ b/e2e/runner/solana.go @@ -1,13 +1,16 @@ package runner import ( + "fmt" "math/big" "time" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/gagliardetto/solana-go" + associatedtokenaccount "github.com/gagliardetto/solana-go/programs/associated-token-account" "github.com/gagliardetto/solana-go/programs/system" "github.com/gagliardetto/solana-go/programs/token" + "github.com/gagliardetto/solana-go/rpc" "github.com/near/borsh-go" "github.com/stretchr/testify/require" @@ -60,6 +63,94 @@ func (r *E2ERunner) CreateDepositInstruction( return inst } +func (r *E2ERunner) CreateWhitelistSPLMintInstruction( + signer solana.PublicKey, + whitelistEntry solana.PublicKey, + whitelistCandidate solana.PublicKey, +) solana.Instruction { + // compute the gateway PDA address + pdaComputed := r.ComputePdaAddress() + programID := r.GatewayProgram + + // create 'whitelist_spl_mint' instruction + inst := &solana.GenericInstruction{} + accountSlice := []*solana.AccountMeta{} + accountSlice = append(accountSlice, solana.Meta(whitelistEntry).WRITE()) + accountSlice = append(accountSlice, solana.Meta(whitelistCandidate)) + accountSlice = append(accountSlice, solana.Meta(pdaComputed).WRITE()) + accountSlice = append(accountSlice, solana.Meta(signer).WRITE().SIGNER()) + accountSlice = append(accountSlice, solana.Meta(solana.SystemProgramID)) + inst.ProgID = programID + inst.AccountValues = accountSlice + + var err error + inst.DataBytes, err = borsh.Serialize(solanacontract.WhitelistInstructionParams{ + Discriminator: solanacontract.DiscriminatorWhitelistSplMint, + // remaining fields are empty because no tss signature is needed if signer is admin account + }) + require.NoError(r, err) + + return inst +} + +/* + + #[account(mut)] + pub signer: Signer<'info>, + + #[account(seeds = [b"meta"], bump)] + pub pda: Account<'info, Pda>, + + #[account(seeds=[b"whitelist", mint_account.key().as_ref()], bump)] + pub whitelist_entry: Account<'info, WhitelistEntry>, // attach whitelist entry to show the mint_account is whitelisted + + pub mint_account: Account<'info, Mint>, + + pub token_program: Program<'info, Token>, + + #[account(mut)] + pub from: Account<'info, TokenAccount>, // this must be owned by signer; normally the ATA of signer + #[account(mut)] + pub to: Account<'info, TokenAccount>, // this must be ATA of PDA +*/ + +func (r *E2ERunner) CreateDepositSPLInstruction( + amount uint64, + signer solana.PublicKey, + whitelistEntry solana.PublicKey, + mint solana.PublicKey, + from solana.PublicKey, + to solana.PublicKey, + receiver solana.PublicKey, +) solana.Instruction { + // compute the gateway PDA address + pdaComputed := r.ComputePdaAddress() + programID := r.GatewayProgram + + // create 'deposit_spl' instruction + inst := &solana.GenericInstruction{} + accountSlice := []*solana.AccountMeta{} + accountSlice = append(accountSlice, solana.Meta(signer).WRITE().SIGNER()) + accountSlice = append(accountSlice, solana.Meta(pdaComputed)) + accountSlice = append(accountSlice, solana.Meta(whitelistEntry)) + accountSlice = append(accountSlice, solana.Meta(mint)) + accountSlice = append(accountSlice, solana.Meta(solana.TokenProgramID)) + accountSlice = append(accountSlice, solana.Meta(from).WRITE()) + accountSlice = append(accountSlice, solana.Meta(to).WRITE()) + inst.ProgID = programID + inst.AccountValues = accountSlice + + var err error + inst.DataBytes, err = borsh.Serialize(solanacontract.DepositInstructionParams{ + Discriminator: solanacontract.DiscriminatorDepositSPL, + Amount: amount, + Memo: receiver.Bytes(), + }) + require.NoError(r, err) + + return inst +} + // CreateSignedTransaction creates a signed transaction from instructions func (r *E2ERunner) CreateSignedTransaction( instructions []solana.Instruction, @@ -97,7 +188,48 @@ func (r *E2ERunner) CreateSignedTransaction( return tx } -func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey) *solana.Wallet { +func (r *E2ERunner) DepositSPL(privateKey *solana.PrivateKey, tokenAccount solana.Wallet) { + // ata for pda + pda := r.ComputePdaAddress() + pdaAta, _, err := solana.FindAssociatedTokenAddress(pda, tokenAccount.PublicKey()) + require.NoError(r, err) + + ata, _, err := solana.FindAssociatedTokenAddress(privateKey.PublicKey(), tokenAccount.PublicKey()) + require.NoError(r, err) + + ataInstruction := associatedtokenaccount.NewCreateInstruction(privateKey.PublicKey(), pda, tokenAccount.PublicKey()).Build() + signedTx := r.CreateSignedTransaction( + []solana.Instruction{ataInstruction}, + *privateKey, + []solana.PrivateKey{tokenAccount.PrivateKey}, + ) + // broadcast the transaction and wait for finalization + _, out := r.BroadcastTxSync(signedTx) + r.Logger.Info("pda ata spl logs: %v", out.Meta.LogMessages) + + _, err = r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentConfirmed) + require.NoError(r, err) + + // deposit spl + seed := [][]byte{[]byte("whitelist"), tokenAccount.PublicKey().Bytes()} + whitelistEntryPDA, _, err := solana.FindProgramAddress(seed, r.GatewayProgram) + require.NoError(r, err) + + depositSPLInstruction := r.CreateDepositSPLInstruction(uint64(500_000), privateKey.PublicKey(), whitelistEntryPDA, tokenAccount.PublicKey(), ata, pdaAta, privateKey.PublicKey()) + signedTx = r.CreateSignedTransaction( + []solana.Instruction{depositSPLInstruction}, + *privateKey, + []solana.PrivateKey{}, + ) + // broadcast the transaction and wait for finalization + _, out = r.BroadcastTxSync(signedTx) + r.Logger.Info("deposit spl logs: %v", out.Meta.LogMessages) + + _, err = r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentConfirmed) + require.NoError(r, err) +} + +func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey, whitelist bool) *solana.Wallet { lamport, err := r.SolanaClient.GetMinimumBalanceForRentExemption(r.Ctx, token.MINT_SIZE, rpc.CommitmentFinalized) require.NoError(r, err) @@ -128,6 +260,67 @@ func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey) *solana.Wallet { _, out := r.BroadcastTxSync(signedTx) r.Logger.Info("create spl logs: %v", out.Meta.LogMessages) + if whitelist { + seed := [][]byte{[]byte("whitelist"), tokenAccount.PublicKey().Bytes()} + whitelistEntryPDA, _, err := solana.FindProgramAddress(seed, r.GatewayProgram) + require.NoError(r, err) + + whitelistEntryInfo, err := r.SolanaClient.GetAccountInfo(r.Ctx, whitelistEntryPDA) + require.Error(r, err) + + // already whitelisted + if whitelistEntryInfo != nil { + return tokenAccount + } + + // create 'whitelist_spl_mint' instruction + instruction := r.CreateWhitelistSPLMintInstruction(privateKey.PublicKey(), whitelistEntryPDA, tokenAccount.PublicKey()) + // create and sign the transaction + signedTx := r.CreateSignedTransaction([]solana.Instruction{instruction}, *privateKey, []solana.PrivateKey{}) + + // broadcast the transaction and wait for finalization + _, out := r.BroadcastTxSync(signedTx) + r.Logger.Info("whitelist spl mint logs: %v", out.Meta.LogMessages) + + whitelistEntryInfo, err = r.SolanaClient.GetAccountInfo(r.Ctx, whitelistEntryPDA) + require.NoError(r, err) + require.NotNil(r, whitelistEntryInfo) + + fmt.Println("minting tokens to deployer...") + + ata, _, err := solana.FindAssociatedTokenAddress(privateKey.PublicKey(), tokenAccount.PublicKey()) + require.NoError(r, err) + + ataInstruction := associatedtokenaccount.NewCreateInstruction(privateKey.PublicKey(), privateKey.PublicKey(), tokenAccount.PublicKey()).Build() + signedTx = r.CreateSignedTransaction( + []solana.Instruction{ataInstruction}, + *privateKey, + []solana.PrivateKey{tokenAccount.PrivateKey}, + ) + // broadcast the transaction and wait for finalization + _, out = r.BroadcastTxSync(signedTx) + r.Logger.Info("ata spl logs: %v", out.Meta.LogMessages) + + _, err = r.SolanaClient.GetTokenAccountBalance(r.Ctx, ata, rpc.CommitmentConfirmed) + require.NoError(r, err) + + amount := uint64(1_000_000) + mintToInstruction := token.NewMintToInstruction(amount, tokenAccount.PublicKey(), ata, privateKey.PublicKey(), []solana.PublicKey{}).Build() + signedTx = r.CreateSignedTransaction( + []solana.Instruction{mintToInstruction}, + *privateKey, + []solana.PrivateKey{}, + ) + + // broadcast the transaction and wait for finalization + _, out = r.BroadcastTxSync(signedTx) + r.Logger.Info("mint spl logs: %v", out.Meta.LogMessages) + + _, err = r.SolanaClient.GetTokenAccountBalance(r.Ctx, ata, rpc.CommitmentConfirmed) + require.NoError(r, err) + + } + return tokenAccount } diff --git a/e2e/txserver/zeta_tx_server.go b/e2e/txserver/zeta_tx_server.go index 9b6e2d0b65..0031a14762 100644 --- a/e2e/txserver/zeta_tx_server.go +++ b/e2e/txserver/zeta_tx_server.go @@ -381,27 +381,27 @@ func (zts ZetaTxServer) DeploySystemContracts( } // DeployZRC20s deploys the ZRC20 contracts -// returns the addresses of erc20 zrc20 +// returns the addresses of erc20 and spl zrc20 func (zts ZetaTxServer) DeployZRC20s( - accountOperational, accountAdmin, erc20Addr string, + accountOperational, accountAdmin, erc20Addr string, splAddr string, skipChain func(chainID int64) bool, -) (string, error) { +) (string, string, error) { // retrieve account accOperational, err := zts.clientCtx.Keyring.Key(accountOperational) if err != nil { - return "", err + return "", "", err } addrOperational, err := accOperational.GetAddress() if err != nil { - return "", err + return "", "", err } accAdmin, err := zts.clientCtx.Keyring.Key(accountAdmin) if err != nil { - return "", err + return "", "", err } addrAdmin, err := accAdmin.GetAddress() if err != nil { - return "", err + return "", "", err } // authorization for deploying new ZRC20 has changed from accountOperational to accountAdmin in v19 @@ -411,7 +411,7 @@ func (zts ZetaTxServer) DeployZRC20s( deployerAddr := addrAdmin.String() authorization, preV19, err := zts.fetchMessagePermissions(&fungibletypes.MsgDeployFungibleCoinZRC20{}) if err != nil { - return "", fmt.Errorf("failed to fetch message permissions: %s", err.Error()) + return "", "", fmt.Errorf("failed to fetch message permissions: %s", err.Error()) } if preV19 || authorization == authoritytypes.PolicyType_groupOperational { deployerAccount = accountOperational @@ -449,6 +449,16 @@ func (zts ZetaTxServer) DeployZRC20s( coin.CoinType_Gas, 100000, ), + fungibletypes.NewMsgDeployFungibleCoinZRC20( + deployerAddr, + splAddr, + chains.SolanaLocalnet.ChainId, + 9, + "USDT", + "USDT", + coin.CoinType_ERC20, + 100000, + ), fungibletypes.NewMsgDeployFungibleCoinZRC20( deployerAddr, "", @@ -484,12 +494,12 @@ func (zts ZetaTxServer) DeployZRC20s( res, err := zts.BroadcastTx(deployerAccount, deployMsgsIface...) if err != nil { - return "", fmt.Errorf("deploy zrc20s: %w", err) + return "", "", fmt.Errorf("deploy zrc20s: %w", err) } deployedEvents, ok := EventsOfType[*fungibletypes.EventZRC20Deployed](res.Events) if !ok { - return "", fmt.Errorf("no EventZRC20Deployed in %s", res.TxHash) + return "", "", fmt.Errorf("no EventZRC20Deployed in %s", res.TxHash) } zrc20Addrs := lo.Map(deployedEvents, func(ev *fungibletypes.EventZRC20Deployed, _ int) string { @@ -498,7 +508,7 @@ func (zts ZetaTxServer) DeployZRC20s( err = zts.InitializeLiquidityCaps(zrc20Addrs...) if err != nil { - return "", fmt.Errorf("initialize liquidity cap: %w", err) + return "", "", fmt.Errorf("initialize liquidity cap: %w", err) } // find erc20 zrc20 @@ -506,10 +516,18 @@ func (zts ZetaTxServer) DeployZRC20s( return ev.ChainId == chains.GoerliLocalnet.ChainId && ev.CoinType == coin.CoinType_ERC20 }) if !ok { - return "", fmt.Errorf("unable to find erc20 zrc20") + return "", "", fmt.Errorf("unable to find erc20 zrc20") + } + + // find spl zrc20 + splzrc20, ok := lo.Find(deployedEvents, func(ev *fungibletypes.EventZRC20Deployed) bool { + return ev.ChainId == chains.SolanaLocalnet.ChainId && ev.CoinType == coin.CoinType_ERC20 + }) + if !ok { + return "", "", fmt.Errorf("unable to find spl zrc20") } - return erc20zrc20.Contract, nil + return erc20zrc20.Contract, splzrc20.Contract, nil } // FundEmissionsPool funds the emissions pool with the given amount diff --git a/pkg/contracts/solana/gateway.go b/pkg/contracts/solana/gateway.go index a3adcf5eae..e0b8eeee7f 100644 --- a/pkg/contracts/solana/gateway.go +++ b/pkg/contracts/solana/gateway.go @@ -17,6 +17,8 @@ const ( // AccountsNumberOfDeposit is the number of accounts required for Solana gateway deposit instruction // [signer, pda, system_program] AccountsNumDeposit = 3 + + AccountsNumberDepositSPL = 7 ) var ( diff --git a/pkg/contracts/solana/instruction.go b/pkg/contracts/solana/instruction.go index df5db0416b..7f7ebc5e41 100644 --- a/pkg/contracts/solana/instruction.go +++ b/pkg/contracts/solana/instruction.go @@ -34,6 +34,18 @@ type DepositInstructionParams struct { Memo []byte } +// DepositSPLInstructionParams contains the parameters for a gateway deposit instruction +type DepositSPLInstructionParams struct { + // Discriminator is the unique identifier for the deposit instruction + Discriminator [8]byte + + // Amount is the lamports amount for the deposit + Amount uint64 + + // Memo is the memo for the deposit + Memo []byte +} + // OutboundInstruction is the interface for all gateway outbound instructions type OutboundInstruction interface { // Signer returns the signer of the instruction diff --git a/x/fungible/keeper/foreign_coins.go b/x/fungible/keeper/foreign_coins.go index 4dd5ac72a4..3851de88fd 100644 --- a/x/fungible/keeper/foreign_coins.go +++ b/x/fungible/keeper/foreign_coins.go @@ -5,7 +5,6 @@ import ( "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" - ethcommon "github.com/ethereum/go-ethereum/common" "github.com/zeta-chain/node/pkg/coin" "github.com/zeta-chain/node/x/fungible/types" @@ -110,15 +109,15 @@ func (k Keeper) GetGasCoinForForeignCoin(ctx sdk.Context, chainID int64) (types. // GetForeignCoinFromAsset returns the foreign coin for a given asset for a given chain func (k Keeper) GetForeignCoinFromAsset(ctx sdk.Context, asset string, chainID int64) (types.ForeignCoins, bool) { - if !ethcommon.IsHexAddress(asset) { - return types.ForeignCoins{}, false - } - assetAddr := ethcommon.HexToAddress(asset) + // if !ethcommon.IsHexAddress(asset) { + // return types.ForeignCoins{}, false + // } + // assetAddr := ethcommon.HexToAddress(asset) foreignCoinList := k.GetAllForeignCoinsForChain(ctx, chainID) for _, coin := range foreignCoinList { - coinAssetAddr := ethcommon.HexToAddress(coin.Asset) - if coinAssetAddr == assetAddr && coin.ForeignChainId == chainID { + // coinAssetAddr := ethcommon.HexToAddress(coin.Asset) + if asset == coin.Asset && coin.ForeignChainId == chainID { return coin, true } } diff --git a/zetaclient/chains/solana/observer/inbound.go b/zetaclient/chains/solana/observer/inbound.go index 4c93d95470..abe6fb6a09 100644 --- a/zetaclient/chains/solana/observer/inbound.go +++ b/zetaclient/chains/solana/observer/inbound.go @@ -315,12 +315,50 @@ func (ob *Observer) ParseInboundAsDeposit( // ParseInboundAsDepositSPL tries to parse an instruction as a 'deposit_spl_token'. // It returns nil if the instruction can't be parsed as a 'deposit_spl_token'. func (ob *Observer) ParseInboundAsDepositSPL( - _ *solana.Transaction, - _ int, - _ uint64, + tx *solana.Transaction, + instructionIndex int, + slot uint64, ) (*clienttypes.InboundEvent, error) { - // not implemented yet - return nil, nil + // get instruction by index + instruction := tx.Message.Instructions[instructionIndex] + + // try deserializing instruction as a 'deposit_spl_token' + var inst solanacontracts.DepositSPLInstructionParams + err := borsh.Deserialize(&inst, instruction.Data) + if err != nil { + return nil, nil + } + + // check if the instruction is a deposit or not + if inst.Discriminator != solanacontracts.DiscriminatorDepositSPL { + return nil, nil + } + + // get the sender address (skip if unable to parse signer address) + sender, spl, err := ob.GetFromDepositSPLAccounts(tx, &instruction) + if err != nil { + ob.Logger(). + Inbound.Err(err). + Msgf("unable to get signer for sig %s instruction %d", tx.Signatures[0], instructionIndex) + return nil, nil + } + + // build inbound event + event := &clienttypes.InboundEvent{ + SenderChainID: ob.Chain().ChainId, + Sender: sender, + Receiver: sender, + TxOrigin: sender, + Amount: inst.Amount, + Memo: inst.Memo, + BlockNumber: slot, // instead of using block, Solana explorer uses slot for indexing + TxHash: tx.Signatures[0].String(), + Index: 0, // hardcode to 0 for Solana, not a EVM smart contract call + CoinType: coin.CoinType_ERC20, + Asset: spl, + } + + return event, nil } // GetSignerDeposit returns the signer address of the deposit instruction @@ -359,3 +397,16 @@ func (ob *Observer) GetSignerDeposit(tx *solana.Transaction, inst *solana.Compil // sender is the signer account return tx.Message.AccountKeys[signerIndex].String(), nil } + +func (ob *Observer) GetFromDepositSPLAccounts(tx *solana.Transaction, inst *solana.CompiledInstruction) (string, string, error) { + // there should be 7 accounts for a deposit spl instruction + if len(inst.Accounts) != solanacontracts.AccountsNumberDepositSPL { + return "", "", fmt.Errorf("want %d accounts, got %d", solanacontracts.AccountsNumberDepositSPL, len(inst.Accounts)) + } + + // the accounts are [signer, pda, whitelist_entry, mint_account, token_program, from, to] + signer := tx.Message.AccountKeys[0] + spl := tx.Message.AccountKeys[inst.Accounts[3]] + + return signer.String(), spl.String(), nil +} From a2f9cc320c4beeed9132f947902b26bc4e327b67 Mon Sep 17 00:00:00 2001 From: skosito Date: Wed, 6 Nov 2024 17:07:26 +0100 Subject: [PATCH 02/13] test wip for deposit spl and call --- .../test_solana_deposit_and_call_spl.go | 41 +++++++++++++++++++ e2e/e2etests/test_solana_deposit_spl.go | 11 ++++- e2e/runner/solana.go | 13 +++--- 3 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 e2e/e2etests/test_solana_deposit_and_call_spl.go diff --git a/e2e/e2etests/test_solana_deposit_and_call_spl.go b/e2e/e2etests/test_solana_deposit_and_call_spl.go new file mode 100644 index 0000000000..9c9a808147 --- /dev/null +++ b/e2e/e2etests/test_solana_deposit_and_call_spl.go @@ -0,0 +1,41 @@ +package e2etests + +import ( + "math/big" + + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + testcontract "github.com/zeta-chain/node/testutil/contracts" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" +) + +func TestSolanaDepositSPL(r *runner.E2ERunner, _ []string) { + // require.Len(r, args, 1) + + // deploy an example contract in ZEVM + contractAddr, _, contract, err := testcontract.DeployExample(r.ZEVMAuth, r.ZEVMClient) + require.NoError(r, err) + 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) + + wall, err := solana.WalletFromPrivateKeyBase58(r.SPLPrivateKey.String()) + require.NoError(r, err) + + // execute the deposit transaction + data := []byte("hello lamports") + sig := r.DepositSPL(&privKey, *wall, contractAddr, data) + + // wait for the cctx to be mined + cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, sig.String(), r.CctxClient, r.Logger, r.CctxTimeout) + r.Logger.CCTX(*cctx, "solana_deposit") + utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) + + // check if example contract has been called, bar value should be set to amount + utils.MustHaveCalledExampleContract(r, contract, big.NewInt(500_000)) +} diff --git a/e2e/e2etests/test_solana_deposit_spl.go b/e2e/e2etests/test_solana_deposit_spl.go index 896b741ca0..60e2144c42 100644 --- a/e2e/e2etests/test_solana_deposit_spl.go +++ b/e2e/e2etests/test_solana_deposit_spl.go @@ -5,9 +5,11 @@ 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 TestSolanaDepositSPL(r *runner.E2ERunner, _ []string) { +func TestSolanaDepositSPL2(r *runner.E2ERunner, _ []string) { // require.Len(r, args, 1) // load deployer private key @@ -16,5 +18,10 @@ func TestSolanaDepositSPL(r *runner.E2ERunner, _ []string) { wall, err := solana.WalletFromPrivateKeyBase58(r.SPLPrivateKey.String()) require.NoError(r, err) - r.DepositSPL(&privKey, *wall) + sig := r.DepositSPL(&privKey, *wall, r.EVMAddress(), nil) + + // wait for the cctx to be mined + cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, sig.String(), r.CctxClient, r.Logger, r.CctxTimeout) + r.Logger.CCTX(*cctx, "solana_deposit") + utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) } diff --git a/e2e/runner/solana.go b/e2e/runner/solana.go index 98b253237d..2d0df4084d 100644 --- a/e2e/runner/solana.go +++ b/e2e/runner/solana.go @@ -121,7 +121,8 @@ func (r *E2ERunner) CreateDepositSPLInstruction( mint solana.PublicKey, from solana.PublicKey, to solana.PublicKey, - receiver solana.PublicKey, + receiver ethcommon.Address, + data []byte, ) solana.Instruction { // compute the gateway PDA address pdaComputed := r.ComputePdaAddress() @@ -144,7 +145,7 @@ func (r *E2ERunner) CreateDepositSPLInstruction( inst.DataBytes, err = borsh.Serialize(solanacontract.DepositInstructionParams{ Discriminator: solanacontract.DiscriminatorDepositSPL, Amount: amount, - Memo: receiver.Bytes(), + Memo: append(receiver.Bytes(), data...), }) require.NoError(r, err) @@ -188,7 +189,7 @@ func (r *E2ERunner) CreateSignedTransaction( return tx } -func (r *E2ERunner) DepositSPL(privateKey *solana.PrivateKey, tokenAccount solana.Wallet) { +func (r *E2ERunner) DepositSPL(privateKey *solana.PrivateKey, tokenAccount solana.Wallet, receiver ethcommon.Address, data []byte) solana.Signature { // ata for pda pda := r.ComputePdaAddress() pdaAta, _, err := solana.FindAssociatedTokenAddress(pda, tokenAccount.PublicKey()) @@ -215,18 +216,20 @@ func (r *E2ERunner) DepositSPL(privateKey *solana.PrivateKey, tokenAccount solan whitelistEntryPDA, _, err := solana.FindProgramAddress(seed, r.GatewayProgram) require.NoError(r, err) - depositSPLInstruction := r.CreateDepositSPLInstruction(uint64(500_000), privateKey.PublicKey(), whitelistEntryPDA, tokenAccount.PublicKey(), ata, pdaAta, privateKey.PublicKey()) + depositSPLInstruction := r.CreateDepositSPLInstruction(uint64(500_000), privateKey.PublicKey(), whitelistEntryPDA, tokenAccount.PublicKey(), ata, pdaAta, receiver, data) signedTx = r.CreateSignedTransaction( []solana.Instruction{depositSPLInstruction}, *privateKey, []solana.PrivateKey{}, ) // broadcast the transaction and wait for finalization - _, out = r.BroadcastTxSync(signedTx) + sig, out := r.BroadcastTxSync(signedTx) r.Logger.Info("deposit spl logs: %v", out.Meta.LogMessages) _, err = r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentConfirmed) require.NoError(r, err) + + return sig } func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey, whitelist bool) *solana.Wallet { From 0ed2f0eafa519ca9f741bf72b5ba6143eebc1e4a Mon Sep 17 00:00:00 2001 From: skosito Date: Wed, 6 Nov 2024 23:59:52 +0100 Subject: [PATCH 03/13] fix up creation of ata --- cmd/zetae2e/config/config.go | 6 +- cmd/zetae2e/config/contracts.go | 4 - cmd/zetae2e/local/local.go | 17 +-- e2e/config/config.go | 1 - e2e/e2etests/e2etests.go | 11 +- .../test_solana_deposit_and_call_spl.go | 14 ++- e2e/e2etests/test_solana_deposit_spl.go | 11 +- e2e/e2etests/test_solana_whitelist_spl.go | 3 +- e2e/runner/runner.go | 1 - e2e/runner/setup_solana.go | 3 +- e2e/runner/solana.go | 101 +++++++++--------- 11 files changed, 84 insertions(+), 88 deletions(-) diff --git a/cmd/zetae2e/config/config.go b/cmd/zetae2e/config/config.go index 32446a4799..abb758215c 100644 --- a/cmd/zetae2e/config/config.go +++ b/cmd/zetae2e/config/config.go @@ -54,18 +54,16 @@ func RunnerFromConfig( // ExportContractsFromRunner export contracts from the runner to config using a source config func ExportContractsFromRunner(r *runner.E2ERunner, conf config.Config) config.Config { + // copy contracts from deployer runner conf.Contracts.Solana.GatewayProgramID = r.GatewayProgram.String() + conf.Contracts.Solana.SPL = config.DoubleQuotedString(r.SPLAddr.String()) - // copy contracts from deployer runner conf.Contracts.EVM.ZetaEthAddr = config.DoubleQuotedString(r.ZetaEthAddr.Hex()) conf.Contracts.EVM.ConnectorEthAddr = config.DoubleQuotedString(r.ConnectorEthAddr.Hex()) conf.Contracts.EVM.CustodyAddr = config.DoubleQuotedString(r.ERC20CustodyAddr.Hex()) conf.Contracts.EVM.ERC20 = config.DoubleQuotedString(r.ERC20Addr.Hex()) conf.Contracts.EVM.TestDappAddr = config.DoubleQuotedString(r.EvmTestDAppAddr.Hex()) - conf.Contracts.Solana.SPL = config.DoubleQuotedString(r.SPLAddr.String()) - conf.Contracts.Solana.SPLPrivateKey = r.SPLPrivateKey - conf.Contracts.ZEVM.SystemContractAddr = config.DoubleQuotedString(r.SystemContractAddr.Hex()) conf.Contracts.ZEVM.ETHZRC20Addr = config.DoubleQuotedString(r.ETHZRC20Addr.Hex()) conf.Contracts.ZEVM.ERC20ZRC20Addr = config.DoubleQuotedString(r.ERC20ZRC20Addr.Hex()) diff --git a/cmd/zetae2e/config/contracts.go b/cmd/zetae2e/config/contracts.go index 5fa5d09a4e..2fbc69563c 100644 --- a/cmd/zetae2e/config/contracts.go +++ b/cmd/zetae2e/config/contracts.go @@ -34,10 +34,6 @@ func setContractsFromConfig(r *runner.E2ERunner, conf config.Config) error { r.GatewayProgram = solana.MustPublicKeyFromBase58(c) } - if c := conf.Contracts.Solana.SPLPrivateKey; c != nil { - r.SPLPrivateKey = solana.PrivateKey(c) - } - if c := conf.Contracts.Solana.SPL; c != "" { r.SPLAddr = solana.MustPublicKeyFromBase58(c.String()) } diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 0b182263ed..47da6e21a0 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -404,16 +404,17 @@ func localE2ETest(cmd *cobra.Command, _ []string) { os.Exit(1) } solanaTests := []string{ - // e2etests.TestSolanaDepositName, - // e2etests.TestSolanaWithdrawName, - // e2etests.TestSolanaDepositAndCallName, - // e2etests.TestSolanaDepositAndCallRefundName, - // e2etests.TestSolanaDepositRestrictedName, - // e2etests.TestSolanaWithdrawRestrictedName, - // // TODO move under admin tests - // // https://github.com/zeta-chain/node/issues/3085 + e2etests.TestSolanaDepositName, + e2etests.TestSolanaWithdrawName, + e2etests.TestSolanaDepositAndCallName, + e2etests.TestSolanaDepositAndCallRefundName, + e2etests.TestSolanaDepositRestrictedName, + e2etests.TestSolanaWithdrawRestrictedName, + // TODO move under admin tests + // https://github.com/zeta-chain/node/issues/3085 // e2etests.TestSolanaWhitelistSPLName, e2etests.TestSolanaDepositSPLName, + e2etests.TestSolanaDepositSPLAndCallName, } eg.Go(solanaTestRoutine(conf, deployerRunner, verbose, solanaTests...)) } diff --git a/e2e/config/config.go b/e2e/config/config.go index 9b9065896e..4351e577f6 100644 --- a/e2e/config/config.go +++ b/e2e/config/config.go @@ -121,7 +121,6 @@ type Contracts struct { type Solana struct { GatewayProgramID string `yaml:"gateway_program_id"` SPL DoubleQuotedString `yaml:"spl"` - SPLPrivateKey []byte `yaml:"spl_private_key"` } // EVM contains the addresses of predeployed contracts on the EVM chain diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index 4f40c31284..65eaee3923 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -62,6 +62,7 @@ const ( TestSolanaDepositRestrictedName = "solana_deposit_restricted" TestSolanaWithdrawRestrictedName = "solana_withdraw_restricted" TestSolanaDepositSPLName = "solana_deposit_spl" + TestSolanaDepositSPLAndCallName = "solana_deposit_spl_and_call" /** * TON tests @@ -465,10 +466,18 @@ var AllE2ETests = []runner.E2ETest{ TestSolanaDepositSPLName, "deposit SPL into ZEVM", []runner.ArgDefinition{ - // {Description: "amount in lamport", DefaultValue: "12000000"}, + {Description: "amount of spl tokens", DefaultValue: "500000"}, }, TestSolanaDepositSPL, ), + runner.NewE2ETest( + TestSolanaDepositSPLAndCallName, + "deposit SPL into ZEVM and call", + []runner.ArgDefinition{ + {Description: "amount of spl tokens", DefaultValue: "500000"}, + }, + TestSolanaDepositSPLAndCall, + ), /* TON tests */ diff --git a/e2e/e2etests/test_solana_deposit_and_call_spl.go b/e2e/e2etests/test_solana_deposit_and_call_spl.go index 9c9a808147..198a2ba75d 100644 --- a/e2e/e2etests/test_solana_deposit_and_call_spl.go +++ b/e2e/e2etests/test_solana_deposit_and_call_spl.go @@ -12,8 +12,9 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) -func TestSolanaDepositSPL(r *runner.E2ERunner, _ []string) { - // require.Len(r, args, 1) +func TestSolanaDepositSPLAndCall(r *runner.E2ERunner, args []string) { + require.Len(r, args, 1) + amount := parseInt(r, args[0]) // deploy an example contract in ZEVM contractAddr, _, contract, err := testcontract.DeployExample(r.ZEVMAuth, r.ZEVMClient) @@ -24,16 +25,13 @@ func TestSolanaDepositSPL(r *runner.E2ERunner, _ []string) { privKey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String()) require.NoError(r, err) - wall, err := solana.WalletFromPrivateKeyBase58(r.SPLPrivateKey.String()) - require.NoError(r, err) - // execute the deposit transaction - data := []byte("hello lamports") - sig := r.DepositSPL(&privKey, *wall, contractAddr, data) + data := []byte("hello spl tokens") + sig := r.DepositSPL(&privKey, uint64(amount), r.SPLAddr, contractAddr, data) // wait for the cctx to be mined cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, sig.String(), r.CctxClient, r.Logger, r.CctxTimeout) - r.Logger.CCTX(*cctx, "solana_deposit") + r.Logger.CCTX(*cctx, "solana_deposit_spl_and_call") utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) // check if example contract has been called, bar value should be set to amount diff --git a/e2e/e2etests/test_solana_deposit_spl.go b/e2e/e2etests/test_solana_deposit_spl.go index 60e2144c42..c72af3c29d 100644 --- a/e2e/e2etests/test_solana_deposit_spl.go +++ b/e2e/e2etests/test_solana_deposit_spl.go @@ -9,19 +9,18 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) -func TestSolanaDepositSPL2(r *runner.E2ERunner, _ []string) { - // require.Len(r, args, 1) +func TestSolanaDepositSPL(r *runner.E2ERunner, args []string) { + require.Len(r, args, 1) + amount := parseInt(r, args[0]) // load deployer private key privKey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String()) require.NoError(r, err) - wall, err := solana.WalletFromPrivateKeyBase58(r.SPLPrivateKey.String()) - require.NoError(r, err) - sig := r.DepositSPL(&privKey, *wall, r.EVMAddress(), nil) + sig := r.DepositSPL(&privKey, uint64(amount), r.SPLAddr, r.EVMAddress(), nil) // wait for the cctx to be mined cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, sig.String(), r.CctxClient, r.Logger, r.CctxTimeout) - r.Logger.CCTX(*cctx, "solana_deposit") + r.Logger.CCTX(*cctx, "solana_deposit_spl") utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) } diff --git a/e2e/e2etests/test_solana_whitelist_spl.go b/e2e/e2etests/test_solana_whitelist_spl.go index 0827afd034..98f577fd56 100644 --- a/e2e/e2etests/test_solana_whitelist_spl.go +++ b/e2e/e2etests/test_solana_whitelist_spl.go @@ -19,7 +19,8 @@ func TestSolanaWhitelistSPL(r *runner.E2ERunner, _ []string) { privkey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String()) require.NoError(r, err) - spl := r.DeploySPL(&privkey, false) + // deploy SPL token, but don't whitelist in gateway + spl := r.DeploySPL(&privkey, false, uint64(1_000_000)) // check that whitelist entry doesn't exist for this spl seed := [][]byte{[]byte("whitelist"), spl.PublicKey().Bytes()} diff --git a/e2e/runner/runner.go b/e2e/runner/runner.go index 8a7d02f305..ef68abe6a8 100644 --- a/e2e/runner/runner.go +++ b/e2e/runner/runner.go @@ -111,7 +111,6 @@ type E2ERunner struct { // programs on Solana GatewayProgram solana.PublicKey SPLAddr solana.PublicKey - SPLPrivateKey solana.PrivateKey // private key for token account is needed for instruction accounts // contracts evm ZetaEthAddr ethcommon.Address diff --git a/e2e/runner/setup_solana.go b/e2e/runner/setup_solana.go index 907f80c4f8..c8e22e3299 100644 --- a/e2e/runner/setup_solana.go +++ b/e2e/runner/setup_solana.go @@ -89,9 +89,8 @@ func (r *E2ERunner) SetupSolana(deployerPrivateKey string) { require.NoError(r, err) // deploy test spl - tokenAccount := r.DeploySPL(&privkey, true) + tokenAccount := r.DeploySPL(&privkey, true, uint64(1_000_000)) r.SPLAddr = tokenAccount.PublicKey() - r.SPLPrivateKey = tokenAccount.PrivateKey } func (r *E2ERunner) ensureSolanaChainParams() error { diff --git a/e2e/runner/solana.go b/e2e/runner/solana.go index 2d0df4084d..7839c6a6bb 100644 --- a/e2e/runner/solana.go +++ b/e2e/runner/solana.go @@ -1,7 +1,6 @@ package runner import ( - "fmt" "math/big" "time" @@ -189,35 +188,46 @@ func (r *E2ERunner) CreateSignedTransaction( return tx } -func (r *E2ERunner) DepositSPL(privateKey *solana.PrivateKey, tokenAccount solana.Wallet, receiver ethcommon.Address, data []byte) solana.Signature { - // ata for pda - pda := r.ComputePdaAddress() - pdaAta, _, err := solana.FindAssociatedTokenAddress(pda, tokenAccount.PublicKey()) - require.NoError(r, err) - - ata, _, err := solana.FindAssociatedTokenAddress(privateKey.PublicKey(), tokenAccount.PublicKey()) +func (r *E2ERunner) FindOrCreateAssociatedTokenAccount(payer solana.PrivateKey, owner solana.PublicKey, tokenAccount solana.PublicKey) solana.PublicKey { + pdaAta, _, err := solana.FindAssociatedTokenAddress(owner, tokenAccount) require.NoError(r, err) - ataInstruction := associatedtokenaccount.NewCreateInstruction(privateKey.PublicKey(), pda, tokenAccount.PublicKey()).Build() + info, _ := r.SolanaClient.GetAccountInfo(r.Ctx, pdaAta) + if info != nil { + // already exists + return pdaAta + } + // doesn't exist, create it + ataInstruction := associatedtokenaccount.NewCreateInstruction(payer.PublicKey(), owner, tokenAccount).Build() signedTx := r.CreateSignedTransaction( []solana.Instruction{ataInstruction}, - *privateKey, - []solana.PrivateKey{tokenAccount.PrivateKey}, + payer, + []solana.PrivateKey{}, ) // broadcast the transaction and wait for finalization - _, out := r.BroadcastTxSync(signedTx) - r.Logger.Info("pda ata spl logs: %v", out.Meta.LogMessages) + r.BroadcastTxSync(signedTx) - _, err = r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentConfirmed) + return pdaAta +} + +func (r *E2ERunner) DepositSPL(privateKey *solana.PrivateKey, amount uint64, tokenAccount solana.PublicKey, receiver ethcommon.Address, data []byte) solana.Signature { + // ata for pda + pda := r.ComputePdaAddress() + pdaAta := r.FindOrCreateAssociatedTokenAccount(*privateKey, pda, tokenAccount) + + // deployer ata + ata := r.FindOrCreateAssociatedTokenAccount(*privateKey, privateKey.PublicKey(), tokenAccount) + + _, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentConfirmed) require.NoError(r, err) // deposit spl - seed := [][]byte{[]byte("whitelist"), tokenAccount.PublicKey().Bytes()} + seed := [][]byte{[]byte("whitelist"), tokenAccount.Bytes()} whitelistEntryPDA, _, err := solana.FindProgramAddress(seed, r.GatewayProgram) require.NoError(r, err) - depositSPLInstruction := r.CreateDepositSPLInstruction(uint64(500_000), privateKey.PublicKey(), whitelistEntryPDA, tokenAccount.PublicKey(), ata, pdaAta, receiver, data) - signedTx = r.CreateSignedTransaction( + depositSPLInstruction := r.CreateDepositSPLInstruction(amount, privateKey.PublicKey(), whitelistEntryPDA, tokenAccount, ata, pdaAta, receiver, data) + signedTx := r.CreateSignedTransaction( []solana.Instruction{depositSPLInstruction}, *privateKey, []solana.PrivateKey{}, @@ -232,7 +242,7 @@ func (r *E2ERunner) DepositSPL(privateKey *solana.PrivateKey, tokenAccount solan return sig } -func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey, whitelist bool) *solana.Wallet { +func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey, whitelist bool, amountToMint uint64) *solana.Wallet { lamport, err := r.SolanaClient.GetMinimumBalanceForRentExemption(r.Ctx, token.MINT_SIZE, rpc.CommitmentFinalized) require.NoError(r, err) @@ -263,6 +273,27 @@ func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey, whitelist bool) *so _, out := r.BroadcastTxSync(signedTx) 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()) + + _, err = r.SolanaClient.GetTokenAccountBalance(r.Ctx, ata, rpc.CommitmentConfirmed) + require.NoError(r, err) + + mintToInstruction := token.NewMintToInstruction(amountToMint, tokenAccount.PublicKey(), ata, privateKey.PublicKey(), []solana.PublicKey{}).Build() + signedTx = r.CreateSignedTransaction( + []solana.Instruction{mintToInstruction}, + *privateKey, + []solana.PrivateKey{}, + ) + + // broadcast the transaction and wait for finalization + _, out = r.BroadcastTxSync(signedTx) + r.Logger.Info("mint spl logs: %v", out.Meta.LogMessages) + + _, err = r.SolanaClient.GetTokenAccountBalance(r.Ctx, ata, rpc.CommitmentConfirmed) + require.NoError(r, err) + + // optionally whitelist spl token in gateway if whitelist { seed := [][]byte{[]byte("whitelist"), tokenAccount.PublicKey().Bytes()} whitelistEntryPDA, _, err := solana.FindProgramAddress(seed, r.GatewayProgram) @@ -288,40 +319,6 @@ func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey, whitelist bool) *so whitelistEntryInfo, err = r.SolanaClient.GetAccountInfo(r.Ctx, whitelistEntryPDA) require.NoError(r, err) require.NotNil(r, whitelistEntryInfo) - - fmt.Println("minting tokens to deployer...") - - ata, _, err := solana.FindAssociatedTokenAddress(privateKey.PublicKey(), tokenAccount.PublicKey()) - require.NoError(r, err) - - ataInstruction := associatedtokenaccount.NewCreateInstruction(privateKey.PublicKey(), privateKey.PublicKey(), tokenAccount.PublicKey()).Build() - signedTx = r.CreateSignedTransaction( - []solana.Instruction{ataInstruction}, - *privateKey, - []solana.PrivateKey{tokenAccount.PrivateKey}, - ) - // broadcast the transaction and wait for finalization - _, out = r.BroadcastTxSync(signedTx) - r.Logger.Info("ata spl logs: %v", out.Meta.LogMessages) - - _, err = r.SolanaClient.GetTokenAccountBalance(r.Ctx, ata, rpc.CommitmentConfirmed) - require.NoError(r, err) - - amount := uint64(1_000_000) - mintToInstruction := token.NewMintToInstruction(amount, tokenAccount.PublicKey(), ata, privateKey.PublicKey(), []solana.PublicKey{}).Build() - signedTx = r.CreateSignedTransaction( - []solana.Instruction{mintToInstruction}, - *privateKey, - []solana.PrivateKey{}, - ) - - // broadcast the transaction and wait for finalization - _, out = r.BroadcastTxSync(signedTx) - r.Logger.Info("mint spl logs: %v", out.Meta.LogMessages) - - _, err = r.SolanaClient.GetTokenAccountBalance(r.Ctx, ata, rpc.CommitmentConfirmed) - require.NoError(r, err) - } return tokenAccount From 3dfa55b8e8f9f0171188c937c7467e4b9a4606ef Mon Sep 17 00:00:00 2001 From: skosito Date: Thu, 7 Nov 2024 01:05:12 +0100 Subject: [PATCH 04/13] add balance assertions for deposit spl e2e tests --- cmd/zetae2e/local/local.go | 2 +- .../test_solana_deposit_and_call_spl.go | 36 +++++++++++++++++ e2e/e2etests/test_solana_deposit_spl.go | 39 +++++++++++++++++++ e2e/runner/solana.go | 12 ------ 4 files changed, 76 insertions(+), 13 deletions(-) diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 47da6e21a0..76d435e94e 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -412,7 +412,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) { e2etests.TestSolanaWithdrawRestrictedName, // TODO move under admin tests // https://github.com/zeta-chain/node/issues/3085 - // e2etests.TestSolanaWhitelistSPLName, + e2etests.TestSolanaWhitelistSPLName, e2etests.TestSolanaDepositSPLName, e2etests.TestSolanaDepositSPLAndCallName, } diff --git a/e2e/e2etests/test_solana_deposit_and_call_spl.go b/e2e/e2etests/test_solana_deposit_and_call_spl.go index 198a2ba75d..ad67d2445f 100644 --- a/e2e/e2etests/test_solana_deposit_and_call_spl.go +++ b/e2e/e2etests/test_solana_deposit_and_call_spl.go @@ -3,7 +3,9 @@ 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" @@ -25,6 +27,21 @@ func TestSolanaDepositSPLAndCall(r *runner.E2ERunner, args []string) { privKey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String()) require.NoError(r, err) + // get SPL balance for pda and sender atas + pda := r.ComputePdaAddress() + pdaAta := r.FindOrCreateAssociatedTokenAccount(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) + senderBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, senderAta, rpc.CommitmentConfirmed) + require.NoError(r, err) + + // get zrc20 balance for recepient + zrc20BalanceBefore, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, contractAddr) + require.NoError(r, err) + // execute the deposit transaction data := []byte("hello spl tokens") sig := r.DepositSPL(&privKey, uint64(amount), r.SPLAddr, contractAddr, data) @@ -36,4 +53,23 @@ func TestSolanaDepositSPLAndCall(r *runner.E2ERunner, args []string) { // check if example contract has been called, bar value should be set to amount utils.MustHaveCalledExampleContract(r, contract, big.NewInt(500_000)) + + // verify balances are updated + pdaBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentConfirmed) + require.NoError(r, err) + + senderBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, senderAta, rpc.CommitmentConfirmed) + require.NoError(r, err) + + zrc20BalanceAfter, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, contractAddr) + require.NoError(r, err) + + // verify amount is deposited to pda ata + require.Equal(r, parseInt(r, pdaBalanceBefore.Value.Amount)+amount, parseInt(r, pdaBalanceAfter.Value.Amount)) + + // verify amount is substracted from sender ata + require.Equal(r, parseInt(r, senderBalanceBefore.Value.Amount)-amount, parseInt(r, senderBalanceAfter.Value.Amount)) + + // verify amount is minted to receiver + require.Zero(r, zrc20BalanceBefore.Add(zrc20BalanceBefore, big.NewInt(int64(amount))).Cmp(zrc20BalanceAfter)) } diff --git a/e2e/e2etests/test_solana_deposit_spl.go b/e2e/e2etests/test_solana_deposit_spl.go index c72af3c29d..262c1b120b 100644 --- a/e2e/e2etests/test_solana_deposit_spl.go +++ b/e2e/e2etests/test_solana_deposit_spl.go @@ -1,7 +1,11 @@ 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" @@ -17,10 +21,45 @@ func TestSolanaDepositSPL(r *runner.E2ERunner, args []string) { privKey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String()) require.NoError(r, err) + // get SPL balance for pda and sender atas + pda := r.ComputePdaAddress() + pdaAta := r.FindOrCreateAssociatedTokenAccount(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) + senderBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, senderAta, rpc.CommitmentConfirmed) + require.NoError(r, err) + + // get zrc20 balance for recepient + zrc20BalanceBefore, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) + require.NoError(r, err) + + // deposit SPL tokens sig := r.DepositSPL(&privKey, uint64(amount), r.SPLAddr, r.EVMAddress(), nil) // wait for the cctx to be mined cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, sig.String(), r.CctxClient, r.Logger, r.CctxTimeout) r.Logger.CCTX(*cctx, "solana_deposit_spl") utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) + + // verify balances are updated + pdaBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentConfirmed) + require.NoError(r, err) + + senderBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, senderAta, rpc.CommitmentConfirmed) + require.NoError(r, err) + + zrc20BalanceAfter, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) + require.NoError(r, err) + + // verify amount is deposited to pda ata + require.Equal(r, parseInt(r, pdaBalanceBefore.Value.Amount)+amount, parseInt(r, pdaBalanceAfter.Value.Amount)) + + // verify amount is substracted from sender ata + require.Equal(r, parseInt(r, senderBalanceBefore.Value.Amount)-amount, parseInt(r, senderBalanceAfter.Value.Amount)) + + // verify amount is minted to receiver + require.Zero(r, zrc20BalanceBefore.Add(zrc20BalanceBefore, big.NewInt(int64(amount))).Cmp(zrc20BalanceAfter)) } diff --git a/e2e/runner/solana.go b/e2e/runner/solana.go index 7839c6a6bb..69352cf6be 100644 --- a/e2e/runner/solana.go +++ b/e2e/runner/solana.go @@ -218,9 +218,6 @@ func (r *E2ERunner) DepositSPL(privateKey *solana.PrivateKey, amount uint64, tok // deployer ata ata := r.FindOrCreateAssociatedTokenAccount(*privateKey, privateKey.PublicKey(), tokenAccount) - _, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentConfirmed) - require.NoError(r, err) - // deposit spl seed := [][]byte{[]byte("whitelist"), tokenAccount.Bytes()} whitelistEntryPDA, _, err := solana.FindProgramAddress(seed, r.GatewayProgram) @@ -236,9 +233,6 @@ func (r *E2ERunner) DepositSPL(privateKey *solana.PrivateKey, amount uint64, tok sig, out := r.BroadcastTxSync(signedTx) r.Logger.Info("deposit spl logs: %v", out.Meta.LogMessages) - _, err = r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentConfirmed) - require.NoError(r, err) - return sig } @@ -276,9 +270,6 @@ func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey, whitelist bool, amo // minting some tokens to deployer for testing ata := r.FindOrCreateAssociatedTokenAccount(*privateKey, privateKey.PublicKey(), tokenAccount.PublicKey()) - _, err = r.SolanaClient.GetTokenAccountBalance(r.Ctx, ata, rpc.CommitmentConfirmed) - require.NoError(r, err) - mintToInstruction := token.NewMintToInstruction(amountToMint, tokenAccount.PublicKey(), ata, privateKey.PublicKey(), []solana.PublicKey{}).Build() signedTx = r.CreateSignedTransaction( []solana.Instruction{mintToInstruction}, @@ -290,9 +281,6 @@ func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey, whitelist bool, amo _, out = r.BroadcastTxSync(signedTx) r.Logger.Info("mint spl logs: %v", out.Meta.LogMessages) - _, err = r.SolanaClient.GetTokenAccountBalance(r.Ctx, ata, rpc.CommitmentConfirmed) - require.NoError(r, err) - // optionally whitelist spl token in gateway if whitelist { seed := [][]byte{[]byte("whitelist"), tokenAccount.PublicKey().Bytes()} From 7015f827d356e25cb2c1033c07168c572684afc9 Mon Sep 17 00:00:00 2001 From: skosito Date: Thu, 7 Nov 2024 01:24:06 +0100 Subject: [PATCH 05/13] CI fixes --- changelog.md | 1 + e2e/runner/solana.go | 35 ++++++++++++++++---- x/fungible/keeper/foreign_coins.go | 6 ---- x/fungible/keeper/foreign_coins_test.go | 35 ++++++++++---------- zetaclient/chains/solana/observer/inbound.go | 11 ++++-- 5 files changed, 57 insertions(+), 31 deletions(-) diff --git a/changelog.md b/changelog.md index 805c6d4a26..5b9b767877 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,7 @@ ### Features * [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) - deposit spl integration ### Tests * [3075](https://github.com/zeta-chain/node/pull/3075) - ton: withdraw concurrent, deposit & revert. diff --git a/e2e/runner/solana.go b/e2e/runner/solana.go index 69352cf6be..69d74cd9d2 100644 --- a/e2e/runner/solana.go +++ b/e2e/runner/solana.go @@ -9,7 +9,6 @@ import ( associatedtokenaccount "github.com/gagliardetto/solana-go/programs/associated-token-account" "github.com/gagliardetto/solana-go/programs/system" "github.com/gagliardetto/solana-go/programs/token" - "github.com/gagliardetto/solana-go/rpc" "github.com/near/borsh-go" "github.com/stretchr/testify/require" @@ -188,7 +187,11 @@ func (r *E2ERunner) CreateSignedTransaction( return tx } -func (r *E2ERunner) FindOrCreateAssociatedTokenAccount(payer solana.PrivateKey, owner solana.PublicKey, tokenAccount solana.PublicKey) solana.PublicKey { +func (r *E2ERunner) FindOrCreateAssociatedTokenAccount( + payer solana.PrivateKey, + owner solana.PublicKey, + tokenAccount solana.PublicKey, +) solana.PublicKey { pdaAta, _, err := solana.FindAssociatedTokenAddress(owner, tokenAccount) require.NoError(r, err) @@ -210,7 +213,13 @@ func (r *E2ERunner) FindOrCreateAssociatedTokenAccount(payer solana.PrivateKey, return pdaAta } -func (r *E2ERunner) DepositSPL(privateKey *solana.PrivateKey, amount uint64, tokenAccount solana.PublicKey, receiver ethcommon.Address, data []byte) solana.Signature { +func (r *E2ERunner) DepositSPL( + privateKey *solana.PrivateKey, + amount uint64, + tokenAccount solana.PublicKey, + receiver ethcommon.Address, + data []byte, +) solana.Signature { // ata for pda pda := r.ComputePdaAddress() pdaAta := r.FindOrCreateAssociatedTokenAccount(*privateKey, pda, tokenAccount) @@ -223,7 +232,16 @@ func (r *E2ERunner) DepositSPL(privateKey *solana.PrivateKey, amount uint64, tok whitelistEntryPDA, _, err := solana.FindProgramAddress(seed, r.GatewayProgram) require.NoError(r, err) - depositSPLInstruction := r.CreateDepositSPLInstruction(amount, privateKey.PublicKey(), whitelistEntryPDA, tokenAccount, ata, pdaAta, receiver, data) + depositSPLInstruction := r.CreateDepositSPLInstruction( + amount, + privateKey.PublicKey(), + whitelistEntryPDA, + tokenAccount, + ata, + pdaAta, + receiver, + data, + ) signedTx := r.CreateSignedTransaction( []solana.Instruction{depositSPLInstruction}, *privateKey, @@ -270,7 +288,8 @@ func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey, whitelist bool, amo // minting some tokens to deployer for testing ata := r.FindOrCreateAssociatedTokenAccount(*privateKey, privateKey.PublicKey(), tokenAccount.PublicKey()) - mintToInstruction := token.NewMintToInstruction(amountToMint, tokenAccount.PublicKey(), ata, privateKey.PublicKey(), []solana.PublicKey{}).Build() + mintToInstruction := token.NewMintToInstruction(amountToMint, tokenAccount.PublicKey(), ata, privateKey.PublicKey(), []solana.PublicKey{}). + Build() signedTx = r.CreateSignedTransaction( []solana.Instruction{mintToInstruction}, *privateKey, @@ -296,7 +315,11 @@ func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey, whitelist bool, amo } // create 'whitelist_spl_mint' instruction - instruction := r.CreateWhitelistSPLMintInstruction(privateKey.PublicKey(), whitelistEntryPDA, tokenAccount.PublicKey()) + instruction := r.CreateWhitelistSPLMintInstruction( + privateKey.PublicKey(), + whitelistEntryPDA, + tokenAccount.PublicKey(), + ) // create and sign the transaction signedTx := r.CreateSignedTransaction([]solana.Instruction{instruction}, *privateKey, []solana.PrivateKey{}) diff --git a/x/fungible/keeper/foreign_coins.go b/x/fungible/keeper/foreign_coins.go index 3851de88fd..23cd1b6375 100644 --- a/x/fungible/keeper/foreign_coins.go +++ b/x/fungible/keeper/foreign_coins.go @@ -109,14 +109,8 @@ func (k Keeper) GetGasCoinForForeignCoin(ctx sdk.Context, chainID int64) (types. // GetForeignCoinFromAsset returns the foreign coin for a given asset for a given chain func (k Keeper) GetForeignCoinFromAsset(ctx sdk.Context, asset string, chainID int64) (types.ForeignCoins, bool) { - // if !ethcommon.IsHexAddress(asset) { - // return types.ForeignCoins{}, false - // } - // assetAddr := ethcommon.HexToAddress(asset) - foreignCoinList := k.GetAllForeignCoinsForChain(ctx, chainID) for _, coin := range foreignCoinList { - // coinAssetAddr := ethcommon.HexToAddress(coin.Asset) if asset == coin.Asset && coin.ForeignChainId == chainID { return coin, true } diff --git a/x/fungible/keeper/foreign_coins_test.go b/x/fungible/keeper/foreign_coins_test.go index d02c3a29c9..8dda8c0204 100644 --- a/x/fungible/keeper/foreign_coins_test.go +++ b/x/fungible/keeper/foreign_coins_test.go @@ -136,23 +136,24 @@ func TestKeeperGetForeignCoinFromAsset(t *testing.T) { require.False(t, found) }) - t.Run("can get foreign coin with non-checksum address", func(t *testing.T) { - k, ctx, _, _ := keepertest.FungibleKeeper(t) - - setForeignCoins(ctx, k, - types.ForeignCoins{ - Zrc20ContractAddress: sample.EthAddress().String(), - Asset: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - ForeignChainId: 1, - CoinType: coin.CoinType_ERC20, - Name: "foo", - }, - ) - - fc, found := k.GetForeignCoinFromAsset(ctx, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 1) - require.True(t, found) - require.Equal(t, "foo", fc.Name) - }) + // TODO: add validation per chain on setting foreign coin, not when getting + // t.Run("can get foreign coin with non-checksum address", func(t *testing.T) { + // k, ctx, _, _ := keepertest.FungibleKeeper(t) + + // setForeignCoins(ctx, k, + // types.ForeignCoins{ + // Zrc20ContractAddress: sample.EthAddress().String(), + // Asset: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // ForeignChainId: 1, + // CoinType: coin.CoinType_ERC20, + // Name: "foo", + // }, + // ) + + // fc, found := k.GetForeignCoinFromAsset(ctx, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 1) + // require.True(t, found) + // require.Equal(t, "foo", fc.Name) + // }) } func TestKeeperGetAllForeignCoinMap(t *testing.T) { diff --git a/zetaclient/chains/solana/observer/inbound.go b/zetaclient/chains/solana/observer/inbound.go index abe6fb6a09..0a1f82193e 100644 --- a/zetaclient/chains/solana/observer/inbound.go +++ b/zetaclient/chains/solana/observer/inbound.go @@ -398,10 +398,17 @@ func (ob *Observer) GetSignerDeposit(tx *solana.Transaction, inst *solana.Compil return tx.Message.AccountKeys[signerIndex].String(), nil } -func (ob *Observer) GetFromDepositSPLAccounts(tx *solana.Transaction, inst *solana.CompiledInstruction) (string, string, error) { +func (ob *Observer) GetFromDepositSPLAccounts( + tx *solana.Transaction, + inst *solana.CompiledInstruction, +) (string, string, error) { // there should be 7 accounts for a deposit spl instruction if len(inst.Accounts) != solanacontracts.AccountsNumberDepositSPL { - return "", "", fmt.Errorf("want %d accounts, got %d", solanacontracts.AccountsNumberDepositSPL, len(inst.Accounts)) + return "", "", fmt.Errorf( + "want %d accounts, got %d", + solanacontracts.AccountsNumberDepositSPL, + len(inst.Accounts), + ) } // the accounts are [signer, pda, whitelist_entry, mint_account, token_program, from, to] From 30e4571a420f3ee07ec69c2204676f27ae0b0908 Mon Sep 17 00:00:00 2001 From: skosito Date: Thu, 7 Nov 2024 01:25:24 +0100 Subject: [PATCH 06/13] lint fix --- e2e/e2etests/test_solana_deposit_and_call_spl.go | 1 + e2e/e2etests/test_solana_deposit_spl.go | 1 + 2 files changed, 2 insertions(+) diff --git a/e2e/e2etests/test_solana_deposit_and_call_spl.go b/e2e/e2etests/test_solana_deposit_and_call_spl.go index ad67d2445f..369f9a547e 100644 --- a/e2e/e2etests/test_solana_deposit_and_call_spl.go +++ b/e2e/e2etests/test_solana_deposit_and_call_spl.go @@ -44,6 +44,7 @@ func TestSolanaDepositSPLAndCall(r *runner.E2ERunner, args []string) { // execute the deposit transaction data := []byte("hello spl tokens") + // #nosec G115 e2eTest - always in range sig := r.DepositSPL(&privKey, uint64(amount), r.SPLAddr, contractAddr, data) // wait for the cctx to be mined diff --git a/e2e/e2etests/test_solana_deposit_spl.go b/e2e/e2etests/test_solana_deposit_spl.go index 262c1b120b..87503d1254 100644 --- a/e2e/e2etests/test_solana_deposit_spl.go +++ b/e2e/e2etests/test_solana_deposit_spl.go @@ -37,6 +37,7 @@ func TestSolanaDepositSPL(r *runner.E2ERunner, args []string) { require.NoError(r, err) // deposit SPL tokens + // #nosec G115 e2eTest - always in range sig := r.DepositSPL(&privKey, uint64(amount), r.SPLAddr, r.EVMAddress(), nil) // wait for the cctx to be mined From 1b0afb03fcd0f446bcaa9ce653e18e53987f9718 Mon Sep 17 00:00:00 2001 From: skosito Date: Thu, 7 Nov 2024 15:30:44 +0100 Subject: [PATCH 07/13] add inbound parse unit test and cleanup --- .../test_solana_deposit_and_call_spl.go | 6 +- e2e/e2etests/test_solana_deposit_spl.go | 6 +- e2e/runner/solana.go | 24 +---- pkg/contracts/solana/gateway.go | 2 + pkg/contracts/solana/instruction.go | 2 +- x/fungible/keeper/foreign_coins_test.go | 19 ---- zetaclient/chains/solana/observer/inbound.go | 14 +-- .../chains/solana/observer/inbound_test.go | 49 ++++++++++ ...x5z5JfXs5ed8LZqpXUy4VijoU3x15mBd66ZGE.json | 93 +++++++++++++++++++ 9 files changed, 160 insertions(+), 55 deletions(-) create mode 100644 zetaclient/testdata/solana/chain_901_inbound_tx_result_aY8yLDze6nHSRi7L5REozKAZY1aAyPJ6TfibiqQL5JGwgSBkYux5z5JfXs5ed8LZqpXUy4VijoU3x15mBd66ZGE.json diff --git a/e2e/e2etests/test_solana_deposit_and_call_spl.go b/e2e/e2etests/test_solana_deposit_and_call_spl.go index 369f9a547e..764e2c1423 100644 --- a/e2e/e2etests/test_solana_deposit_and_call_spl.go +++ b/e2e/e2etests/test_solana_deposit_and_call_spl.go @@ -38,14 +38,14 @@ func TestSolanaDepositSPLAndCall(r *runner.E2ERunner, args []string) { senderBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, senderAta, rpc.CommitmentConfirmed) require.NoError(r, err) - // get zrc20 balance for recepient + // get zrc20 balance for recipient zrc20BalanceBefore, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, contractAddr) require.NoError(r, err) // execute the deposit transaction data := []byte("hello spl tokens") // #nosec G115 e2eTest - always in range - sig := r.DepositSPL(&privKey, uint64(amount), r.SPLAddr, contractAddr, data) + sig := r.SPLDepositAndCall(&privKey, uint64(amount), r.SPLAddr, contractAddr, data) // wait for the cctx to be mined cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, sig.String(), r.CctxClient, r.Logger, r.CctxTimeout) @@ -68,7 +68,7 @@ func TestSolanaDepositSPLAndCall(r *runner.E2ERunner, args []string) { // verify amount is deposited to pda ata require.Equal(r, parseInt(r, pdaBalanceBefore.Value.Amount)+amount, parseInt(r, pdaBalanceAfter.Value.Amount)) - // verify amount is substracted from sender ata + // verify amount is subtracted from sender ata require.Equal(r, parseInt(r, senderBalanceBefore.Value.Amount)-amount, parseInt(r, senderBalanceAfter.Value.Amount)) // verify amount is minted to receiver diff --git a/e2e/e2etests/test_solana_deposit_spl.go b/e2e/e2etests/test_solana_deposit_spl.go index 87503d1254..16c12051b4 100644 --- a/e2e/e2etests/test_solana_deposit_spl.go +++ b/e2e/e2etests/test_solana_deposit_spl.go @@ -32,13 +32,13 @@ func TestSolanaDepositSPL(r *runner.E2ERunner, args []string) { senderBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, senderAta, rpc.CommitmentConfirmed) require.NoError(r, err) - // get zrc20 balance for recepient + // get zrc20 balance for recipient zrc20BalanceBefore, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) require.NoError(r, err) // deposit SPL tokens // #nosec G115 e2eTest - always in range - sig := r.DepositSPL(&privKey, uint64(amount), r.SPLAddr, r.EVMAddress(), nil) + sig := r.SPLDepositAndCall(&privKey, uint64(amount), r.SPLAddr, r.EVMAddress(), nil) // wait for the cctx to be mined cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, sig.String(), r.CctxClient, r.Logger, r.CctxTimeout) @@ -58,7 +58,7 @@ func TestSolanaDepositSPL(r *runner.E2ERunner, args []string) { // verify amount is deposited to pda ata require.Equal(r, parseInt(r, pdaBalanceBefore.Value.Amount)+amount, parseInt(r, pdaBalanceAfter.Value.Amount)) - // verify amount is substracted from sender ata + // verify amount is subtracted from sender ata require.Equal(r, parseInt(r, senderBalanceBefore.Value.Amount)-amount, parseInt(r, senderBalanceAfter.Value.Amount)) // verify amount is minted to receiver diff --git a/e2e/runner/solana.go b/e2e/runner/solana.go index 69d74cd9d2..71fdde7bbb 100644 --- a/e2e/runner/solana.go +++ b/e2e/runner/solana.go @@ -91,27 +91,6 @@ func (r *E2ERunner) CreateWhitelistSPLMintInstruction( return inst } -/* - - #[account(mut)] - pub signer: Signer<'info>, - - #[account(seeds = [b"meta"], bump)] - pub pda: Account<'info, Pda>, - - #[account(seeds=[b"whitelist", mint_account.key().as_ref()], bump)] - pub whitelist_entry: Account<'info, WhitelistEntry>, // attach whitelist entry to show the mint_account is whitelisted - - pub mint_account: Account<'info, Mint>, - - pub token_program: Program<'info, Token>, - - #[account(mut)] - pub from: Account<'info, TokenAccount>, // this must be owned by signer; normally the ATA of signer - #[account(mut)] - pub to: Account<'info, TokenAccount>, // this must be ATA of PDA -*/ - func (r *E2ERunner) CreateDepositSPLInstruction( amount uint64, signer solana.PublicKey, @@ -213,7 +192,8 @@ func (r *E2ERunner) FindOrCreateAssociatedTokenAccount( return pdaAta } -func (r *E2ERunner) DepositSPL( +// SPLDepositAndCall deposits an amount of SPL tokens and calls a contract (if data is provided) +func (r *E2ERunner) SPLDepositAndCall( privateKey *solana.PrivateKey, amount uint64, tokenAccount solana.PublicKey, diff --git a/pkg/contracts/solana/gateway.go b/pkg/contracts/solana/gateway.go index e0b8eeee7f..a509f90ee8 100644 --- a/pkg/contracts/solana/gateway.go +++ b/pkg/contracts/solana/gateway.go @@ -18,6 +18,8 @@ const ( // [signer, pda, system_program] AccountsNumDeposit = 3 + // AccountsNumberOfDeposit is the number of accounts required for Solana gateway deposit spl instruction + // [signer, pda, whitelist_entry, mint_account, token_program, from, to] AccountsNumberDepositSPL = 7 ) diff --git a/pkg/contracts/solana/instruction.go b/pkg/contracts/solana/instruction.go index 7f7ebc5e41..65b6e6e4c3 100644 --- a/pkg/contracts/solana/instruction.go +++ b/pkg/contracts/solana/instruction.go @@ -34,7 +34,7 @@ type DepositInstructionParams struct { Memo []byte } -// DepositSPLInstructionParams contains the parameters for a gateway deposit instruction +// DepositSPLInstructionParams contains the parameters for a gateway deposit spl instruction type DepositSPLInstructionParams struct { // Discriminator is the unique identifier for the deposit instruction Discriminator [8]byte diff --git a/x/fungible/keeper/foreign_coins_test.go b/x/fungible/keeper/foreign_coins_test.go index 8dda8c0204..3ebc13f2e6 100644 --- a/x/fungible/keeper/foreign_coins_test.go +++ b/x/fungible/keeper/foreign_coins_test.go @@ -135,25 +135,6 @@ func TestKeeperGetForeignCoinFromAsset(t *testing.T) { fc, found = k.GetForeignCoinFromAsset(ctx, gasAsset, 3) require.False(t, found) }) - - // TODO: add validation per chain on setting foreign coin, not when getting - // t.Run("can get foreign coin with non-checksum address", func(t *testing.T) { - // k, ctx, _, _ := keepertest.FungibleKeeper(t) - - // setForeignCoins(ctx, k, - // types.ForeignCoins{ - // Zrc20ContractAddress: sample.EthAddress().String(), - // Asset: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - // ForeignChainId: 1, - // CoinType: coin.CoinType_ERC20, - // Name: "foo", - // }, - // ) - - // fc, found := k.GetForeignCoinFromAsset(ctx, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 1) - // require.True(t, found) - // require.Equal(t, "foo", fc.Name) - // }) } func TestKeeperGetAllForeignCoinMap(t *testing.T) { diff --git a/zetaclient/chains/solana/observer/inbound.go b/zetaclient/chains/solana/observer/inbound.go index 0a1f82193e..09a1c3d99d 100644 --- a/zetaclient/chains/solana/observer/inbound.go +++ b/zetaclient/chains/solana/observer/inbound.go @@ -298,7 +298,7 @@ func (ob *Observer) ParseInboundAsDeposit( event := &clienttypes.InboundEvent{ SenderChainID: ob.Chain().ChainId, Sender: sender, - Receiver: sender, + Receiver: sender, // receiver is pulled out from memo TxOrigin: sender, Amount: inst.Amount, Memo: inst.Memo, @@ -329,17 +329,17 @@ func (ob *Observer) ParseInboundAsDepositSPL( return nil, nil } - // check if the instruction is a deposit or not + // check if the instruction is a deposit spl or not if inst.Discriminator != solanacontracts.DiscriminatorDepositSPL { return nil, nil } - // get the sender address (skip if unable to parse signer address) - sender, spl, err := ob.GetFromDepositSPLAccounts(tx, &instruction) + // get the sender and spl addresses + sender, spl, err := ob.GetSignerAndSPLFromDepositSPLAccounts(tx, &instruction) if err != nil { ob.Logger(). Inbound.Err(err). - Msgf("unable to get signer for sig %s instruction %d", tx.Signatures[0], instructionIndex) + Msgf("unable to get signer and spl for sig %s instruction %d", tx.Signatures[0], instructionIndex) return nil, nil } @@ -347,7 +347,7 @@ func (ob *Observer) ParseInboundAsDepositSPL( event := &clienttypes.InboundEvent{ SenderChainID: ob.Chain().ChainId, Sender: sender, - Receiver: sender, + Receiver: sender, // receiver is pulled out from memo TxOrigin: sender, Amount: inst.Amount, Memo: inst.Memo, @@ -398,7 +398,7 @@ func (ob *Observer) GetSignerDeposit(tx *solana.Transaction, inst *solana.Compil return tx.Message.AccountKeys[signerIndex].String(), nil } -func (ob *Observer) GetFromDepositSPLAccounts( +func (ob *Observer) GetSignerAndSPLFromDepositSPLAccounts( tx *solana.Transaction, inst *solana.CompiledInstruction, ) (string, string, error) { diff --git a/zetaclient/chains/solana/observer/inbound_test.go b/zetaclient/chains/solana/observer/inbound_test.go index 577c10ee9a..e7e06ad7ae 100644 --- a/zetaclient/chains/solana/observer/inbound_test.go +++ b/zetaclient/chains/solana/observer/inbound_test.go @@ -2,6 +2,7 @@ package observer_test import ( "context" + "encoding/hex" "testing" "github.com/stretchr/testify/require" @@ -207,3 +208,51 @@ func Test_ParseInboundAsDeposit(t *testing.T) { require.EqualValues(t, eventExpected, event) }) } + +func Test_ParseInboundAsDepositSPL(t *testing.T) { + // load archived inbound deposit spl tx from localnet result + txHash := "aY8yLDze6nHSRi7L5REozKAZY1aAyPJ6TfibiqQL5JGwgSBkYux5z5JfXs5ed8LZqpXUy4VijoU3x15mBd66ZGE" + + chain := chains.SolanaDevnet + + txResult := testutils.LoadSolanaInboundTxResult(t, TestDataDir, chain.ChainId, txHash, false) + tx, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + database, err := db.NewFromSqliteInMemory(true) + require.NoError(t, err) + + // create observer + chainParams := sample.ChainParams(chain.ChainId) + chainParams.GatewayAddress = testutils.GatewayAddresses[chain.ChainId] + ob, err := observer.NewObserver(chain, nil, *chainParams, nil, nil, 60, database, base.DefaultLogger(), nil) + require.NoError(t, err) + + // expected result + // solana e2e deployer account + sender := "37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ" + // solana e2e user evm account + expectedMemo, err := hex.DecodeString("103fd9224f00ce3013e95629e52dfc31d805d68d") + require.NoError(t, err) + eventExpected := &clienttypes.InboundEvent{ + SenderChainID: chain.ChainId, + Sender: sender, + Receiver: sender, + TxOrigin: sender, + Amount: 500000, + Memo: expectedMemo, + BlockNumber: txResult.Slot, + TxHash: txHash, + Index: 0, // not a EVM smart contract call + CoinType: coin.CoinType_ERC20, + Asset: "4GddKQ7baJpMyKna7bPPnhh7UQtpzfSGL1FgZ31hj4mp", // SPL address + } + + t.Run("should parse inbound event deposit SPL", func(t *testing.T) { + event, err := ob.ParseInboundAsDepositSPL(tx, 0, txResult.Slot) + require.NoError(t, err) + + // check result + require.EqualValues(t, eventExpected, event) + }) +} diff --git a/zetaclient/testdata/solana/chain_901_inbound_tx_result_aY8yLDze6nHSRi7L5REozKAZY1aAyPJ6TfibiqQL5JGwgSBkYux5z5JfXs5ed8LZqpXUy4VijoU3x15mBd66ZGE.json b/zetaclient/testdata/solana/chain_901_inbound_tx_result_aY8yLDze6nHSRi7L5REozKAZY1aAyPJ6TfibiqQL5JGwgSBkYux5z5JfXs5ed8LZqpXUy4VijoU3x15mBd66ZGE.json new file mode 100644 index 0000000000..2fb146776b --- /dev/null +++ b/zetaclient/testdata/solana/chain_901_inbound_tx_result_aY8yLDze6nHSRi7L5REozKAZY1aAyPJ6TfibiqQL5JGwgSBkYux5z5JfXs5ed8LZqpXUy4VijoU3x15mBd66ZGE.json @@ -0,0 +1,93 @@ +{ + "slot": 539, + "blockTime": 1730986363, + "transaction": { + "signatures": [ + "aY8yLDze6nHSRi7L5REozKAZY1aAyPJ6TfibiqQL5JGwgSBkYux5z5JfXs5ed8LZqpXUy4VijoU3x15mBd66ZGE" + ], + "message": { + "accountKeys": [ + "37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ", + "HX8BXQKVw1xpoyDXMr9ujyrrpSYRWcFZ4oB3fRHpM8V7", + "5EVPJV5hjwYGYko2pSykSdJG5ZfBbMEDhYci2PrqQmby", + "9dcAyYG4bawApZocwZSyJBi9Mynf5EuKAJfifXdfkqik", + "8vquQi8xNxxBTsohf1u8xQHYbo7Fv8BEWCJz63RJSaeE", + "4GddKQ7baJpMyKna7bPPnhh7UQtpzfSGL1FgZ31hj4mp", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d" + ], + "header": { + "numRequiredSignatures": 1, + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 5 + }, + "recentBlockhash": "5ZB2NWKwp86t47ojCHPjRyd5vMKSWBdhxqDdtSZVPUj", + "instructions": [ + { + "programIdIndex": 7, + "accounts": [ + 0, + 3, + 4, + 5, + 6, + 1, + 2 + ], + "data": "5JndgWCNHDyr2qQccK9VM1NxJFrVbUDvG2hfAxRC5z6nPPAVPsj7q3A" + } + ] + } + }, + "meta": { + "err": null, + "fee": 5000, + "preBalances": [ + 99992030600, + 2039280, + 2039280, + 1447680, + 946560, + 1461600, + 929020800, + 1141440 + ], + "postBalances": [ + 99992025600, + 2039280, + 2039280, + 1447680, + 946560, + 1461600, + 929020800, + 1141440 + ], + "innerInstructions": [ + { + "index": 0, + "instructions": [] + } + ], + "preTokenBalances": [], + "postTokenBalances": [], + "logMessages": [ + "Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d invoke [1]", + "Program log: Instruction: DepositSplToken", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", + "Program log: Instruction: Transfer Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 181038 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success Program log: deposit spl token successfully", + "Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d consumed 24017 of 200000 compute units", + "Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d success" + ], + "status": { + "Ok": null + }, + "rewards": [], + "loadedAddresses": { + "readonly": [], + "writable": [] + }, + "computeUnitsConsumed": 274902869112 + }, + "version": 0 +} \ No newline at end of file From 6b3808473289405f9c8e1439ac33c5ba10b16bb8 Mon Sep 17 00:00:00 2001 From: skosito Date: Thu, 7 Nov 2024 16:52:24 +0100 Subject: [PATCH 08/13] comment --- e2e/runner/solana.go | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e/runner/solana.go b/e2e/runner/solana.go index 71fdde7bbb..3b01bbcdd7 100644 --- a/e2e/runner/solana.go +++ b/e2e/runner/solana.go @@ -166,6 +166,7 @@ func (r *E2ERunner) CreateSignedTransaction( return tx } +// FindOrCreateAssociatedTokenAccount checks if ata exists, and if not creates it func (r *E2ERunner) FindOrCreateAssociatedTokenAccount( payer solana.PrivateKey, owner solana.PublicKey, From a3f6653674b6f28e5b1df18e06ccdfee5bff4215 Mon Sep 17 00:00:00 2001 From: skosito Date: Thu, 7 Nov 2024 19:15:10 +0100 Subject: [PATCH 09/13] PR comments --- cmd/zetae2e/config/config.go | 2 +- cmd/zetae2e/config/contracts.go | 2 +- cmd/zetae2e/local/local.go | 4 +- e2e/config/config.go | 4 +- e2e/e2etests/e2etests.go | 12 +-- ...ana_deposit_spl.go => test_spl_deposit.go} | 2 +- ...ll_spl.go => test_spl_deposit_and_call.go} | 4 +- e2e/runner/solana.go | 94 +++++++++---------- zetaclient/chains/solana/observer/inbound.go | 14 ++- 9 files changed, 67 insertions(+), 71 deletions(-) rename e2e/e2etests/{test_solana_deposit_spl.go => test_spl_deposit.go} (97%) rename e2e/e2etests/{test_solana_deposit_and_call_spl.go => test_spl_deposit_and_call.go} (95%) diff --git a/cmd/zetae2e/config/config.go b/cmd/zetae2e/config/config.go index abb758215c..65e5418f53 100644 --- a/cmd/zetae2e/config/config.go +++ b/cmd/zetae2e/config/config.go @@ -56,7 +56,7 @@ func RunnerFromConfig( func ExportContractsFromRunner(r *runner.E2ERunner, conf config.Config) config.Config { // copy contracts from deployer runner conf.Contracts.Solana.GatewayProgramID = r.GatewayProgram.String() - conf.Contracts.Solana.SPL = config.DoubleQuotedString(r.SPLAddr.String()) + conf.Contracts.Solana.SPLAddr = config.DoubleQuotedString(r.SPLAddr.String()) conf.Contracts.EVM.ZetaEthAddr = config.DoubleQuotedString(r.ZetaEthAddr.Hex()) conf.Contracts.EVM.ConnectorEthAddr = config.DoubleQuotedString(r.ConnectorEthAddr.Hex()) diff --git a/cmd/zetae2e/config/contracts.go b/cmd/zetae2e/config/contracts.go index 2fbc69563c..5c46cdc047 100644 --- a/cmd/zetae2e/config/contracts.go +++ b/cmd/zetae2e/config/contracts.go @@ -34,7 +34,7 @@ func setContractsFromConfig(r *runner.E2ERunner, conf config.Config) error { r.GatewayProgram = solana.MustPublicKeyFromBase58(c) } - if c := conf.Contracts.Solana.SPL; c != "" { + if c := conf.Contracts.Solana.SPLAddr; c != "" { r.SPLAddr = solana.MustPublicKeyFromBase58(c.String()) } diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 9b24fa8cf4..f01b81ac4e 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -413,8 +413,8 @@ func localE2ETest(cmd *cobra.Command, _ []string) { // TODO move under admin tests // https://github.com/zeta-chain/node/issues/3085 e2etests.TestSolanaWhitelistSPLName, - e2etests.TestSolanaDepositSPLName, - e2etests.TestSolanaDepositSPLAndCallName, + e2etests.TestSPLDepositName, + e2etests.TestSPLDepositAndCallName, } eg.Go(solanaTestRoutine(conf, deployerRunner, verbose, solanaTests...)) } diff --git a/e2e/config/config.go b/e2e/config/config.go index 4351e577f6..15ca4a1f2c 100644 --- a/e2e/config/config.go +++ b/e2e/config/config.go @@ -117,10 +117,10 @@ type Contracts struct { Solana Solana `yaml:"solana"` } -// Solana contains the addresses of predeployed contracts on the Solana chain +// Solana contains the addresses of predeployed contracts and accounts on the Solana chain type Solana struct { GatewayProgramID string `yaml:"gateway_program_id"` - SPL DoubleQuotedString `yaml:"spl"` + SPLAddr DoubleQuotedString `yaml:"spl"` } // EVM contains the addresses of predeployed contracts on the EVM chain diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index ec54933d59..4d0f793509 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -61,8 +61,8 @@ const ( TestSolanaDepositAndCallRefundName = "solana_deposit_and_call_refund" TestSolanaDepositRestrictedName = "solana_deposit_restricted" TestSolanaWithdrawRestrictedName = "solana_withdraw_restricted" - TestSolanaDepositSPLName = "solana_deposit_spl" - TestSolanaDepositSPLAndCallName = "solana_deposit_spl_and_call" + TestSPLDepositName = "spl_deposit" + TestSPLDepositAndCallName = "spl_deposit_and_call" /** * TON tests @@ -465,20 +465,20 @@ var AllE2ETests = []runner.E2ETest{ TestSolanaWhitelistSPL, ), runner.NewE2ETest( - TestSolanaDepositSPLName, + TestSPLDepositName, "deposit SPL into ZEVM", []runner.ArgDefinition{ {Description: "amount of spl tokens", DefaultValue: "500000"}, }, - TestSolanaDepositSPL, + TestSPLDeposit, ), runner.NewE2ETest( - TestSolanaDepositSPLAndCallName, + TestSPLDepositAndCallName, "deposit SPL into ZEVM and call", []runner.ArgDefinition{ {Description: "amount of spl tokens", DefaultValue: "500000"}, }, - TestSolanaDepositSPLAndCall, + TestSPLDepositAndCall, ), /* TON tests diff --git a/e2e/e2etests/test_solana_deposit_spl.go b/e2e/e2etests/test_spl_deposit.go similarity index 97% rename from e2e/e2etests/test_solana_deposit_spl.go rename to e2e/e2etests/test_spl_deposit.go index 16c12051b4..ee5013d16c 100644 --- a/e2e/e2etests/test_solana_deposit_spl.go +++ b/e2e/e2etests/test_spl_deposit.go @@ -13,7 +13,7 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) -func TestSolanaDepositSPL(r *runner.E2ERunner, args []string) { +func TestSPLDeposit(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) amount := parseInt(r, args[0]) diff --git a/e2e/e2etests/test_solana_deposit_and_call_spl.go b/e2e/e2etests/test_spl_deposit_and_call.go similarity index 95% rename from e2e/e2etests/test_solana_deposit_and_call_spl.go rename to e2e/e2etests/test_spl_deposit_and_call.go index 764e2c1423..cdfc94daa1 100644 --- a/e2e/e2etests/test_solana_deposit_and_call_spl.go +++ b/e2e/e2etests/test_spl_deposit_and_call.go @@ -14,7 +14,7 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) -func TestSolanaDepositSPLAndCall(r *runner.E2ERunner, args []string) { +func TestSPLDepositAndCall(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) amount := parseInt(r, args[0]) @@ -53,7 +53,7 @@ func TestSolanaDepositSPLAndCall(r *runner.E2ERunner, args []string) { utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) // check if example contract has been called, bar value should be set to amount - utils.MustHaveCalledExampleContract(r, contract, big.NewInt(500_000)) + utils.MustHaveCalledExampleContract(r, contract, big.NewInt(int64(amount))) // verify balances are updated pdaBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentConfirmed) diff --git a/e2e/runner/solana.go b/e2e/runner/solana.go index 3b01bbcdd7..cf4bcb20c6 100644 --- a/e2e/runner/solana.go +++ b/e2e/runner/solana.go @@ -37,60 +37,53 @@ func (r *E2ERunner) CreateDepositInstruction( data []byte, amount uint64, ) solana.Instruction { - // compute the gateway PDA address - pdaComputed := r.ComputePdaAddress() + pda := r.ComputePdaAddress() programID := r.GatewayProgram - // create 'deposit' instruction - inst := &solana.GenericInstruction{} - accountSlice := []*solana.AccountMeta{} - accountSlice = append(accountSlice, solana.Meta(signer).WRITE().SIGNER()) - accountSlice = append(accountSlice, solana.Meta(pdaComputed).WRITE()) - accountSlice = append(accountSlice, solana.Meta(solana.SystemProgramID)) - inst.ProgID = programID - inst.AccountValues = accountSlice - - var err error - inst.DataBytes, err = borsh.Serialize(solanacontract.DepositInstructionParams{ + depositData, err := borsh.Serialize(solanacontract.DepositInstructionParams{ Discriminator: solanacontract.DiscriminatorDeposit, Amount: amount, Memo: append(receiver.Bytes(), data...), }) require.NoError(r, err) - return inst + return &solana.GenericInstruction{ + ProgID: programID, + DataBytes: depositData, + AccountValues: []*solana.AccountMeta{ + solana.Meta(signer).WRITE().SIGNER(), + solana.Meta(pda).WRITE(), + solana.Meta(solana.SystemProgramID), + }, + } } +// CreateWhitelistSPLMintInstruction creates a 'whitelist_spl_mint' instruction func (r *E2ERunner) CreateWhitelistSPLMintInstruction( - signer solana.PublicKey, - whitelistEntry solana.PublicKey, - whitelistCandidate solana.PublicKey, + signer, whitelistEntry, whitelistCandidate solana.PublicKey, ) solana.Instruction { - // compute the gateway PDA address - pdaComputed := r.ComputePdaAddress() + pda := r.ComputePdaAddress() programID := r.GatewayProgram - // create 'whitelist_spl_mint' instruction - inst := &solana.GenericInstruction{} - accountSlice := []*solana.AccountMeta{} - accountSlice = append(accountSlice, solana.Meta(whitelistEntry).WRITE()) - accountSlice = append(accountSlice, solana.Meta(whitelistCandidate)) - accountSlice = append(accountSlice, solana.Meta(pdaComputed).WRITE()) - accountSlice = append(accountSlice, solana.Meta(signer).WRITE().SIGNER()) - accountSlice = append(accountSlice, solana.Meta(solana.SystemProgramID)) - inst.ProgID = programID - inst.AccountValues = accountSlice - - var err error - inst.DataBytes, err = borsh.Serialize(solanacontract.WhitelistInstructionParams{ + data, err := borsh.Serialize(solanacontract.WhitelistInstructionParams{ Discriminator: solanacontract.DiscriminatorWhitelistSplMint, - // remaining fields are empty because no tss signature is needed if signer is admin account }) require.NoError(r, err) - return inst + return &solana.GenericInstruction{ + ProgID: programID, + DataBytes: data, + AccountValues: []*solana.AccountMeta{ + solana.Meta(whitelistEntry).WRITE(), + solana.Meta(whitelistCandidate), + solana.Meta(pda).WRITE(), + solana.Meta(signer).WRITE().SIGNER(), + solana.Meta(solana.SystemProgramID), + }, + } } +// CreateDepositSPLInstruction creates a 'deposit_spl' instruction func (r *E2ERunner) CreateDepositSPLInstruction( amount uint64, signer solana.PublicKey, @@ -101,32 +94,29 @@ func (r *E2ERunner) CreateDepositSPLInstruction( receiver ethcommon.Address, data []byte, ) solana.Instruction { - // compute the gateway PDA address - pdaComputed := r.ComputePdaAddress() + pda := r.ComputePdaAddress() programID := r.GatewayProgram - // create 'deposit_spl' instruction - inst := &solana.GenericInstruction{} - accountSlice := []*solana.AccountMeta{} - accountSlice = append(accountSlice, solana.Meta(signer).WRITE().SIGNER()) - accountSlice = append(accountSlice, solana.Meta(pdaComputed)) - accountSlice = append(accountSlice, solana.Meta(whitelistEntry)) - accountSlice = append(accountSlice, solana.Meta(mint)) - accountSlice = append(accountSlice, solana.Meta(solana.TokenProgramID)) - accountSlice = append(accountSlice, solana.Meta(from).WRITE()) - accountSlice = append(accountSlice, solana.Meta(to).WRITE()) - inst.ProgID = programID - inst.AccountValues = accountSlice - - var err error - inst.DataBytes, err = borsh.Serialize(solanacontract.DepositInstructionParams{ + depositSPLData, err := borsh.Serialize(solanacontract.DepositInstructionParams{ Discriminator: solanacontract.DiscriminatorDepositSPL, Amount: amount, Memo: append(receiver.Bytes(), data...), }) require.NoError(r, err) - return inst + return &solana.GenericInstruction{ + ProgID: programID, + DataBytes: depositSPLData, + AccountValues: []*solana.AccountMeta{ + solana.Meta(signer).WRITE().SIGNER(), + solana.Meta(pda), + solana.Meta(whitelistEntry), + solana.Meta(mint), + solana.Meta(solana.TokenProgramID), + solana.Meta(from).WRITE(), + solana.Meta(to).WRITE(), + }, + } } // CreateSignedTransaction creates a signed transaction from instructions diff --git a/zetaclient/chains/solana/observer/inbound.go b/zetaclient/chains/solana/observer/inbound.go index 09a1c3d99d..9d73e75106 100644 --- a/zetaclient/chains/solana/observer/inbound.go +++ b/zetaclient/chains/solana/observer/inbound.go @@ -277,7 +277,10 @@ func (ob *Observer) ParseInboundAsDeposit( var inst solanacontracts.DepositInstructionParams err := borsh.Deserialize(&inst, instruction.Data) if err != nil { - return nil, nil + ob.Logger(). + Inbound.Err(err). + Msgf("failed to deserialize instruction data for sig %s instruction %d", tx.Signatures[0], instructionIndex) + return nil, err } // check if the instruction is a deposit or not @@ -291,7 +294,7 @@ func (ob *Observer) ParseInboundAsDeposit( ob.Logger(). Inbound.Err(err). Msgf("unable to get signer for sig %s instruction %d", tx.Signatures[0], instructionIndex) - return nil, nil + return nil, err } // build inbound event @@ -326,7 +329,10 @@ func (ob *Observer) ParseInboundAsDepositSPL( var inst solanacontracts.DepositSPLInstructionParams err := borsh.Deserialize(&inst, instruction.Data) if err != nil { - return nil, nil + ob.Logger(). + Inbound.Err(err). + Msgf("failed to deserialize instruction data for sig %s instruction %d", tx.Signatures[0], instructionIndex) + return nil, err } // check if the instruction is a deposit spl or not @@ -340,7 +346,7 @@ func (ob *Observer) ParseInboundAsDepositSPL( ob.Logger(). Inbound.Err(err). Msgf("unable to get signer and spl for sig %s instruction %d", tx.Signatures[0], instructionIndex) - return nil, nil + return nil, err } // build inbound event From 0341ceca3924ad60f06d2ab3b01f2afa1a955f30 Mon Sep 17 00:00:00 2001 From: skosito Date: Fri, 8 Nov 2024 00:29:12 +0100 Subject: [PATCH 10/13] move inbound parsing to solana pkg --- e2e/runner/solana.go | 21 +- pkg/contracts/solana/gateway.go | 4 +- pkg/contracts/solana/inbound.go | 127 +++++++++++ pkg/contracts/solana/inbound_test.go | 105 ++++++++++ ...axBorK9q1JFVbqnAvu9jXm6ertj7kT7HpYw1j.json | 64 ++++++ ...x5z5JfXs5ed8LZqpXUy4VijoU3x15mBd66ZGE.json | 0 zetaclient/chains/solana/observer/inbound.go | 198 +++--------------- .../chains/solana/observer/inbound_test.go | 93 -------- 8 files changed, 334 insertions(+), 278 deletions(-) create mode 100644 pkg/contracts/solana/inbound.go create mode 100644 pkg/contracts/solana/inbound_test.go create mode 100644 pkg/contracts/solana/testdata/MS3MPLN7hkbyCZFwKqXcg8fmEvQMD74fN6Ps2LSWXJoRxPW5ehaxBorK9q1JFVbqnAvu9jXm6ertj7kT7HpYw1j.json rename zetaclient/testdata/solana/chain_901_inbound_tx_result_aY8yLDze6nHSRi7L5REozKAZY1aAyPJ6TfibiqQL5JGwgSBkYux5z5JfXs5ed8LZqpXUy4VijoU3x15mBd66ZGE.json => pkg/contracts/solana/testdata/aY8yLDze6nHSRi7L5REozKAZY1aAyPJ6TfibiqQL5JGwgSBkYux5z5JfXs5ed8LZqpXUy4VijoU3x15mBd66ZGE.json (100%) diff --git a/e2e/runner/solana.go b/e2e/runner/solana.go index cf4bcb20c6..898ed4d94c 100644 --- a/e2e/runner/solana.go +++ b/e2e/runner/solana.go @@ -37,9 +37,6 @@ func (r *E2ERunner) CreateDepositInstruction( data []byte, amount uint64, ) solana.Instruction { - pda := r.ComputePdaAddress() - programID := r.GatewayProgram - depositData, err := borsh.Serialize(solanacontract.DepositInstructionParams{ Discriminator: solanacontract.DiscriminatorDeposit, Amount: amount, @@ -48,11 +45,11 @@ func (r *E2ERunner) CreateDepositInstruction( require.NoError(r, err) return &solana.GenericInstruction{ - ProgID: programID, + ProgID: r.GatewayProgram, DataBytes: depositData, AccountValues: []*solana.AccountMeta{ solana.Meta(signer).WRITE().SIGNER(), - solana.Meta(pda).WRITE(), + solana.Meta(r.ComputePdaAddress()).WRITE(), solana.Meta(solana.SystemProgramID), }, } @@ -62,21 +59,18 @@ func (r *E2ERunner) CreateDepositInstruction( func (r *E2ERunner) CreateWhitelistSPLMintInstruction( signer, whitelistEntry, whitelistCandidate solana.PublicKey, ) solana.Instruction { - pda := r.ComputePdaAddress() - programID := r.GatewayProgram - data, err := borsh.Serialize(solanacontract.WhitelistInstructionParams{ Discriminator: solanacontract.DiscriminatorWhitelistSplMint, }) require.NoError(r, err) return &solana.GenericInstruction{ - ProgID: programID, + ProgID: r.GatewayProgram, DataBytes: data, AccountValues: []*solana.AccountMeta{ solana.Meta(whitelistEntry).WRITE(), solana.Meta(whitelistCandidate), - solana.Meta(pda).WRITE(), + solana.Meta(r.ComputePdaAddress()).WRITE(), solana.Meta(signer).WRITE().SIGNER(), solana.Meta(solana.SystemProgramID), }, @@ -94,9 +88,6 @@ func (r *E2ERunner) CreateDepositSPLInstruction( receiver ethcommon.Address, data []byte, ) solana.Instruction { - pda := r.ComputePdaAddress() - programID := r.GatewayProgram - depositSPLData, err := borsh.Serialize(solanacontract.DepositInstructionParams{ Discriminator: solanacontract.DiscriminatorDepositSPL, Amount: amount, @@ -105,11 +96,11 @@ func (r *E2ERunner) CreateDepositSPLInstruction( require.NoError(r, err) return &solana.GenericInstruction{ - ProgID: programID, + ProgID: r.GatewayProgram, DataBytes: depositSPLData, AccountValues: []*solana.AccountMeta{ solana.Meta(signer).WRITE().SIGNER(), - solana.Meta(pda), + solana.Meta(r.ComputePdaAddress()), solana.Meta(whitelistEntry), solana.Meta(mint), solana.Meta(solana.TokenProgramID), diff --git a/pkg/contracts/solana/gateway.go b/pkg/contracts/solana/gateway.go index a509f90ee8..12e3a10aa8 100644 --- a/pkg/contracts/solana/gateway.go +++ b/pkg/contracts/solana/gateway.go @@ -16,11 +16,11 @@ const ( // AccountsNumberOfDeposit is the number of accounts required for Solana gateway deposit instruction // [signer, pda, system_program] - AccountsNumDeposit = 3 + accountsNumDeposit = 3 // AccountsNumberOfDeposit is the number of accounts required for Solana gateway deposit spl instruction // [signer, pda, whitelist_entry, mint_account, token_program, from, to] - AccountsNumberDepositSPL = 7 + accountsNumberDepositSPL = 7 ) var ( diff --git a/pkg/contracts/solana/inbound.go b/pkg/contracts/solana/inbound.go new file mode 100644 index 0000000000..766f84aa58 --- /dev/null +++ b/pkg/contracts/solana/inbound.go @@ -0,0 +1,127 @@ +package solana + +import ( + "fmt" + + "github.com/gagliardetto/solana-go" + "github.com/near/borsh-go" +) + +const ( + // MaxSignaturesPerTicker is the maximum number of signatures to process on a ticker + MaxSignaturesPerTicker = 100 +) + +type Deposit struct { + Sender string + Amount uint64 + Memo []byte + Slot uint64 + Asset string +} + +// ParseInboundAsDeposit tries to parse an instruction as a 'deposit'. +// It returns nil if the instruction can't be parsed as a 'deposit'. +func ParseInboundAsDeposit( + tx *solana.Transaction, + instructionIndex int, + slot uint64, +) (*Deposit, error) { + // get instruction by index + instruction := tx.Message.Instructions[instructionIndex] + + // try deserializing instruction as a 'deposit' + var inst DepositInstructionParams + err := borsh.Deserialize(&inst, instruction.Data) + if err != nil { + return nil, nil + } + + // check if the instruction is a deposit or not, if not, skip parsing + if inst.Discriminator != DiscriminatorDeposit { + return nil, nil + } + + // get the sender address (skip if unable to parse signer address) + sender, err := getSignerDeposit(tx, &instruction) + if err != nil { + return nil, err + } + + return &Deposit{ + Sender: sender, + Amount: inst.Amount, + Memo: inst.Memo, + Slot: slot, + Asset: "", // no asset for gas token SOL + }, nil +} + +// ParseInboundAsDepositSPL tries to parse an instruction as a 'deposit_spl_token'. +// It returns nil if the instruction can't be parsed as a 'deposit_spl_token'. +func ParseInboundAsDepositSPL( + tx *solana.Transaction, + instructionIndex int, + slot uint64, +) (*Deposit, error) { + // get instruction by index + instruction := tx.Message.Instructions[instructionIndex] + + // try deserializing instruction as a 'deposit_spl_token' + var inst DepositSPLInstructionParams + err := borsh.Deserialize(&inst, instruction.Data) + if err != nil { + return nil, nil + } + + // check if the instruction is a deposit spl or not, if not, skip parsing + if inst.Discriminator != DiscriminatorDepositSPL { + return nil, nil + } + + // get the sender and spl addresses + sender, spl, err := getSignerAndSPLFromDepositSPLAccounts(tx, &instruction) + if err != nil { + return nil, err + } + + return &Deposit{ + Sender: sender, + Amount: inst.Amount, + Memo: inst.Memo, + Slot: slot, + Asset: spl, + }, nil +} + +// GetSignerDeposit returns the signer address of the deposit instruction +// Note: solana-go is not able to parse the AccountMeta 'is_signer' ATM. This is a workaround. +func getSignerDeposit(tx *solana.Transaction, inst *solana.CompiledInstruction) (string, error) { + // there should be 3 accounts for a deposit instruction + if len(inst.Accounts) != accountsNumDeposit { + return "", fmt.Errorf("want %d accounts, got %d", accountsNumDeposit, len(inst.Accounts)) + } + + // sender is the signer account + return tx.Message.AccountKeys[0].String(), nil +} + +func getSignerAndSPLFromDepositSPLAccounts( + tx *solana.Transaction, + inst *solana.CompiledInstruction, +) (string, string, error) { + // there should be 7 accounts for a deposit spl instruction + if len(inst.Accounts) != accountsNumberDepositSPL { + return "", "", fmt.Errorf( + "want %d accounts, got %d", + accountsNumberDepositSPL, + len(inst.Accounts), + ) + } + + // the accounts are [signer, pda, whitelist_entry, mint_account, token_program, from, to] + signer := tx.Message.AccountKeys[0] + spl := tx.Message.AccountKeys[inst.Accounts[3]] + + return signer.String(), spl.String(), nil +} diff --git a/pkg/contracts/solana/inbound_test.go b/pkg/contracts/solana/inbound_test.go new file mode 100644 index 0000000000..00bb17f994 --- /dev/null +++ b/pkg/contracts/solana/inbound_test.go @@ -0,0 +1,105 @@ +package solana + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/gagliardetto/solana-go/rpc" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/zetaclient/testutils" +) + +func LoadObjectFromJSONFile(t *testing.T, obj interface{}, filename string) { + file, err := os.Open(filepath.Clean(filename)) + require.NoError(t, err) + defer file.Close() + + // read the struct from the file + decoder := json.NewDecoder(file) + err = decoder.Decode(&obj) + require.NoError(t, err) +} + +func LoadSolanaInboundTxResult( + t *testing.T, + txHash string, +) *rpc.GetTransactionResult { + txResult := &rpc.GetTransactionResult{} + LoadObjectFromJSONFile(t, txResult, fmt.Sprintf("testdata/%s.json", txHash)) + return txResult +} + +func Test_ParseInboundAsDeposit(t *testing.T) { + txHash := "MS3MPLN7hkbyCZFwKqXcg8fmEvQMD74fN6Ps2LSWXJoRxPW5ehaxBorK9q1JFVbqnAvu9jXm6ertj7kT7HpYw1j" + chain := chains.SolanaDevnet + + txResult := LoadSolanaInboundTxResult(t, txHash) + tx, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + require.NoError(t, err) + + // create observer + chainParams := sample.ChainParams(chain.ChainId) + chainParams.GatewayAddress = testutils.GatewayAddresses[chain.ChainId] + require.NoError(t, err) + + // expected result + sender := "AS48jKNQsDGkEdDvfwu1QpqjtqbCadrAq9nGXjFmdX3Z" + expectedDeposit := &Deposit{ + Sender: sender, + Amount: 100000, + Memo: []byte("0x7F8ae2ABb69A558CE6bAd546f25F0464D9e09e5B4955a3F38ff86ae92A914445099caa8eA2B9bA32"), + Slot: txResult.Slot, + Asset: "", + } + + t.Run("should parse inbound event deposit SOL", func(t *testing.T) { + deposit, err := ParseInboundAsDeposit(tx, 0, txResult.Slot) + require.NoError(t, err) + + // check result + require.EqualValues(t, expectedDeposit, deposit) + }) +} + +func Test_ParseInboundAsDepositSPL(t *testing.T) { + txHash := "aY8yLDze6nHSRi7L5REozKAZY1aAyPJ6TfibiqQL5JGwgSBkYux5z5JfXs5ed8LZqpXUy4VijoU3x15mBd66ZGE" + chain := chains.SolanaDevnet + + txResult := LoadSolanaInboundTxResult(t, txHash) + tx, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + // create observer + chainParams := sample.ChainParams(chain.ChainId) + chainParams.GatewayAddress = testutils.GatewayAddresses[chain.ChainId] + + // expected result + // solana e2e deployer account + sender := "37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ" + // solana e2e user evm account + expectedMemo, err := hex.DecodeString("103fd9224f00ce3013e95629e52dfc31d805d68d") + require.NoError(t, err) + expectedDeposit := &Deposit{ + Sender: sender, + Amount: 500000, + Memo: expectedMemo, + Slot: txResult.Slot, + Asset: "4GddKQ7baJpMyKna7bPPnhh7UQtpzfSGL1FgZ31hj4mp", // SPL address + } + + t.Run("should parse inbound event deposit SPL", func(t *testing.T) { + deposit, err := ParseInboundAsDepositSPL(tx, 0, txResult.Slot) + require.NoError(t, err) + + // check result + require.EqualValues(t, expectedDeposit, deposit) + }) +} diff --git a/pkg/contracts/solana/testdata/MS3MPLN7hkbyCZFwKqXcg8fmEvQMD74fN6Ps2LSWXJoRxPW5ehaxBorK9q1JFVbqnAvu9jXm6ertj7kT7HpYw1j.json b/pkg/contracts/solana/testdata/MS3MPLN7hkbyCZFwKqXcg8fmEvQMD74fN6Ps2LSWXJoRxPW5ehaxBorK9q1JFVbqnAvu9jXm6ertj7kT7HpYw1j.json new file mode 100644 index 0000000000..cf7edb3b81 --- /dev/null +++ b/pkg/contracts/solana/testdata/MS3MPLN7hkbyCZFwKqXcg8fmEvQMD74fN6Ps2LSWXJoRxPW5ehaxBorK9q1JFVbqnAvu9jXm6ertj7kT7HpYw1j.json @@ -0,0 +1,64 @@ +{ + "slot": 321701608, + "blockTime": 1724732369, + "transaction": { + "signatures": [ + "MS3MPLN7hkbyCZFwKqXcg8fmEvQMD74fN6Ps2LSWXJoRxPW5ehaxBorK9q1JFVbqnAvu9jXm6ertj7kT7HpYw1j" + ], + "message": { + "accountKeys": [ + "AS48jKNQsDGkEdDvfwu1QpqjtqbCadrAq9nGXjFmdX3Z", + "9dcAyYG4bawApZocwZSyJBi9Mynf5EuKAJfifXdfkqik", + "11111111111111111111111111111111", + "94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d" + ], + "header": { + "numRequiredSignatures": 1, + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 2 + }, + "recentBlockhash": "41txNvjedo2eu6aAofQfyLskAcgtrtgch9RpqnrKcv1a", + "instructions": [ + { + "programIdIndex": 3, + "accounts": [0, 1, 2], + "data": "4ALHYcAj3zFsNjmfeq7nDK1E8BsxRQRzhLjrqzmjYzL97Qkiz4rP1iQePmFAehfFEET7uczYLhhEVhtndBYNNm6ekHSkgsLzYDeSD2JSudHa6D5tqhVGjvXZ7qEouPiy9eptZfuYHE9X" + } + ] + } + }, + "meta": { + "err": null, + "fee": 5000, + "preBalances": [9999364000, 1001447680, 1, 1141440], + "postBalances": [9999259000, 1001547680, 1, 1141440], + "innerInstructions": [ + { + "index": 0, + "instructions": [ + { + "programIdIndex": 2, + "accounts": [0, 1], + "data": "3Bxs4ThwQbE4vyj5" + } + ] + } + ], + "preTokenBalances": [], + "postTokenBalances": [], + "logMessages": [ + "Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d invoke [1]", + "Program log: Instruction: Deposit", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program log: AS48jKNQsDGkEdDvfwu1QpqjtqbCadrAq9nGXjFmdX3Z deposits 100000 lamports to PDA", + "Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d consumed 17006 of 200000 compute units", + "Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d success" + ], + "status": { "Ok": null }, + "rewards": [], + "loadedAddresses": { "readonly": [], "writable": [] }, + "computeUnitsConsumed": 17006 + }, + "version": 0 +} diff --git a/zetaclient/testdata/solana/chain_901_inbound_tx_result_aY8yLDze6nHSRi7L5REozKAZY1aAyPJ6TfibiqQL5JGwgSBkYux5z5JfXs5ed8LZqpXUy4VijoU3x15mBd66ZGE.json b/pkg/contracts/solana/testdata/aY8yLDze6nHSRi7L5REozKAZY1aAyPJ6TfibiqQL5JGwgSBkYux5z5JfXs5ed8LZqpXUy4VijoU3x15mBd66ZGE.json similarity index 100% rename from zetaclient/testdata/solana/chain_901_inbound_tx_result_aY8yLDze6nHSRi7L5REozKAZY1aAyPJ6TfibiqQL5JGwgSBkYux5z5JfXs5ed8LZqpXUy4VijoU3x15mBd66ZGE.json rename to pkg/contracts/solana/testdata/aY8yLDze6nHSRi7L5REozKAZY1aAyPJ6TfibiqQL5JGwgSBkYux5z5JfXs5ed8LZqpXUy4VijoU3x15mBd66ZGE.json diff --git a/zetaclient/chains/solana/observer/inbound.go b/zetaclient/chains/solana/observer/inbound.go index 9d73e75106..bd0e9a98b7 100644 --- a/zetaclient/chains/solana/observer/inbound.go +++ b/zetaclient/chains/solana/observer/inbound.go @@ -9,7 +9,6 @@ import ( cosmosmath "cosmossdk.io/math" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" - "github.com/near/borsh-go" "github.com/pkg/errors" "github.com/rs/zerolog" @@ -197,12 +196,24 @@ func (ob *Observer) FilterInboundEvents(txResult *rpc.GetTransactionResult) ([]* // try parsing the instruction as a 'deposit' if not seen yet if !seenDeposit { - event, err := ob.ParseInboundAsDeposit(tx, i, txResult.Slot) + deposit, err := solanacontracts.ParseInboundAsDeposit(tx, i, txResult.Slot) if err != nil { return nil, errors.Wrap(err, "error ParseInboundAsDeposit") - } else if event != nil { + } else if deposit != nil { seenDeposit = true - events = append(events, event) + events = append(events, &clienttypes.InboundEvent{ + SenderChainID: ob.Chain().ChainId, + Sender: deposit.Sender, + Receiver: deposit.Sender, // receiver is pulled out from memo + TxOrigin: deposit.Sender, + Amount: deposit.Amount, + Memo: deposit.Memo, + BlockNumber: deposit.Slot, // instead of using block, Solana explorer uses slot for indexing + TxHash: tx.Signatures[0].String(), + Index: 0, // hardcode to 0 for Solana, not a EVM smart contract call + CoinType: coin.CoinType_Gas, + Asset: deposit.Asset, + }) ob.Logger().Inbound.Info(). Msgf("FilterInboundEvents: deposit detected in sig %s instruction %d", tx.Signatures[0], i) } @@ -213,12 +224,24 @@ func (ob *Observer) FilterInboundEvents(txResult *rpc.GetTransactionResult) ([]* // try parsing the instruction as a 'deposit_spl_token' if not seen yet if !seenDepositSPL { - event, err := ob.ParseInboundAsDepositSPL(tx, i, txResult.Slot) + deposit, err := solanacontracts.ParseInboundAsDepositSPL(tx, i, txResult.Slot) if err != nil { return nil, errors.Wrap(err, "error ParseInboundAsDepositSPL") - } else if event != nil { + } else if deposit != nil { seenDepositSPL = true - events = append(events, event) + events = append(events, &clienttypes.InboundEvent{ + SenderChainID: ob.Chain().ChainId, + Sender: deposit.Sender, + Receiver: deposit.Sender, // receiver is pulled out from memo + TxOrigin: deposit.Sender, + Amount: deposit.Amount, + Memo: deposit.Memo, + BlockNumber: deposit.Slot, // instead of using block, Solana explorer uses slot for indexing + TxHash: tx.Signatures[0].String(), + Index: 0, // hardcode to 0 for Solana, not a EVM smart contract call + CoinType: coin.CoinType_ERC20, + Asset: deposit.Asset, + }) ob.Logger().Inbound.Info(). Msgf("FilterInboundEvents: SPL deposit detected in sig %s instruction %d", tx.Signatures[0], i) } @@ -262,164 +285,3 @@ func (ob *Observer) BuildInboundVoteMsgFromEvent(event *clienttypes.InboundEvent 0, // not a smart contract call ) } - -// ParseInboundAsDeposit tries to parse an instruction as a 'deposit'. -// It returns nil if the instruction can't be parsed as a 'deposit'. -func (ob *Observer) ParseInboundAsDeposit( - tx *solana.Transaction, - instructionIndex int, - slot uint64, -) (*clienttypes.InboundEvent, error) { - // get instruction by index - instruction := tx.Message.Instructions[instructionIndex] - - // try deserializing instruction as a 'deposit' - var inst solanacontracts.DepositInstructionParams - err := borsh.Deserialize(&inst, instruction.Data) - if err != nil { - ob.Logger(). - Inbound.Err(err). - Msgf("failed to deserialize instruction data for sig %s instruction %d", tx.Signatures[0], instructionIndex) - return nil, err - } - - // check if the instruction is a deposit or not - if inst.Discriminator != solanacontracts.DiscriminatorDeposit { - return nil, nil - } - - // get the sender address (skip if unable to parse signer address) - sender, err := ob.GetSignerDeposit(tx, &instruction) - if err != nil { - ob.Logger(). - Inbound.Err(err). - Msgf("unable to get signer for sig %s instruction %d", tx.Signatures[0], instructionIndex) - return nil, err - } - - // build inbound event - event := &clienttypes.InboundEvent{ - SenderChainID: ob.Chain().ChainId, - Sender: sender, - Receiver: sender, // receiver is pulled out from memo - TxOrigin: sender, - Amount: inst.Amount, - Memo: inst.Memo, - BlockNumber: slot, // instead of using block, Solana explorer uses slot for indexing - TxHash: tx.Signatures[0].String(), - Index: 0, // hardcode to 0 for Solana, not a EVM smart contract call - CoinType: coin.CoinType_Gas, - Asset: "", // no asset for gas token SOL - } - - return event, nil -} - -// ParseInboundAsDepositSPL tries to parse an instruction as a 'deposit_spl_token'. -// It returns nil if the instruction can't be parsed as a 'deposit_spl_token'. -func (ob *Observer) ParseInboundAsDepositSPL( - tx *solana.Transaction, - instructionIndex int, - slot uint64, -) (*clienttypes.InboundEvent, error) { - // get instruction by index - instruction := tx.Message.Instructions[instructionIndex] - - // try deserializing instruction as a 'deposit_spl_token' - var inst solanacontracts.DepositSPLInstructionParams - err := borsh.Deserialize(&inst, instruction.Data) - if err != nil { - ob.Logger(). - Inbound.Err(err). - Msgf("failed to deserialize instruction data for sig %s instruction %d", tx.Signatures[0], instructionIndex) - return nil, err - } - - // check if the instruction is a deposit spl or not - if inst.Discriminator != solanacontracts.DiscriminatorDepositSPL { - return nil, nil - } - - // get the sender and spl addresses - sender, spl, err := ob.GetSignerAndSPLFromDepositSPLAccounts(tx, &instruction) - if err != nil { - ob.Logger(). - Inbound.Err(err). - Msgf("unable to get signer and spl for sig %s instruction %d", tx.Signatures[0], instructionIndex) - return nil, err - } - - // build inbound event - event := &clienttypes.InboundEvent{ - SenderChainID: ob.Chain().ChainId, - Sender: sender, - Receiver: sender, // receiver is pulled out from memo - TxOrigin: sender, - Amount: inst.Amount, - Memo: inst.Memo, - BlockNumber: slot, // instead of using block, Solana explorer uses slot for indexing - TxHash: tx.Signatures[0].String(), - Index: 0, // hardcode to 0 for Solana, not a EVM smart contract call - CoinType: coin.CoinType_ERC20, - Asset: spl, - } - - return event, nil -} - -// GetSignerDeposit returns the signer address of the deposit instruction -// Note: solana-go is not able to parse the AccountMeta 'is_signer' ATM. This is a workaround. -func (ob *Observer) GetSignerDeposit(tx *solana.Transaction, inst *solana.CompiledInstruction) (string, error) { - // there should be 3 accounts for a deposit instruction - if len(inst.Accounts) != solanacontracts.AccountsNumDeposit { - return "", fmt.Errorf("want %d accounts, got %d", solanacontracts.AccountsNumDeposit, len(inst.Accounts)) - } - - // the accounts are [signer, pda, system_program] - signerIndex, pdaIndex, systemIndex := -1, -1, -1 - - // try to find the indexes of all above accounts - for _, accIndex := range inst.Accounts { - // #nosec G701 always in range - accIndexInt := int(accIndex) - accKey := tx.Message.AccountKeys[accIndexInt] - - switch accKey { - case ob.pda: - pdaIndex = accIndexInt - case solana.SystemProgramID: - systemIndex = accIndexInt - default: - // the last remaining account is the signer - signerIndex = accIndexInt - } - } - - // all above accounts must be found - if signerIndex == -1 || pdaIndex == -1 || systemIndex == -1 { - return "", fmt.Errorf("invalid accounts for deposit instruction") - } - - // sender is the signer account - return tx.Message.AccountKeys[signerIndex].String(), nil -} - -func (ob *Observer) GetSignerAndSPLFromDepositSPLAccounts( - tx *solana.Transaction, - inst *solana.CompiledInstruction, -) (string, string, error) { - // there should be 7 accounts for a deposit spl instruction - if len(inst.Accounts) != solanacontracts.AccountsNumberDepositSPL { - return "", "", fmt.Errorf( - "want %d accounts, got %d", - solanacontracts.AccountsNumberDepositSPL, - len(inst.Accounts), - ) - } - - // the accounts are [signer, pda, whitelist_entry, mint_account, token_program, from, to] - signer := tx.Message.AccountKeys[0] - spl := tx.Message.AccountKeys[inst.Accounts[3]] - - return signer.String(), spl.String(), nil -} diff --git a/zetaclient/chains/solana/observer/inbound_test.go b/zetaclient/chains/solana/observer/inbound_test.go index e7e06ad7ae..c38d3ae281 100644 --- a/zetaclient/chains/solana/observer/inbound_test.go +++ b/zetaclient/chains/solana/observer/inbound_test.go @@ -2,7 +2,6 @@ package observer_test import ( "context" - "encoding/hex" "testing" "github.com/stretchr/testify/require" @@ -164,95 +163,3 @@ func Test_BuildInboundVoteMsgFromEvent(t *testing.T) { require.Nil(t, msg) }) } - -func Test_ParseInboundAsDeposit(t *testing.T) { - // load archived inbound deposit tx result - // https://explorer.solana.com/tx/MS3MPLN7hkbyCZFwKqXcg8fmEvQMD74fN6Ps2LSWXJoRxPW5ehaxBorK9q1JFVbqnAvu9jXm6ertj7kT7HpYw1j?cluster=devnet - txHash := "MS3MPLN7hkbyCZFwKqXcg8fmEvQMD74fN6Ps2LSWXJoRxPW5ehaxBorK9q1JFVbqnAvu9jXm6ertj7kT7HpYw1j" - chain := chains.SolanaDevnet - - txResult := testutils.LoadSolanaInboundTxResult(t, TestDataDir, chain.ChainId, txHash, false) - tx, err := txResult.Transaction.GetTransaction() - require.NoError(t, err) - - database, err := db.NewFromSqliteInMemory(true) - require.NoError(t, err) - - // create observer - chainParams := sample.ChainParams(chain.ChainId) - chainParams.GatewayAddress = testutils.GatewayAddresses[chain.ChainId] - ob, err := observer.NewObserver(chain, nil, *chainParams, nil, nil, 60, database, base.DefaultLogger(), nil) - require.NoError(t, err) - - // expected result - sender := "AS48jKNQsDGkEdDvfwu1QpqjtqbCadrAq9nGXjFmdX3Z" - eventExpected := &clienttypes.InboundEvent{ - SenderChainID: chain.ChainId, - Sender: sender, - Receiver: sender, - TxOrigin: sender, - Amount: 100000, - Memo: []byte("0x7F8ae2ABb69A558CE6bAd546f25F0464D9e09e5B4955a3F38ff86ae92A914445099caa8eA2B9bA32"), - BlockNumber: txResult.Slot, - TxHash: txHash, - Index: 0, // not a EVM smart contract call - CoinType: coin.CoinType_Gas, - Asset: "", // no asset for gas token SOL - } - - t.Run("should parse inbound event deposit SOL", func(t *testing.T) { - event, err := ob.ParseInboundAsDeposit(tx, 0, txResult.Slot) - require.NoError(t, err) - - // check result - require.EqualValues(t, eventExpected, event) - }) -} - -func Test_ParseInboundAsDepositSPL(t *testing.T) { - // load archived inbound deposit spl tx from localnet result - txHash := "aY8yLDze6nHSRi7L5REozKAZY1aAyPJ6TfibiqQL5JGwgSBkYux5z5JfXs5ed8LZqpXUy4VijoU3x15mBd66ZGE" - - chain := chains.SolanaDevnet - - txResult := testutils.LoadSolanaInboundTxResult(t, TestDataDir, chain.ChainId, txHash, false) - tx, err := txResult.Transaction.GetTransaction() - require.NoError(t, err) - - database, err := db.NewFromSqliteInMemory(true) - require.NoError(t, err) - - // create observer - chainParams := sample.ChainParams(chain.ChainId) - chainParams.GatewayAddress = testutils.GatewayAddresses[chain.ChainId] - ob, err := observer.NewObserver(chain, nil, *chainParams, nil, nil, 60, database, base.DefaultLogger(), nil) - require.NoError(t, err) - - // expected result - // solana e2e deployer account - sender := "37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ" - // solana e2e user evm account - expectedMemo, err := hex.DecodeString("103fd9224f00ce3013e95629e52dfc31d805d68d") - require.NoError(t, err) - eventExpected := &clienttypes.InboundEvent{ - SenderChainID: chain.ChainId, - Sender: sender, - Receiver: sender, - TxOrigin: sender, - Amount: 500000, - Memo: expectedMemo, - BlockNumber: txResult.Slot, - TxHash: txHash, - Index: 0, // not a EVM smart contract call - CoinType: coin.CoinType_ERC20, - Asset: "4GddKQ7baJpMyKna7bPPnhh7UQtpzfSGL1FgZ31hj4mp", // SPL address - } - - t.Run("should parse inbound event deposit SPL", func(t *testing.T) { - event, err := ob.ParseInboundAsDepositSPL(tx, 0, txResult.Slot) - require.NoError(t, err) - - // check result - require.EqualValues(t, eventExpected, event) - }) -} From 498f4ea9a9e428302d0fa7257ced8fd8163b1cc6 Mon Sep 17 00:00:00 2001 From: skosito Date: Fri, 8 Nov 2024 01:10:03 +0100 Subject: [PATCH 11/13] refactor e2e test zrc20 deployment --- cmd/zetae2e/local/local.go | 10 ++- cmd/zetae2e/stress.go | 6 +- e2e/e2etests/test_solana_whitelist_spl.go | 2 +- e2e/runner/setup_solana.go | 2 +- e2e/runner/setup_zeta.go | 21 +++---- e2e/runner/solana.go | 4 +- e2e/txserver/zeta_tx_server.go | 76 ++++++++++++++--------- 7 files changed, 76 insertions(+), 45 deletions(-) diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index f01b81ac4e..ec8fe4bc64 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -227,7 +227,15 @@ func localE2ETest(cmd *cobra.Command, _ []string) { // NOTE: v2 (gateway) setup called here because system contract needs to be set first, then gateway, then zrc20 deployerRunner.SetZEVMContractsV2() - deployerRunner.SetZEVMZRC20s() + zrc20Deployment := txserver.ZRC20Deployment{ + ERC20Addr: deployerRunner.ERC20Addr, + SPLAddr: nil, + } + if testSolana { + zrc20Deployment.SPLAddr = deployerRunner.SPLAddr.ToPointer() + } + + deployerRunner.SetZEVMZRC20s(zrc20Deployment) // Update the chain params to use v2 contract for ERC20Custody // TODO: this function should be removed and the chain params should be directly set to use v2 contract diff --git a/cmd/zetae2e/stress.go b/cmd/zetae2e/stress.go index 66e8cd700d..b2ab4a9154 100644 --- a/cmd/zetae2e/stress.go +++ b/cmd/zetae2e/stress.go @@ -22,6 +22,7 @@ import ( zetae2econfig "github.com/zeta-chain/node/cmd/zetae2e/config" "github.com/zeta-chain/node/cmd/zetae2e/local" "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/txserver" "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/testutil" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" @@ -142,7 +143,10 @@ func StressTest(cmd *cobra.Command, _ []string) { case "LOCAL": // deploy and set zevm contract e2eTest.SetZEVMSystemContracts() - e2eTest.SetZEVMZRC20s() + e2eTest.SetZEVMZRC20s(txserver.ZRC20Deployment{ + ERC20Addr: e2eTest.ERC20Addr, + SPLAddr: nil, // no stress tests for solana atm + }) // deposit on ZetaChain e2eTest.DepositEther() diff --git a/e2e/e2etests/test_solana_whitelist_spl.go b/e2e/e2etests/test_solana_whitelist_spl.go index 98f577fd56..fadff22805 100644 --- a/e2e/e2etests/test_solana_whitelist_spl.go +++ b/e2e/e2etests/test_solana_whitelist_spl.go @@ -20,7 +20,7 @@ func TestSolanaWhitelistSPL(r *runner.E2ERunner, _ []string) { require.NoError(r, err) // deploy SPL token, but don't whitelist in gateway - spl := r.DeploySPL(&privkey, false, uint64(1_000_000)) + spl := r.DeploySPL(&privkey, false) // check that whitelist entry doesn't exist for this spl seed := [][]byte{[]byte("whitelist"), spl.PublicKey().Bytes()} diff --git a/e2e/runner/setup_solana.go b/e2e/runner/setup_solana.go index c8e22e3299..a7589d6af1 100644 --- a/e2e/runner/setup_solana.go +++ b/e2e/runner/setup_solana.go @@ -89,7 +89,7 @@ func (r *E2ERunner) SetupSolana(deployerPrivateKey string) { require.NoError(r, err) // deploy test spl - tokenAccount := r.DeploySPL(&privkey, true, uint64(1_000_000)) + tokenAccount := r.DeploySPL(&privkey, true) r.SPLAddr = tokenAccount.PublicKey() } diff --git a/e2e/runner/setup_zeta.go b/e2e/runner/setup_zeta.go index 7aa9d8d0c2..072957346c 100644 --- a/e2e/runner/setup_zeta.go +++ b/e2e/runner/setup_zeta.go @@ -167,7 +167,7 @@ func (r *E2ERunner) SetZEVMSystemContracts() { } // SetZEVMZRC20s set ZRC20 for the ZEVM -func (r *E2ERunner) SetZEVMZRC20s() { +func (r *E2ERunner) SetZEVMZRC20s(zrc20Deployment txserver.ZRC20Deployment) { r.Logger.Print("⚙️ deploying ZRC20s on ZEVM") startTime := time.Now() defer func() { @@ -175,24 +175,23 @@ func (r *E2ERunner) SetZEVMZRC20s() { }() // deploy system contracts and ZRC20 contracts on ZetaChain - erc20zrc20Addr, splzrc20Addr, err := r.ZetaTxServer.DeployZRC20s( - e2eutils.OperationalPolicyName, - e2eutils.AdminPolicyName, - r.ERC20Addr.Hex(), - r.SPLAddr.String(), + deployedZRC20Addresses, err := r.ZetaTxServer.DeployZRC20s( + zrc20Deployment, r.skipChainOperations, ) require.NoError(r, err) // Set ERC20ZRC20Addr - r.ERC20ZRC20Addr = ethcommon.HexToAddress(erc20zrc20Addr) + r.ERC20ZRC20Addr = deployedZRC20Addresses.ERC20ZRC20Addr r.ERC20ZRC20, err = zrc20.NewZRC20(r.ERC20ZRC20Addr, r.ZEVMClient) require.NoError(r, err) - // Set SPLZRC20Addr - r.SPLZRC20Addr = ethcommon.HexToAddress(splzrc20Addr) - r.SPLZRC20, err = zrc20.NewZRC20(r.SPLZRC20Addr, r.ZEVMClient) - require.NoError(r, err) + // Set SPLZRC20Addr if set + if deployedZRC20Addresses.SPLZRC20Addr != (ethcommon.Address{}) { + r.SPLZRC20Addr = deployedZRC20Addresses.SPLZRC20Addr + r.SPLZRC20, err = zrc20.NewZRC20(r.SPLZRC20Addr, r.ZEVMClient) + require.NoError(r, err) + } // set ZRC20 contracts r.SetupETHZRC20() diff --git a/e2e/runner/solana.go b/e2e/runner/solana.go index 898ed4d94c..542968938d 100644 --- a/e2e/runner/solana.go +++ b/e2e/runner/solana.go @@ -216,7 +216,7 @@ func (r *E2ERunner) SPLDepositAndCall( return sig } -func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey, whitelist bool, amountToMint uint64) *solana.Wallet { +func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey, whitelist bool) *solana.Wallet { lamport, err := r.SolanaClient.GetMinimumBalanceForRentExemption(r.Ctx, token.MINT_SIZE, rpc.CommitmentFinalized) require.NoError(r, err) @@ -250,7 +250,7 @@ func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey, whitelist bool, amo // minting some tokens to deployer for testing ata := r.FindOrCreateAssociatedTokenAccount(*privateKey, privateKey.PublicKey(), tokenAccount.PublicKey()) - mintToInstruction := token.NewMintToInstruction(amountToMint, tokenAccount.PublicKey(), ata, privateKey.PublicKey(), []solana.PublicKey{}). + mintToInstruction := token.NewMintToInstruction(uint64(1_000_000), tokenAccount.PublicKey(), ata, privateKey.PublicKey(), []solana.PublicKey{}). Build() signedTx = r.CreateSignedTransaction( []solana.Instruction{mintToInstruction}, diff --git a/e2e/txserver/zeta_tx_server.go b/e2e/txserver/zeta_tx_server.go index 0031a14762..0419348859 100644 --- a/e2e/txserver/zeta_tx_server.go +++ b/e2e/txserver/zeta_tx_server.go @@ -33,6 +33,8 @@ import ( stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" "github.com/cosmos/gogoproto/proto" + "github.com/ethereum/go-ethereum/common" + "github.com/gagliardetto/solana-go" "github.com/samber/lo" "github.com/zeta-chain/ethermint/crypto/hd" etherminttypes "github.com/zeta-chain/ethermint/types" @@ -56,6 +58,18 @@ type SystemContractAddresses struct { UniswapV2FactoryAddr, UniswapV2RouterAddr, ZEVMConnectorAddr, WZETAAddr, ERC20zrc20Addr string } +// ZRC20Deployment configures deployment of ZRC20 contracts +type ZRC20Deployment struct { + ERC20Addr common.Address + SPLAddr *solana.PublicKey // if nil - no SPL ZRC20 is deployed +} + +// ZRC20Addresses contains the addresses of deployed ZRC20 contracts +type ZRC20Addresses struct { + ERC20ZRC20Addr common.Address + SPLZRC20Addr common.Address +} + // EmissionsPoolAddress is the address of the emissions pool // This address is constant for all networks because it is derived from emissions name const EmissionsPoolAddress = "zeta1w43fn2ze2wyhu5hfmegr6vp52c3dgn0srdgymy" @@ -383,38 +397,38 @@ func (zts ZetaTxServer) DeploySystemContracts( // DeployZRC20s deploys the ZRC20 contracts // returns the addresses of erc20 and spl zrc20 func (zts ZetaTxServer) DeployZRC20s( - accountOperational, accountAdmin, erc20Addr string, splAddr string, + zrc20Deployment ZRC20Deployment, skipChain func(chainID int64) bool, -) (string, string, error) { +) (*ZRC20Addresses, error) { // retrieve account - accOperational, err := zts.clientCtx.Keyring.Key(accountOperational) + accOperational, err := zts.clientCtx.Keyring.Key(utils.OperationalPolicyName) if err != nil { - return "", "", err + return nil, err } addrOperational, err := accOperational.GetAddress() if err != nil { - return "", "", err + return nil, err } - accAdmin, err := zts.clientCtx.Keyring.Key(accountAdmin) + accAdmin, err := zts.clientCtx.Keyring.Key(utils.AdminPolicyName) if err != nil { - return "", "", err + return nil, err } addrAdmin, err := accAdmin.GetAddress() if err != nil { - return "", "", err + return nil, err } // authorization for deploying new ZRC20 has changed from accountOperational to accountAdmin in v19 // we use this query to check the current authorization for the message // if pre v19 the query is not implement and authorization is operational - deployerAccount := accountAdmin + deployerAccount := utils.AdminPolicyName deployerAddr := addrAdmin.String() authorization, preV19, err := zts.fetchMessagePermissions(&fungibletypes.MsgDeployFungibleCoinZRC20{}) if err != nil { - return "", "", fmt.Errorf("failed to fetch message permissions: %s", err.Error()) + return nil, fmt.Errorf("failed to fetch message permissions: %s", err.Error()) } if preV19 || authorization == authoritytypes.PolicyType_groupOperational { - deployerAccount = accountOperational + deployerAccount = utils.OperationalPolicyName deployerAddr = addrOperational.String() } @@ -449,16 +463,6 @@ func (zts ZetaTxServer) DeployZRC20s( coin.CoinType_Gas, 100000, ), - fungibletypes.NewMsgDeployFungibleCoinZRC20( - deployerAddr, - splAddr, - chains.SolanaLocalnet.ChainId, - 9, - "USDT", - "USDT", - coin.CoinType_ERC20, - 100000, - ), fungibletypes.NewMsgDeployFungibleCoinZRC20( deployerAddr, "", @@ -471,7 +475,7 @@ func (zts ZetaTxServer) DeployZRC20s( ), fungibletypes.NewMsgDeployFungibleCoinZRC20( deployerAddr, - erc20Addr, + zrc20Deployment.ERC20Addr.Hex(), chains.GoerliLocalnet.ChainId, 6, "USDT", @@ -481,6 +485,19 @@ func (zts ZetaTxServer) DeployZRC20s( ), } + if zrc20Deployment.SPLAddr != nil { + deployMsgs = append(deployMsgs, fungibletypes.NewMsgDeployFungibleCoinZRC20( + deployerAddr, + zrc20Deployment.SPLAddr.String(), + chains.SolanaLocalnet.ChainId, + 9, + "USDT", + "USDT", + coin.CoinType_ERC20, + 100000, + )) + } + // apply skipChain filter and convert to sdk.Msg deployMsgsIface := lo.FilterMap( deployMsgs, @@ -494,12 +511,12 @@ func (zts ZetaTxServer) DeployZRC20s( res, err := zts.BroadcastTx(deployerAccount, deployMsgsIface...) if err != nil { - return "", "", fmt.Errorf("deploy zrc20s: %w", err) + return nil, fmt.Errorf("deploy zrc20s: %w", err) } deployedEvents, ok := EventsOfType[*fungibletypes.EventZRC20Deployed](res.Events) if !ok { - return "", "", fmt.Errorf("no EventZRC20Deployed in %s", res.TxHash) + return nil, fmt.Errorf("no EventZRC20Deployed in %s", res.TxHash) } zrc20Addrs := lo.Map(deployedEvents, func(ev *fungibletypes.EventZRC20Deployed, _ int) string { @@ -508,7 +525,7 @@ func (zts ZetaTxServer) DeployZRC20s( err = zts.InitializeLiquidityCaps(zrc20Addrs...) if err != nil { - return "", "", fmt.Errorf("initialize liquidity cap: %w", err) + return nil, fmt.Errorf("initialize liquidity cap: %w", err) } // find erc20 zrc20 @@ -516,7 +533,7 @@ func (zts ZetaTxServer) DeployZRC20s( return ev.ChainId == chains.GoerliLocalnet.ChainId && ev.CoinType == coin.CoinType_ERC20 }) if !ok { - return "", "", fmt.Errorf("unable to find erc20 zrc20") + return nil, fmt.Errorf("unable to find erc20 zrc20") } // find spl zrc20 @@ -524,10 +541,13 @@ func (zts ZetaTxServer) DeployZRC20s( return ev.ChainId == chains.SolanaLocalnet.ChainId && ev.CoinType == coin.CoinType_ERC20 }) if !ok { - return "", "", fmt.Errorf("unable to find spl zrc20") + return nil, fmt.Errorf("unable to find spl zrc20") } - return erc20zrc20.Contract, splzrc20.Contract, nil + return &ZRC20Addresses{ + ERC20ZRC20Addr: common.HexToAddress(erc20zrc20.Contract), + SPLZRC20Addr: common.HexToAddress(splzrc20.Contract), + }, nil } // FundEmissionsPool funds the emissions pool with the given amount From ca6c029e6ffae6debba465c46bc89ed6cd075302 Mon Sep 17 00:00:00 2001 From: skosito Date: Fri, 8 Nov 2024 01:46:13 +0100 Subject: [PATCH 12/13] fix e2e tests --- e2e/txserver/zeta_tx_server.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/e2e/txserver/zeta_tx_server.go b/e2e/txserver/zeta_tx_server.go index 0419348859..146dda4cf2 100644 --- a/e2e/txserver/zeta_tx_server.go +++ b/e2e/txserver/zeta_tx_server.go @@ -537,16 +537,21 @@ func (zts ZetaTxServer) DeployZRC20s( } // find spl zrc20 - splzrc20, ok := lo.Find(deployedEvents, func(ev *fungibletypes.EventZRC20Deployed) bool { - return ev.ChainId == chains.SolanaLocalnet.ChainId && ev.CoinType == coin.CoinType_ERC20 - }) - if !ok { - return nil, fmt.Errorf("unable to find spl zrc20") + splzrc20Addr := common.Address{} + if zrc20Deployment.SPLAddr != nil { + splzrc20, ok := lo.Find(deployedEvents, func(ev *fungibletypes.EventZRC20Deployed) bool { + return ev.ChainId == chains.SolanaLocalnet.ChainId && ev.CoinType == coin.CoinType_ERC20 + }) + if !ok { + return nil, fmt.Errorf("unable to find spl zrc20") + } + + splzrc20Addr = common.HexToAddress(splzrc20.Contract) } return &ZRC20Addresses{ ERC20ZRC20Addr: common.HexToAddress(erc20zrc20.Contract), - SPLZRC20Addr: common.HexToAddress(splzrc20.Contract), + SPLZRC20Addr: splzrc20Addr, }, nil } From db7874bb8f2321f6f2028d551858ba441273883c Mon Sep 17 00:00:00 2001 From: skosito Date: Fri, 8 Nov 2024 13:14:40 +0100 Subject: [PATCH 13/13] Update changelog.md Co-authored-by: Lucas Bertrand --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index bb783771f0..5bf715963a 100644 --- a/changelog.md +++ b/changelog.md @@ -5,7 +5,7 @@ ### Features * [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) - deposit spl integration +* [3124](https://github.com/zeta-chain/node/pull/3124) - integrate SPL deposits ### Refactor * [3122](https://github.com/zeta-chain/node/pull/3122) - improve & refactor zetaclientd cli