Skip to content

Commit

Permalink
feat: sol withdraw and call (#3450)
Browse files Browse the repository at this point in the history
* sol withdraw and call and e2e test

* fmt

* fix solana e2e tests

* fix msg hash unit tests

* cleanup

* bump gateway.so

* cleanup unused function

* cleanup

* PR comments

* linter

* PR comments

* PR comments
  • Loading branch information
skosito authored Feb 13, 2025
1 parent c0d5f13 commit 51a4465
Show file tree
Hide file tree
Showing 25 changed files with 1,232 additions and 142 deletions.
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* [3455](https://github.com/zeta-chain/node/pull/3455) - add `track-cctx` command to zetatools
* [3506](https://github.com/zeta-chain/node/pull/3506) - define `ConfirmationMode` enum and add it to `InboundParams`, `OutboundParams`, `MsgVoteInbound` and `MsgVoteOutbound`
* [3469](https://github.com/zeta-chain/node/pull/3469) - add `MsgRemoveInboundTracker` to remove inbound trackers. This message can be triggered by the emergency policy.
* [3450](https://github.com/zeta-chain/node/pull/3450) - SOL withdraw and call integration

### Refactor

Expand Down
1 change: 1 addition & 0 deletions cmd/zetae2e/local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) {
solanaTests := []string{
e2etests.TestSolanaDepositName,
e2etests.TestSolanaWithdrawName,
e2etests.TestSolanaWithdrawAndCallName,
e2etests.TestSolanaDepositAndCallName,
e2etests.TestSolanaDepositAndCallRevertName,
e2etests.TestSolanaDepositAndCallRevertWithDustName,
Expand Down
3 changes: 3 additions & 0 deletions contrib/localnet/solana/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ COPY ./start-solana.sh /usr/bin/start-solana.sh
RUN chmod +x /usr/bin/start-solana.sh
COPY ./gateway.so .
COPY ./gateway-keypair.json .
COPY ./connected.so .
COPY ./connected-keypair.json .


ENTRYPOINT [ "bash" ]
CMD [ "/usr/bin/start-solana.sh" ]
1 change: 1 addition & 0 deletions contrib/localnet/solana/connected-keypair.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[4,156,87,189,200,207,193,67,118,37,217,115,157,40,134,151,249,231,68,2,190,65,17,126,65,183,171,211,67,236,0,114,58,185,194,112,159,227,216,198,215,6,171,71,237,0,253,253,183,17,201,181,129,216,22,233,192,113,248,203,247,19,100,21]
Binary file added contrib/localnet/solana/connected.so
Binary file not shown.
Binary file modified contrib/localnet/solana/gateway.so
Binary file not shown.
2 changes: 1 addition & 1 deletion contrib/localnet/solana/start-solana.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ sleep 5
solana airdrop 1000
solana airdrop 1000 37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ
solana program deploy gateway.so

solana program deploy connected.so

# leave some time for debug if validator exits due to errors
sleep 1000
11 changes: 10 additions & 1 deletion e2e/e2etests/e2etests.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const (
*/
TestSolanaDepositName = "solana_deposit"
TestSolanaWithdrawName = "solana_withdraw"
TestSolanaWithdrawAndCallName = "solana_withdraw_and_call"
TestSolanaDepositAndCallName = "solana_deposit_and_call"
TestSolanaDepositAndCallRevertName = "solana_deposit_and_call_revert"
TestSolanaDepositAndCallRevertWithDustName = "solana_deposit_and_call_revert_with_dust"
Expand Down Expand Up @@ -441,7 +442,7 @@ var AllE2ETests = []runner.E2ETest{
TestSolanaDepositName,
"deposit SOL into ZEVM",
[]runner.ArgDefinition{
{Description: "amount in lamport", DefaultValue: "12000000"},
{Description: "amount in lamport", DefaultValue: "24000000"},
},
TestSolanaDeposit,
),
Expand All @@ -453,6 +454,14 @@ var AllE2ETests = []runner.E2ETest{
},
TestSolanaWithdraw,
),
runner.NewE2ETest(
TestSolanaWithdrawAndCallName,
"withdraw SOL from ZEVM and call solana program",
[]runner.ArgDefinition{
{Description: "amount in lamport", DefaultValue: "1000000"},
},
TestSolanaWithdrawAndCall,
),
runner.NewE2ETest(
TestSolanaDepositAndCallName,
"deposit SOL into ZEVM and call a contract",
Expand Down
96 changes: 96 additions & 0 deletions e2e/e2etests/test_solana_withdraw_and_call.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package e2etests

import (
"math/big"

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

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

// TestSolanaWithdrawAndCall executes withdrawAndCall on zevm and calls connected program on solana
// message and zevm sender are stored in connected program pda, and withdrawn lamports are stored
// in connected program pda and account provided in remaining accounts to demonstrate that lamports
// can be moved to accounts in connected program as well as gateway program
func TestSolanaWithdrawAndCall(r *runner.E2ERunner, args []string) {
require.Len(r, args, 1)

withdrawAmount := utils.ParseBigInt(r, args[0])

// get ERC20 SOL balance before withdraw
balanceBefore, err := r.SOLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress())
require.NoError(r, err)
r.Logger.Info("runner balance of SOL before withdraw: %d", balanceBefore)

require.Equal(r, 1, balanceBefore.Cmp(withdrawAmount), "Insufficient balance for withdrawal")

// parse withdraw amount (in lamports), approve amount is 1 SOL
approvedAmount := new(big.Int).SetUint64(solana.LAMPORTS_PER_SOL)
require.Equal(
r,
-1,
withdrawAmount.Cmp(approvedAmount),
"Withdrawal amount must be less than the approved amount: %v",
approvedAmount,
)

// load deployer private key
privkey := r.GetSolanaPrivKey()

// check balances before withdraw
connected := solana.MustPublicKeyFromBase58("4xEw862A2SEwMjofPkUyd4NEekmVJKJsdHkK3UkAtDrc")
connectedPda, err := solanacontract.ComputeConnectedPdaAddress(connected)
require.NoError(r, err)

connectedPdaInfoBefore, err := r.SolanaClient.GetAccountInfo(r.Ctx, connectedPda)
require.NoError(r, err)

senderBefore, err := r.SolanaClient.GetAccountInfo(r.Ctx, privkey.PublicKey())
require.NoError(r, err)

// withdraw and call
tx := r.WithdrawAndCallSOLZRC20(connected, withdrawAmount, approvedAmount, []byte("hello"))

// wait for the cctx to be mined
cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout)
utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined)

// get ERC20 SOL balance after withdraw
balanceAfter, err := r.SOLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress())
require.NoError(r, err)
r.Logger.Info("runner balance of SOL after withdraw: %d", 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")

// check pda account info of connected program
connectedPdaInfo, err := r.SolanaClient.GetAccountInfo(r.Ctx, connectedPda)
require.NoError(r, err)

sender, err := r.SolanaClient.GetAccountInfo(r.Ctx, privkey.PublicKey())
require.NoError(r, err)

type ConnectedPdaInfo struct {
Discriminator [8]byte
LastSender [20]byte
LastMessage string
}
pda := ConnectedPdaInfo{}
err = borsh.Deserialize(&pda, connectedPdaInfo.Bytes())
require.NoError(r, err)

require.Equal(r, "hello", pda.LastMessage)
require.Equal(r, r.ZEVMAuth.From.String(), common.BytesToAddress(pda.LastSender[:]).String())

// connected program splits amount between account provided in remaining accounts, and its own pda
require.Equal(r, connectedPdaInfoBefore.Value.Lamports+withdrawAmount.Uint64()/2, connectedPdaInfo.Value.Lamports)
require.Equal(r, senderBefore.Value.Lamports+withdrawAmount.Uint64()/2, sender.Value.Lamports)
}
29 changes: 28 additions & 1 deletion e2e/runner/setup_solana.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,34 @@ func (r *E2ERunner) SetupSolana(gatewayID, deployerPrivateKey string) {

// broadcast the transaction and wait for finalization
_, out := r.BroadcastTxSync(signedTx)
r.Logger.Info("initialize logs: %v", out.Meta.LogMessages)
r.Logger.Info("initialize gateway logs: %v", out.Meta.LogMessages)

// initialize connected program
connectedPda, err := solanacontracts.ComputeConnectedPdaAddress(ConnectedProgramID)
require.NoError(r, err)

var instConnected solana.GenericInstruction
accountSliceConnected := []*solana.AccountMeta{}
accountSliceConnected = append(accountSliceConnected, solana.Meta(privkey.PublicKey()).WRITE().SIGNER())
accountSliceConnected = append(accountSliceConnected, solana.Meta(connectedPda).WRITE())
accountSliceConnected = append(accountSliceConnected, solana.Meta(solana.SystemProgramID))
instConnected.ProgID = ConnectedProgramID
instConnected.AccountValues = accountSliceConnected

type InitializeConnected struct {
Discriminator [8]byte
}
instConnected.DataBytes, err = borsh.Serialize(InitializeConnected{
Discriminator: solanacontracts.DiscriminatorInitialize,
})
require.NoError(r, err)

// create and sign the transaction
signedTx = r.CreateSignedTransaction([]solana.Instruction{&instConnected}, privkey, []solana.PrivateKey{})

// broadcast the transaction and wait for finalization
_, out = r.BroadcastTxSync(signedTx)
r.Logger.Info("initialize connected logs: %v", out.Meta.LogMessages)

// retrieve the PDA account info
pdaInfo, err := r.SolanaClient.GetAccountInfoWithOpts(r.Ctx, pdaComputed, &rpc.GetAccountInfoOpts{
Expand Down
78 changes: 71 additions & 7 deletions e2e/runner/solana.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,22 @@ import (
"github.com/gagliardetto/solana-go/rpc"
"github.com/near/borsh-go"
"github.com/stretchr/testify/require"
"github.com/zeta-chain/protocol-contracts/pkg/gatewayzevm.sol"

"github.com/zeta-chain/node/e2e/utils"
solanacontract "github.com/zeta-chain/node/pkg/contracts/solana"
)

// Connected program used to test sol withdraw and call
var ConnectedProgramID = solana.MustPublicKeyFromBase58("4xEw862A2SEwMjofPkUyd4NEekmVJKJsdHkK3UkAtDrc")

// ComputePdaAddress computes the PDA address for the gateway program
func (r *E2ERunner) ComputePdaAddress() solana.PublicKey {
seed := []byte(solanacontract.PDASeed)
pdaComputed, bump, err := solana.FindProgramAddress([][]byte{seed}, r.GatewayProgram)
require.NoError(r, err)

r.Logger.Info("computed pda: %s, bump %d\n", pdaComputed, bump)
r.Logger.Info("computed pda for gateway program: %s, bump %d\n", pdaComputed, bump)

return pdaComputed
}
Expand Down Expand Up @@ -80,10 +84,10 @@ func (r *E2ERunner) CreateWhitelistSPLMintInstruction(
ProgID: r.GatewayProgram,
DataBytes: data,
AccountValues: []*solana.AccountMeta{
solana.Meta(signer).WRITE().SIGNER(),
solana.Meta(r.ComputePdaAddress()).WRITE(),
solana.Meta(whitelistEntry).WRITE(),
solana.Meta(whitelistCandidate),
solana.Meta(r.ComputePdaAddress()).WRITE(),
solana.Meta(signer).WRITE().SIGNER(),
solana.Meta(solana.SystemProgramID),
},
}
Expand Down Expand Up @@ -236,7 +240,7 @@ func (r *E2ERunner) SPLDepositAndCall(
data,
)

limit := computebudget.NewSetComputeUnitLimitInstruction(50000).Build() // 50k compute unit limit
limit := computebudget.NewSetComputeUnitLimitInstruction(70000).Build() // 70k compute unit limit
feesInit := computebudget.NewSetComputeUnitPriceInstructionBuilder().
SetMicroLamports(100000).Build() // 0.1 lamports per compute unit
signedTx := r.CreateSignedTransaction(
Expand Down Expand Up @@ -423,7 +427,7 @@ func (r *E2ERunner) SOLDepositAndCall(
instruction := r.CreateDepositInstruction(signerPrivKey.PublicKey(), receiver, data, amount.Uint64())

// create and sign the transaction
limit := computebudget.NewSetComputeUnitLimitInstruction(50000).Build() // 50k compute unit limit
limit := computebudget.NewSetComputeUnitLimitInstruction(70000).Build() // 70k compute unit limit
feesInit := computebudget.NewSetComputeUnitPriceInstructionBuilder().
SetMicroLamports(100000).Build() // 0.1 lamports per compute unit
signedTx := r.CreateSignedTransaction(
Expand All @@ -446,13 +450,19 @@ func (r *E2ERunner) WithdrawSOLZRC20(
approveAmount *big.Int,
) *ethtypes.Transaction {
// approve
tx, err := r.SOLZRC20.Approve(r.ZEVMAuth, r.SOLZRC20Addr, approveAmount)
tx, err := r.SOLZRC20.Approve(r.ZEVMAuth, r.GatewayZEVMAddr, approveAmount)
require.NoError(r, err)
receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout)
utils.RequireTxSuccessful(r, receipt, "approve")

// withdraw
tx, err = r.SOLZRC20.Withdraw(r.ZEVMAuth, []byte(to.String()), amount)
tx, err = r.GatewayZEVM.Withdraw(
r.ZEVMAuth,
[]byte(to.String()),
amount,
r.SOLZRC20Addr,
gatewayzevm.RevertOptions{OnRevertGasLimit: big.NewInt(0)},
)
require.NoError(r, err)
r.Logger.EVMTransaction(*tx, "withdraw")

Expand All @@ -464,6 +474,60 @@ func (r *E2ERunner) WithdrawSOLZRC20(
return tx
}

// WithdrawAndCallSOLZRC20 withdraws an amount of ZRC20 SOL tokens and calls program on solana
func (r *E2ERunner) WithdrawAndCallSOLZRC20(
to solana.PublicKey,
amount *big.Int,
approveAmount *big.Int,
data []byte,
) *ethtypes.Transaction {
// approve
tx, err := r.SOLZRC20.Approve(r.ZEVMAuth, r.GatewayZEVMAddr, approveAmount)
require.NoError(r, err)
receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout)
utils.RequireTxSuccessful(r, receipt, "approve")

// create encoded msg
connected := solana.MustPublicKeyFromBase58("4xEw862A2SEwMjofPkUyd4NEekmVJKJsdHkK3UkAtDrc")
connectedPda, err := solanacontract.ComputeConnectedPdaAddress(connected)
require.NoError(r, err)
abiArgs, err := solanacontract.GetExecuteMsgAbi()
require.NoError(r, err)
msg := solanacontract.ExecuteMsg{
Accounts: []solanacontract.AccountMeta{
{PublicKey: [32]byte(connectedPda.Bytes()), IsWritable: true},
{PublicKey: [32]byte(r.ComputePdaAddress().Bytes()), IsWritable: false},
{PublicKey: [32]byte(r.GetSolanaPrivKey().PublicKey().Bytes()), IsWritable: true},
{PublicKey: [32]byte(solana.SystemProgramID.Bytes()), IsWritable: false},
},
Data: data,
}

msgEncoded, err := abiArgs.Pack(msg)
require.NoError(r, err)

// withdraw
// TODO: gas limit?
tx, err = r.GatewayZEVM.WithdrawAndCall0(
r.ZEVMAuth,
[]byte(to.String()),
amount,
r.SOLZRC20Addr,
msgEncoded,
gatewayzevm.CallOptions{GasLimit: big.NewInt(250000)},
gatewayzevm.RevertOptions{OnRevertGasLimit: big.NewInt(0)},
)
require.NoError(r, err)
r.Logger.EVMTransaction(*tx, "withdraw_and_call")

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

return tx
}

// WithdrawSPLZRC20 withdraws an amount of ZRC20 SPL tokens
func (r *E2ERunner) WithdrawSPLZRC20(
to solana.PublicKey,
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ require (
github.com/montanaflynn/stats v0.7.1
github.com/showa-93/go-mask v0.6.2
github.com/tonkeeper/tongo v1.9.3
github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20241108171442-e48d82f94892
github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20250210125843-0384ef07ec07
)

require (
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1404,8 +1404,8 @@ github.com/zeta-chain/go-tss v0.0.0-20241216161449-be92b20f8102 h1:jMb9ydfDFjgdl
github.com/zeta-chain/go-tss v0.0.0-20241216161449-be92b20f8102/go.mod h1:nqelgf4HKkqlXaVg8X38a61WfyYB+ivCt6nnjoTIgCc=
github.com/zeta-chain/protocol-contracts v1.0.2-athens3.0.20250115133723-7232d7838789 h1:8DAZ5bgu+1ZbZ+VQh2eW15NPziwMy1g2k5rlKSfUFRI=
github.com/zeta-chain/protocol-contracts v1.0.2-athens3.0.20250115133723-7232d7838789/go.mod h1:SjT7QirtJE8stnAe1SlNOanxtfSfijJm3MGJ+Ax7w7w=
github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20241108171442-e48d82f94892 h1:oI5qCrw2SXDf2a2UYAn0tpaKHbKpJcR+XDtceyY00wE=
github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20241108171442-e48d82f94892/go.mod h1:DcDY828o773soiU/h0XpC+naxitrIMFVZqEvq/EJxMA=
github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20250210125843-0384ef07ec07 h1:ifx3FO+k1GrENvDsaixhL36DZmrFMRhVWLE/VwmKxMQ=
github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20250210125843-0384ef07ec07/go.mod h1:DcDY828o773soiU/h0XpC+naxitrIMFVZqEvq/EJxMA=
github.com/zeta-chain/tss-lib v0.0.0-20240916163010-2e6b438bd901 h1:9whtN5fjYHfk4yXIuAsYP2EHxImwDWDVUOnZJ2pfL3w=
github.com/zeta-chain/tss-lib v0.0.0-20240916163010-2e6b438bd901/go.mod h1:d2iTC62s9JwKiCMPhcDDXbIZmuzAyJ4lwso0H5QyRbk=
github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U=
Expand Down
14 changes: 14 additions & 0 deletions pkg/contracts/solana/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ var (
// DiscriminatorWithdraw returns the discriminator for Solana gateway 'withdraw' instruction
DiscriminatorWithdraw = idlgateway.IDLGateway.GetDiscriminator("withdraw")

// DiscriminatorExecute returns the discriminator for Solana gateway 'execute' instruction
DiscriminatorExecute = idlgateway.IDLGateway.GetDiscriminator("execute")

// DiscriminatorWithdrawSPL returns the discriminator for Solana gateway 'withdraw_spl_token' instruction
DiscriminatorWithdrawSPL = idlgateway.IDLGateway.GetDiscriminator("withdraw_spl_token")

Expand All @@ -62,3 +65,14 @@ func ParseGatewayWithPDA(gatewayAddress string) (solana.PublicKey, solana.Public

return gatewayID, pda, err
}

// ComputeConnectedPdaAddress computes the PDA address for the custom program PDA with seed "connected"
func ComputeConnectedPdaAddress(connected solana.PublicKey) (solana.PublicKey, error) {
seed := []byte("connected")
pdaComputed, _, err := solana.FindProgramAddress([][]byte{seed}, connected)
if err != nil {
return solana.PublicKey{}, err
}

return pdaComputed, nil
}
Loading

0 comments on commit 51a4465

Please sign in to comment.