Skip to content

Commit e158344

Browse files
ws4charlielumtis
authored andcommitted
feat: withdraw SOL from ZEVM to Solana (#2560)
* 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()
1 parent 5a9e339 commit e158344

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+3007
-473
lines changed

changelog.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
* [2518](https://github.com/zeta-chain/node/pull/2518) - add support for Solana address in zetacore
4444
* [2483](https://github.com/zeta-chain/node/pull/2483) - add priorityFee (gasTipCap) gas to the state
4545
* [2567](https://github.com/zeta-chain/node/pull/2567) - add sign latency metric to zetaclient (zetaclient_sign_latency)
46-
* [2524](https://github.com/zeta-chain/node/pull/2524) - add inscription envolop parsing
46+
* [2524](https://github.com/zeta-chain/node/pull/2524) - add inscription envolop parsing
47+
* [2560](https://github.com/zeta-chain/node/pull/2560) - add support for Solana SOL token withdraw
4748

4849
### Refactor
4950

cmd/zetaclientd/init.go

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package main
22

33
import (
4+
"path"
5+
46
"github.com/rs/zerolog"
57
"github.com/spf13/cobra"
68

@@ -36,6 +38,7 @@ type initArguments struct {
3638
KeyringBackend string
3739
HsmMode bool
3840
HsmHotKey string
41+
SolanaKey string
3942
}
4043

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

7478
func Initialize(_ *cobra.Command, _ []string) error {
@@ -106,8 +110,16 @@ func Initialize(_ *cobra.Command, _ []string) error {
106110
configData.KeyringBackend = config.KeyringBackend(initArgs.KeyringBackend)
107111
configData.HsmMode = initArgs.HsmMode
108112
configData.HsmHotKey = initArgs.HsmHotKey
113+
configData.SolanaKeyFile = initArgs.SolanaKey
109114
configData.ComplianceConfig = testutils.ComplianceConfigTest()
110115

111-
//Save config file
116+
// Save solana test fee payer key file
117+
keyFile := path.Join(rootArgs.zetaCoreHome, initArgs.SolanaKey)
118+
err = createSolanaTestKeyFile(keyFile)
119+
if err != nil {
120+
return err
121+
}
122+
123+
// Save config file
112124
return config.Save(&configData, rootArgs.zetaCoreHome)
113125
}

cmd/zetaclientd/solana_test_key.go

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
)
7+
8+
// solanaTestKey is a local test private key for Solana
9+
// TODO: use separate keys for each zetaclient in Solana E2E tests
10+
// https://github.com/zeta-chain/node/issues/2614
11+
var solanaTestKey = []uint8{
12+
199, 16, 63, 28, 125, 103, 131, 13, 6, 94, 68, 109, 13, 68, 132, 17,
13+
71, 33, 216, 51, 49, 103, 146, 241, 245, 162, 90, 228, 71, 177, 32, 199,
14+
31, 128, 124, 2, 23, 207, 48, 93, 141, 113, 91, 29, 196, 95, 24, 137,
15+
170, 194, 90, 4, 124, 113, 12, 222, 166, 209, 119, 19, 78, 20, 99, 5,
16+
}
17+
18+
// createSolanaTestKeyFile creates a solana test key json file
19+
func createSolanaTestKeyFile(keyFile string) error {
20+
// marshal the byte array to JSON
21+
keyBytes, err := json.Marshal(solanaTestKey)
22+
if err != nil {
23+
return err
24+
}
25+
26+
// create file (or overwrite if it already exists)
27+
// #nosec G304 -- for E2E testing purposes only
28+
file, err := os.Create(keyFile)
29+
if err != nil {
30+
return err
31+
}
32+
defer file.Close()
33+
34+
// write the key bytes to the file
35+
_, err = file.Write(keyBytes)
36+
return err
37+
}

cmd/zetaclientd/utils.go

-2
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,3 @@ func CreateZetacoreClient(cfg config.Config, hotkeyPassword string, logger zerol
4141

4242
return client, nil
4343
}
44-
45-
// TODO

cmd/zetae2e/local/local.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,11 @@ func localE2ETest(cmd *cobra.Command, _ []string) {
330330
logger.Print("❌ solana client is nil, maybe solana rpc is not set")
331331
os.Exit(1)
332332
}
333-
eg.Go(solanaTestRoutine(conf, deployerRunner, verbose, e2etests.TestSolanaDepositName))
333+
solanaTests := []string{
334+
e2etests.TestSolanaDepositName,
335+
e2etests.TestSolanaWithdrawName,
336+
}
337+
eg.Go(solanaTestRoutine(conf, deployerRunner, verbose, solanaTests...))
334338
}
335339
if testV2 {
336340
eg.Go(v2TestRoutine(conf, deployerRunner, verbose,

contrib/localnet/orchestrator/start-zetae2e.sh

+5
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ address=$(yq -r '.additional_accounts.user_bitcoin.evm_address' config.yml)
6666
echo "funding bitcoin tester address ${address} with 10000 Ether"
6767
geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545
6868

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

contrib/localnet/scripts/start-zetacored.sh

+3
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,9 @@ then
247247
# bitcoin tester
248248
address=$(yq -r '.additional_accounts.user_bitcoin.bech32_address' /root/config.yml)
249249
zetacored add-genesis-account "$address" 100000000000000000000000000azeta
250+
# solana tester
251+
address=$(yq -r '.additional_accounts.user_solana.bech32_address' /root/config.yml)
252+
zetacored add-genesis-account "$address" 100000000000000000000000000azeta
250253
# ethers tester
251254
address=$(yq -r '.additional_accounts.user_ether.bech32_address' /root/config.yml)
252255
zetacored add-genesis-account "$address" 100000000000000000000000000azeta

e2e/e2etests/e2etests.go

+11-2
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ const (
5454
/*
5555
Solana tests
5656
*/
57-
TestSolanaDepositName = "solana_deposit"
57+
TestSolanaDepositName = "solana_deposit"
58+
TestSolanaWithdrawName = "solana_withdraw"
5859

5960
/*
6061
Bitcoin tests
@@ -352,10 +353,18 @@ var AllE2ETests = []runner.E2ETest{
352353
TestSolanaDepositName,
353354
"deposit SOL into ZEVM",
354355
[]runner.ArgDefinition{
355-
{Description: "amount in SOL", DefaultValue: "0.1"},
356+
{Description: "amount in lamport", DefaultValue: "13370000"},
356357
},
357358
TestSolanaDeposit,
358359
),
360+
runner.NewE2ETest(
361+
TestSolanaWithdrawName,
362+
"withdraw SOL from ZEVM",
363+
[]runner.ArgDefinition{
364+
{Description: "amount in lamport", DefaultValue: "1336000"},
365+
},
366+
TestSolanaWithdraw,
367+
),
359368
/*
360369
Bitcoin tests
361370
*/

e2e/e2etests/test_solana_deposit.go

+13-4
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
11
package e2etests
22

33
import (
4+
"math/big"
5+
46
"github.com/gagliardetto/solana-go"
7+
"github.com/stretchr/testify/require"
58

69
"github.com/zeta-chain/zetacore/e2e/runner"
710
"github.com/zeta-chain/zetacore/e2e/utils"
811
crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types"
912
)
1013

11-
func TestSolanaDeposit(r *runner.E2ERunner, _ []string) {
14+
func TestSolanaDeposit(r *runner.E2ERunner, args []string) {
15+
require.Len(r, args, 1)
16+
17+
// parse deposit amount (in lamports)
18+
// #nosec G115 e2e - always in range
19+
depositAmount := big.NewInt(int64(parseInt(r, args[0])))
20+
1221
// load deployer private key
13-
privkey := solana.MustPrivateKeyFromBase58(r.Account.SolanaPrivateKey.String())
22+
privkey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String())
23+
require.NoError(r, err)
1424

1525
// create 'deposit' instruction
16-
amount := uint64(13370000)
17-
instruction := r.CreateDepositInstruction(privkey.PublicKey(), r.EVMAddress(), amount)
26+
instruction := r.CreateDepositInstruction(privkey.PublicKey(), r.EVMAddress(), depositAmount.Uint64())
1827

1928
// create and sign the transaction
2029
signedTx := r.CreateSignedTransaction([]solana.Instruction{instruction}, privkey)

e2e/e2etests/test_solana_withdraw.go

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package e2etests
2+
3+
import (
4+
"math/big"
5+
6+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
7+
"github.com/gagliardetto/solana-go"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/zeta-chain/zetacore/e2e/runner"
11+
)
12+
13+
func TestSolanaWithdraw(r *runner.E2ERunner, args []string) {
14+
require.Len(r, args, 1)
15+
16+
// print balanceAfter of from address
17+
balanceBefore, err := r.SOLZRC20.BalanceOf(&bind.CallOpts{}, r.ZEVMAuth.From)
18+
require.NoError(r, err)
19+
r.Logger.Info("from address %s balance of SOL before: %d", r.ZEVMAuth.From, balanceBefore)
20+
21+
// parse withdraw amount (in lamports), approve amount is 1 SOL
22+
approvedAmount := new(big.Int).SetUint64(solana.LAMPORTS_PER_SOL)
23+
// #nosec G115 e2e - always in range
24+
withdrawAmount := big.NewInt(int64(parseInt(r, args[0])))
25+
require.Equal(
26+
r,
27+
-1,
28+
withdrawAmount.Cmp(approvedAmount),
29+
"Withdrawal amount must be less than the approved amount (1e9).",
30+
)
31+
32+
// load deployer private key
33+
privkey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String())
34+
require.NoError(r, err)
35+
36+
// withdraw
37+
r.WithdrawSOLZRC20(privkey.PublicKey(), withdrawAmount, approvedAmount)
38+
39+
// print balance of from address after withdraw
40+
balanceAfter, err := r.SOLZRC20.BalanceOf(&bind.CallOpts{}, r.ZEVMAuth.From)
41+
require.NoError(r, err)
42+
r.Logger.Info("from address %s balance of SOL after: %d", r.ZEVMAuth.From, balanceAfter)
43+
44+
// check if the balance is reduced correctly
45+
amountReduced := new(big.Int).Sub(balanceBefore, balanceAfter)
46+
require.True(r, amountReduced.Cmp(withdrawAmount) >= 0, "balance is not reduced correctly")
47+
}

e2e/runner/setup_solana.go

+11-21
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,35 @@
11
package runner
22

33
import (
4-
"time"
5-
64
ethcommon "github.com/ethereum/go-ethereum/common"
75
"github.com/gagliardetto/solana-go"
86
"github.com/gagliardetto/solana-go/rpc"
97
"github.com/near/borsh-go"
108
"github.com/stretchr/testify/require"
119

1210
"github.com/zeta-chain/zetacore/pkg/chains"
13-
solanacontract "github.com/zeta-chain/zetacore/pkg/contract/solana"
11+
solanacontracts "github.com/zeta-chain/zetacore/pkg/contracts/solana"
1412
)
1513

14+
// SetupSolanaAccount imports the deployer's private key
1615
func (r *E2ERunner) SetupSolanaAccount() {
17-
r.Logger.Print("⚙️ setting up Solana account")
18-
startTime := time.Now()
19-
defer func() {
20-
r.Logger.Print("✅ Solana account setup in %s", time.Since(startTime))
21-
}()
22-
23-
r.SetSolanaAddress()
24-
}
25-
26-
// SetSolanaAddress imports the deployer's private key
27-
func (r *E2ERunner) SetSolanaAddress() {
28-
privateKey := solana.MustPrivateKeyFromBase58(r.Account.SolanaPrivateKey.String())
16+
privateKey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String())
17+
require.NoError(r, err)
2918
r.SolanaDeployerAddress = privateKey.PublicKey()
3019

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

3423
// SetSolanaContracts set Solana contracts
3524
func (r *E2ERunner) SetSolanaContracts(deployerPrivateKey string) {
36-
r.Logger.Print("⚙️ setting up Solana contracts")
25+
r.Logger.Print("⚙️ initializing gateway program on Solana")
3726

3827
// set Solana contracts
39-
r.GatewayProgram = solana.MustPublicKeyFromBase58(solanacontract.SolanaGatewayProgramID)
28+
r.GatewayProgram = solana.MustPublicKeyFromBase58(solanacontracts.SolanaGatewayProgramID)
4029

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

60-
inst.DataBytes, err = borsh.Serialize(solanacontract.InitializeParams{
61-
Discriminator: solanacontract.DiscriminatorInitialize(),
50+
inst.DataBytes, err = borsh.Serialize(solanacontracts.InitializeParams{
51+
Discriminator: solanacontracts.DiscriminatorInitialize(),
6252
TssAddress: r.TSSAddress,
6353
ChainID: uint64(chains.SolanaLocalnet.ChainId),
6454
})
@@ -76,7 +66,7 @@ func (r *E2ERunner) SetSolanaContracts(deployerPrivateKey string) {
7666
require.NoError(r, err)
7767

7868
// deserialize the PDA info
79-
pda := solanacontract.PdaInfo{}
69+
pda := solanacontracts.PdaInfo{}
8070
err = borsh.Deserialize(&pda, pdaInfo.Bytes())
8171
require.NoError(r, err)
8272
tssAddress := ethcommon.BytesToAddress(pda.TssAddress[:])

e2e/runner/solana.go

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package runner
22

33
import (
4+
"math/big"
45
"time"
56

67
ethcommon "github.com/ethereum/go-ethereum/common"
@@ -9,7 +10,9 @@ import (
910
"github.com/near/borsh-go"
1011
"github.com/stretchr/testify/require"
1112

12-
solanacontract "github.com/zeta-chain/zetacore/pkg/contract/solana"
13+
"github.com/zeta-chain/zetacore/e2e/utils"
14+
solanacontract "github.com/zeta-chain/zetacore/pkg/contracts/solana"
15+
crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types"
1316
)
1417

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

106109
return sig, out
107110
}
111+
112+
// WithdrawSOLZRC20 withdraws an amount of ZRC20 SOL tokens
113+
func (r *E2ERunner) WithdrawSOLZRC20(to solana.PublicKey, amount *big.Int, approveAmount *big.Int) {
114+
// approve
115+
tx, err := r.SOLZRC20.Approve(r.ZEVMAuth, r.SOLZRC20Addr, approveAmount)
116+
require.NoError(r, err)
117+
receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout)
118+
utils.RequireTxSuccessful(r, receipt)
119+
120+
// withdraw
121+
tx, err = r.SOLZRC20.Withdraw(r.ZEVMAuth, []byte(to.String()), amount)
122+
require.NoError(r, err)
123+
r.Logger.EVMTransaction(*tx, "withdraw")
124+
125+
// wait for tx receipt
126+
receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout)
127+
utils.RequireTxSuccessful(r, receipt)
128+
r.Logger.Info("Receipt txhash %s status %d", receipt.TxHash, receipt.Status)
129+
130+
// wait for the cctx to be mined
131+
cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout)
132+
utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined)
133+
}

e2e/txserver/zeta_tx_server.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ func (zts ZetaTxServer) DeploySystemContractsAndZRC20(
450450
100000,
451451
))
452452
if err != nil {
453-
return SystemContractAddresses{}, fmt.Errorf("failed to deploy btc zrc20: %s", err.Error())
453+
return SystemContractAddresses{}, fmt.Errorf("failed to deploy sol zrc20: %s", err.Error())
454454
}
455455

456456
// deploy erc20 zrc20

0 commit comments

Comments
 (0)