Skip to content

Commit

Permalink
feat: withdraw SOL from ZEVM to Solana (zeta-chain#2560)
Browse files Browse the repository at this point in the history
* port Panruo's outbound code and make compile pass

* make SOL withdraw e2e test passing

* make solana outbound tracker goroutine working

* allow solana gateway address to update

* integrate sub methods of SignMsgWithdraw and SignWithdrawTx

* initiate solana outbound tracker reporter

* implemented solana outbound tx verification

* use the amount in tx result for outbound vote

* post Solana priority fee to zetacore

* config Solana fee payer private key

* resolve 1st wave of comments in PR review

* resolve 2nd wave of comments

* refactor IsOutboundProcessed as VoteOutboundIfConfirmed; move outbound tracker iteration logic into ProcessOutboundTrackers sub method

* resolve 3rd wave of PR feedback

* added description to explain what do we do about the outbound tracker txHash

* add additional error message; add additional method comment

* fix gosec err

* replace contex.TODO() with context.Background()
  • Loading branch information
ws4charlie authored Aug 1, 2024
1 parent aed7caa commit ef2147a
Show file tree
Hide file tree
Showing 61 changed files with 3,008 additions and 474 deletions.
3 changes: 2 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
* [2518](https://github.com/zeta-chain/node/pull/2518) - add support for Solana address in zetacore
* [2483](https://github.com/zeta-chain/node/pull/2483) - add priorityFee (gasTipCap) gas to the state
* [2567](https://github.com/zeta-chain/node/pull/2567) - add sign latency metric to zetaclient (zetaclient_sign_latency)
* [2524](https://github.com/zeta-chain/node/pull/2524) - add inscription envolop parsing
* [2524](https://github.com/zeta-chain/node/pull/2524) - add inscription envolop parsing
* [2560](https://github.com/zeta-chain/node/pull/2560) - add support for Solana SOL token withdraw

### Refactor

Expand Down
14 changes: 13 additions & 1 deletion cmd/zetaclientd/init.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"path"

"github.com/rs/zerolog"
"github.com/spf13/cobra"

Expand Down Expand Up @@ -36,6 +38,7 @@ type initArguments struct {
KeyringBackend string
HsmMode bool
HsmHotKey string
SolanaKey string
}

func init() {
Expand Down Expand Up @@ -69,6 +72,7 @@ func init() {
InitCmd.Flags().BoolVar(&initArgs.HsmMode, "hsm-mode", false, "enable hsm signer, default disabled")
InitCmd.Flags().
StringVar(&initArgs.HsmHotKey, "hsm-hotkey", "hsm-hotkey", "name of hotkey associated with hardware security module")
InitCmd.Flags().StringVar(&initArgs.SolanaKey, "solana-key", "solana-key.json", "solana key file name")
}

func Initialize(_ *cobra.Command, _ []string) error {
Expand Down Expand Up @@ -106,8 +110,16 @@ func Initialize(_ *cobra.Command, _ []string) error {
configData.KeyringBackend = config.KeyringBackend(initArgs.KeyringBackend)
configData.HsmMode = initArgs.HsmMode
configData.HsmHotKey = initArgs.HsmHotKey
configData.SolanaKeyFile = initArgs.SolanaKey
configData.ComplianceConfig = testutils.ComplianceConfigTest()

//Save config file
// Save solana test fee payer key file
keyFile := path.Join(rootArgs.zetaCoreHome, initArgs.SolanaKey)
err = createSolanaTestKeyFile(keyFile)
if err != nil {
return err
}

// Save config file
return config.Save(&configData, rootArgs.zetaCoreHome)
}
37 changes: 37 additions & 0 deletions cmd/zetaclientd/solana_test_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package main

import (
"encoding/json"
"os"
)

// solanaTestKey is a local test private key for Solana
// TODO: use separate keys for each zetaclient in Solana E2E tests
// https://github.com/zeta-chain/node/issues/2614
var solanaTestKey = []uint8{
199, 16, 63, 28, 125, 103, 131, 13, 6, 94, 68, 109, 13, 68, 132, 17,
71, 33, 216, 51, 49, 103, 146, 241, 245, 162, 90, 228, 71, 177, 32, 199,
31, 128, 124, 2, 23, 207, 48, 93, 141, 113, 91, 29, 196, 95, 24, 137,
170, 194, 90, 4, 124, 113, 12, 222, 166, 209, 119, 19, 78, 20, 99, 5,
}

// createSolanaTestKeyFile creates a solana test key json file
func createSolanaTestKeyFile(keyFile string) error {
// marshal the byte array to JSON
keyBytes, err := json.Marshal(solanaTestKey)
if err != nil {
return err
}

// create file (or overwrite if it already exists)
// #nosec G304 -- for E2E testing purposes only
file, err := os.Create(keyFile)
if err != nil {
return err
}
defer file.Close()

// write the key bytes to the file
_, err = file.Write(keyBytes)
return err
}
2 changes: 0 additions & 2 deletions cmd/zetaclientd/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,3 @@ func CreateZetacoreClient(cfg config.Config, hotkeyPassword string, logger zerol

return client, nil
}

// TODO
6 changes: 5 additions & 1 deletion cmd/zetae2e/local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,11 @@ func localE2ETest(cmd *cobra.Command, _ []string) {
logger.Print("❌ solana client is nil, maybe solana rpc is not set")
os.Exit(1)
}
eg.Go(solanaTestRoutine(conf, deployerRunner, verbose, e2etests.TestSolanaDepositName))
solanaTests := []string{
e2etests.TestSolanaDepositName,
e2etests.TestSolanaWithdrawName,
}
eg.Go(solanaTestRoutine(conf, deployerRunner, verbose, solanaTests...))
}

// while tests are executed, monitor blocks in parallel to check if system txs are on top and they have biggest priority
Expand Down
5 changes: 5 additions & 0 deletions contrib/localnet/orchestrator/start-zetae2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ address=$(yq -r '.additional_accounts.user_bitcoin.evm_address' config.yml)
echo "funding bitcoin tester address ${address} with 10000 Ether"
geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545

# unlock solana tester accounts
address=$(yq -r '.additional_accounts.user_solana.evm_address' config.yml)
echo "funding solana tester address ${address} with 10000 Ether"
geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545

# unlock ethers tester accounts
address=$(yq -r '.additional_accounts.user_ether.evm_address' config.yml)
echo "funding ether tester address ${address} with 10000 Ether"
Expand Down
3 changes: 3 additions & 0 deletions contrib/localnet/scripts/start-zetacored.sh
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,9 @@ then
# bitcoin tester
address=$(yq -r '.additional_accounts.user_bitcoin.bech32_address' /root/config.yml)
zetacored add-genesis-account "$address" 100000000000000000000000000azeta
# solana tester
address=$(yq -r '.additional_accounts.user_solana.bech32_address' /root/config.yml)
zetacored add-genesis-account "$address" 100000000000000000000000000azeta
# ethers tester
address=$(yq -r '.additional_accounts.user_ether.bech32_address' /root/config.yml)
zetacored add-genesis-account "$address" 100000000000000000000000000azeta
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Broadcast message UpdateGatewayContract to update the gateway contract address

```
zetacored tx fungible update-gateway-contract [contract-address] [flags]
zetacored tx fungible update-gateway-contract [contract-address] [flags]
```

### Options
Expand Down
13 changes: 11 additions & 2 deletions e2e/e2etests/e2etests.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ const (
/*
Solana tests
*/
TestSolanaDepositName = "solana_deposit"
TestSolanaDepositName = "solana_deposit"
TestSolanaWithdrawName = "solana_withdraw"

/*
Bitcoin tests
Expand Down Expand Up @@ -338,10 +339,18 @@ var AllE2ETests = []runner.E2ETest{
TestSolanaDepositName,
"deposit SOL into ZEVM",
[]runner.ArgDefinition{
{Description: "amount in SOL", DefaultValue: "0.1"},
{Description: "amount in lamport", DefaultValue: "13370000"},
},
TestSolanaDeposit,
),
runner.NewE2ETest(
TestSolanaWithdrawName,
"withdraw SOL from ZEVM",
[]runner.ArgDefinition{
{Description: "amount in lamport", DefaultValue: "1336000"},
},
TestSolanaWithdraw,
),
/*
Bitcoin tests
*/
Expand Down
17 changes: 13 additions & 4 deletions e2e/e2etests/test_solana_deposit.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
package e2etests

import (
"math/big"

"github.com/gagliardetto/solana-go"
"github.com/stretchr/testify/require"

"github.com/zeta-chain/zetacore/e2e/runner"
"github.com/zeta-chain/zetacore/e2e/utils"
crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types"
)

func TestSolanaDeposit(r *runner.E2ERunner, _ []string) {
func TestSolanaDeposit(r *runner.E2ERunner, args []string) {
require.Len(r, args, 1)

// parse deposit amount (in lamports)
// #nosec G115 e2e - always in range
depositAmount := big.NewInt(int64(parseInt(r, args[0])))

// load deployer private key
privkey := solana.MustPrivateKeyFromBase58(r.Account.SolanaPrivateKey.String())
privkey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String())
require.NoError(r, err)

// create 'deposit' instruction
amount := uint64(13370000)
instruction := r.CreateDepositInstruction(privkey.PublicKey(), r.EVMAddress(), amount)
instruction := r.CreateDepositInstruction(privkey.PublicKey(), r.EVMAddress(), depositAmount.Uint64())

// create and sign the transaction
signedTx := r.CreateSignedTransaction([]solana.Instruction{instruction}, privkey)
Expand Down
47 changes: 47 additions & 0 deletions e2e/e2etests/test_solana_withdraw.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package e2etests

import (
"math/big"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/gagliardetto/solana-go"
"github.com/stretchr/testify/require"

"github.com/zeta-chain/zetacore/e2e/runner"
)

func TestSolanaWithdraw(r *runner.E2ERunner, args []string) {
require.Len(r, args, 1)

// print balanceAfter of from address
balanceBefore, err := r.SOLZRC20.BalanceOf(&bind.CallOpts{}, r.ZEVMAuth.From)
require.NoError(r, err)
r.Logger.Info("from address %s balance of SOL before: %d", r.ZEVMAuth.From, balanceBefore)

// parse withdraw amount (in lamports), approve amount is 1 SOL
approvedAmount := new(big.Int).SetUint64(solana.LAMPORTS_PER_SOL)
// #nosec G115 e2e - always in range
withdrawAmount := big.NewInt(int64(parseInt(r, args[0])))
require.Equal(
r,
-1,
withdrawAmount.Cmp(approvedAmount),
"Withdrawal amount must be less than the approved amount (1e9).",
)

// load deployer private key
privkey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String())
require.NoError(r, err)

// withdraw
r.WithdrawSOLZRC20(privkey.PublicKey(), withdrawAmount, approvedAmount)

// print balance of from address after withdraw
balanceAfter, err := r.SOLZRC20.BalanceOf(&bind.CallOpts{}, r.ZEVMAuth.From)
require.NoError(r, err)
r.Logger.Info("from address %s balance of SOL after: %d", r.ZEVMAuth.From, balanceAfter)

// check if the balance is reduced correctly
amountReduced := new(big.Int).Sub(balanceBefore, balanceAfter)
require.True(r, amountReduced.Cmp(withdrawAmount) >= 0, "balance is not reduced correctly")
}
32 changes: 11 additions & 21 deletions e2e/runner/setup_solana.go
Original file line number Diff line number Diff line change
@@ -1,45 +1,35 @@
package runner

import (
"time"

ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"
"github.com/near/borsh-go"
"github.com/stretchr/testify/require"

"github.com/zeta-chain/zetacore/pkg/chains"
solanacontract "github.com/zeta-chain/zetacore/pkg/contract/solana"
solanacontracts "github.com/zeta-chain/zetacore/pkg/contracts/solana"
)

// SetupSolanaAccount imports the deployer's private key
func (r *E2ERunner) SetupSolanaAccount() {
r.Logger.Print("⚙️ setting up Solana account")
startTime := time.Now()
defer func() {
r.Logger.Print("✅ Solana account setup in %s", time.Since(startTime))
}()

r.SetSolanaAddress()
}

// SetSolanaAddress imports the deployer's private key
func (r *E2ERunner) SetSolanaAddress() {
privateKey := solana.MustPrivateKeyFromBase58(r.Account.SolanaPrivateKey.String())
privateKey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String())
require.NoError(r, err)
r.SolanaDeployerAddress = privateKey.PublicKey()

r.Logger.Info("SolanaDeployerAddress: %s", r.SolanaDeployerAddress)
}

// SetSolanaContracts set Solana contracts
func (r *E2ERunner) SetSolanaContracts(deployerPrivateKey string) {
r.Logger.Print("⚙️ setting up Solana contracts")
r.Logger.Print("⚙️ initializing gateway program on Solana")

// set Solana contracts
r.GatewayProgram = solana.MustPublicKeyFromBase58(solanacontract.SolanaGatewayProgramID)
r.GatewayProgram = solana.MustPublicKeyFromBase58(solanacontracts.SolanaGatewayProgramID)

// get deployer account balance
privkey := solana.MustPrivateKeyFromBase58(deployerPrivateKey)
privkey, err := solana.PrivateKeyFromBase58(deployerPrivateKey)
require.NoError(r, err)
bal, err := r.SolanaClient.GetBalance(r.Ctx, privkey.PublicKey(), rpc.CommitmentFinalized)
require.NoError(r, err)
r.Logger.Info("deployer address: %s, balance: %f SOL", privkey.PublicKey().String(), float64(bal.Value)/1e9)
Expand All @@ -57,8 +47,8 @@ func (r *E2ERunner) SetSolanaContracts(deployerPrivateKey string) {
inst.ProgID = r.GatewayProgram
inst.AccountValues = accountSlice

inst.DataBytes, err = borsh.Serialize(solanacontract.InitializeParams{
Discriminator: solanacontract.DiscriminatorInitialize(),
inst.DataBytes, err = borsh.Serialize(solanacontracts.InitializeParams{
Discriminator: solanacontracts.DiscriminatorInitialize(),
TssAddress: r.TSSAddress,
ChainID: uint64(chains.SolanaLocalnet.ChainId),
})
Expand All @@ -76,7 +66,7 @@ func (r *E2ERunner) SetSolanaContracts(deployerPrivateKey string) {
require.NoError(r, err)

// deserialize the PDA info
pda := solanacontract.PdaInfo{}
pda := solanacontracts.PdaInfo{}
err = borsh.Deserialize(&pda, pdaInfo.Bytes())
require.NoError(r, err)
tssAddress := ethcommon.BytesToAddress(pda.TssAddress[:])
Expand Down
28 changes: 27 additions & 1 deletion e2e/runner/solana.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package runner

import (
"math/big"
"time"

ethcommon "github.com/ethereum/go-ethereum/common"
Expand All @@ -9,7 +10,9 @@ import (
"github.com/near/borsh-go"
"github.com/stretchr/testify/require"

solanacontract "github.com/zeta-chain/zetacore/pkg/contract/solana"
"github.com/zeta-chain/zetacore/e2e/utils"
solanacontract "github.com/zeta-chain/zetacore/pkg/contracts/solana"
crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types"
)

// ComputePdaAddress computes the PDA address for the gateway program
Expand Down Expand Up @@ -105,3 +108,26 @@ func (r *E2ERunner) BroadcastTxSync(tx *solana.Transaction) (solana.Signature, *

return sig, out
}

// WithdrawSOLZRC20 withdraws an amount of ZRC20 SOL tokens
func (r *E2ERunner) WithdrawSOLZRC20(to solana.PublicKey, amount *big.Int, approveAmount *big.Int) {
// approve
tx, err := r.SOLZRC20.Approve(r.ZEVMAuth, r.SOLZRC20Addr, approveAmount)
require.NoError(r, err)
receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout)
utils.RequireTxSuccessful(r, receipt)

// withdraw
tx, err = r.SOLZRC20.Withdraw(r.ZEVMAuth, []byte(to.String()), amount)
require.NoError(r, err)
r.Logger.EVMTransaction(*tx, "withdraw")

// wait for tx receipt
receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout)
utils.RequireTxSuccessful(r, receipt)
r.Logger.Info("Receipt txhash %s status %d", receipt.TxHash, receipt.Status)

// wait for the cctx to be mined
cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout)
utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined)
}
2 changes: 1 addition & 1 deletion e2e/txserver/zeta_tx_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ func (zts ZetaTxServer) DeploySystemContractsAndZRC20(
100000,
))
if err != nil {
return SystemContractAddresses{}, fmt.Errorf("failed to deploy btc zrc20: %s", err.Error())
return SystemContractAddresses{}, fmt.Errorf("failed to deploy sol zrc20: %s", err.Error())
}

// deploy erc20 zrc20
Expand Down
Loading

0 comments on commit ef2147a

Please sign in to comment.