Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sol withdraw and call #3450

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* [3461](https://github.com/zeta-chain/node/pull/3461) - add new `ConfirmationParams` field to chain params to enable multiple confirmation count values, deprecating `confirmation_count`
* [3489](https://github.com/zeta-chain/node/pull/3489) - add Sui chain info
* [3455](https://github.com/zeta-chain/node/pull/3455) - add `track-cctx` command to zetatools
* [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
skosito marked this conversation as resolved.
Show resolved Hide resolved

# 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) {
skosito marked this conversation as resolved.
Show resolved Hide resolved
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,
)
skosito marked this conversation as resolved.
Show resolved Hide resolved

// 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)
skosito marked this conversation as resolved.
Show resolved Hide resolved

// 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?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably use gas limit field for compute budget limit

Copy link
Contributor

@ws4charlie ws4charlie Feb 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we use the exact budget paid by user, what will happen if user_paid_gas < gas_required_for_call? If the outbound tx fails, does it mean the nonce will not increment? Do we allow method execute to fail legally?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question, i am not sure, dont we have a way to revert? we didnt before but not sure if it was implemented in meantime
but there is no way afaik to still increment the nonce if it fails because of gas limit, and we have no control over how much gas it will take, so i think revert is important in this case

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 e2e/runner/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func (r *E2ERunner) VerifySolanaWithdrawalAmountFromCCTX(cctx *crosschaintypes.C

// 1st instruction is the withdraw
instruction := tx.Message.Instructions[0]
instWithdrae, err := solanacontracts.ParseInstructionWithdraw(instruction)
instWithdrae, err := solanacontracts.TryParseInstructionWithdraw(instruction)
require.NoError(r, err)

// verify the amount
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
Loading