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: integrate withdraw SPL #3134

Merged
merged 14 commits into from
Nov 12, 2024
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* [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) - integrate SPL deposits
* [3134](https://github.com/zeta-chain/node/pull/3134) - integrate withdraw SPL
skosito marked this conversation as resolved.
Show resolved Hide resolved

### Tests

Expand Down
4 changes: 3 additions & 1 deletion cmd/zetae2e/local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -436,9 +436,11 @@ func localE2ETest(cmd *cobra.Command, _ []string) {
e2etests.TestSolanaWithdrawRestrictedName,
// TODO move under admin tests
// https://github.com/zeta-chain/node/issues/3085
e2etests.TestSolanaWhitelistSPLName,
e2etests.TestSPLDepositName,
e2etests.TestSPLDepositAndCallName,
e2etests.TestSPLWithdrawName,
e2etests.TestSPLWithdrawAndCreateReceiverAtaName,
e2etests.TestSolanaWhitelistSPLName,
}
eg.Go(solanaTestRoutine(conf, deployerRunner, verbose, solanaTests...))
}
Expand Down
3 changes: 2 additions & 1 deletion contrib/localnet/solana/start-solana.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ echo "starting solana test validator..."
solana-test-validator &

sleep 5
# airdrop to e2e sol account
# airdrop to e2e sol account and rent payer (used to generate atas for withdraw spl receivers if they don't exist)
solana airdrop 100
solana airdrop 100 37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ
solana airdrop 100 C6KPvGDYfNusoE4yfRP21F8wK35bxCBMT69xk4xo3X79
skosito marked this conversation as resolved.
Show resolved Hide resolved
solana program deploy gateway.so


Expand Down
38 changes: 28 additions & 10 deletions e2e/e2etests/e2etests.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,16 @@ const (
/*
* Solana tests
*/
TestSolanaDepositName = "solana_deposit"
TestSolanaWithdrawName = "solana_withdraw"
TestSolanaDepositAndCallName = "solana_deposit_and_call"
TestSolanaDepositAndCallRefundName = "solana_deposit_and_call_refund"
TestSolanaDepositRestrictedName = "solana_deposit_restricted"
TestSolanaWithdrawRestrictedName = "solana_withdraw_restricted"
TestSPLDepositName = "spl_deposit"
TestSPLDepositAndCallName = "spl_deposit_and_call"
TestSolanaDepositName = "solana_deposit"
TestSolanaWithdrawName = "solana_withdraw"
TestSolanaDepositAndCallName = "solana_deposit_and_call"
TestSolanaDepositAndCallRefundName = "solana_deposit_and_call_refund"
TestSolanaDepositRestrictedName = "solana_deposit_restricted"
TestSolanaWithdrawRestrictedName = "solana_withdraw_restricted"
TestSPLDepositName = "spl_deposit"
TestSPLDepositAndCallName = "spl_deposit_and_call"
TestSPLWithdrawName = "spl_withdraw"
TestSPLWithdrawAndCreateReceiverAtaName = "spl_withdraw_and_create_receiver_ata"

/**
* TON tests
Expand Down Expand Up @@ -433,6 +435,22 @@ var AllE2ETests = []runner.E2ETest{
},
TestSolanaDepositAndCall,
),
runner.NewE2ETest(
TestSPLWithdrawName,
"withdraw SPL from ZEVM",
[]runner.ArgDefinition{
{Description: "amount in spl tokens", DefaultValue: "1000000"},
},
TestSPLWithdraw,
),
runner.NewE2ETest(
TestSPLWithdrawAndCreateReceiverAtaName,
"withdraw SPL from ZEVM and create receiver ata",
[]runner.ArgDefinition{
{Description: "amount in spl tokens", DefaultValue: "1000000"},
},
TestSPLWithdrawAndCreateReceiverAta,
),
runner.NewE2ETest(
TestSolanaDepositAndCallRefundName,
"deposit SOL into ZEVM and call a contract that reverts; should refund",
Expand Down Expand Up @@ -469,15 +487,15 @@ var AllE2ETests = []runner.E2ETest{
TestSPLDepositName,
"deposit SPL into ZEVM",
[]runner.ArgDefinition{
{Description: "amount of spl tokens", DefaultValue: "500000"},
{Description: "amount of spl tokens", DefaultValue: "12000000"},
},
TestSPLDeposit,
),
runner.NewE2ETest(
TestSPLDepositAndCallName,
"deposit SPL into ZEVM and call",
[]runner.ArgDefinition{
{Description: "amount of spl tokens", DefaultValue: "500000"},
{Description: "amount of spl tokens", DefaultValue: "12000000"},
},
TestSPLDepositAndCall,
),
Expand Down
69 changes: 69 additions & 0 deletions e2e/e2etests/test_spl_withdraw.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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"
)

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

withdrawAmount := parseBigInt(r, args[0])

skosito marked this conversation as resolved.
Show resolved Hide resolved
// get SPL ZRC20 balance before withdraw
zrc20BalanceBefore, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress())
require.NoError(r, err)
r.Logger.Info("runner balance of SPL before withdraw: %d", zrc20BalanceBefore)

require.Equal(r, 1, zrc20BalanceBefore.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 (1e9)",
)
skosito marked this conversation as resolved.
Show resolved Hide resolved

// load deployer private key
privkey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String())
skosito marked this conversation as resolved.
Show resolved Hide resolved
require.NoError(r, err)

// get receiver ata balance before withdraw
receiverAta := r.FindOrCreateAssociatedTokenAccount(privkey, privkey.PublicKey(), r.SPLAddr)
skosito marked this conversation as resolved.
Show resolved Hide resolved
receiverBalanceBefore, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, receiverAta, rpc.CommitmentConfirmed)
skosito marked this conversation as resolved.
Show resolved Hide resolved
require.NoError(r, err)
r.Logger.Info("receiver balance of SPL before withdraw: %s", receiverBalanceBefore.Value.Amount)

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

skosito marked this conversation as resolved.
Show resolved Hide resolved
// get SPL ZRC20 balance after withdraw
zrc20BalanceAfter, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress())
require.NoError(r, err)
r.Logger.Info("runner balance of SPL after withdraw: %d", zrc20BalanceAfter)

// verify balances are updated
receiverBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, receiverAta, rpc.CommitmentConfirmed)
skosito marked this conversation as resolved.
Show resolved Hide resolved
require.NoError(r, err)
r.Logger.Info("receiver balance of SPL after withdraw: %s", receiverBalanceAfter.Value.Amount)

// verify amount is added to receiver ata
require.Zero(
skosito marked this conversation as resolved.
Show resolved Hide resolved
r,
new(
big.Int,
).Add(withdrawAmount, parseBigInt(r, receiverBalanceBefore.Value.Amount)).
Cmp(parseBigInt(r, receiverBalanceAfter.Value.Amount)),
)
skosito marked this conversation as resolved.
Show resolved Hide resolved

// verify amount is subtracted on zrc20
require.Zero(r, new(big.Int).Sub(zrc20BalanceBefore, withdrawAmount).Cmp(zrc20BalanceAfter))
}
72 changes: 72 additions & 0 deletions e2e/e2etests/test_spl_withdraw_and_create_receiver_ata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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"
)

// TestSPLWithdrawAndCreateReceiverAta withdraws spl, but letting gateway to create receiver ata using rent payer
// instead of providing receiver that has it already created
func TestSPLWithdrawAndCreateReceiverAta(r *runner.E2ERunner, args []string) {
require.Len(r, args, 1)

withdrawAmount := parseBigInt(r, args[0])

// get SPL ZRC20 balance before withdraw
zrc20BalanceBefore, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress())
require.NoError(r, err)
r.Logger.Info("runner balance of SPL before withdraw: %d", zrc20BalanceBefore)

require.Equal(r, 1, zrc20BalanceBefore.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 (1e9)",
)
skosito marked this conversation as resolved.
Show resolved Hide resolved

// create new priv key, with empty ata
receiverPrivKey, err := solana.NewRandomPrivateKey()
require.NoError(r, err)

// verify receiver ata account doesn't exist
receiverAta, _, err := solana.FindAssociatedTokenAddress(receiverPrivKey.PublicKey(), r.SPLAddr)
require.NoError(r, err)

receiverAtaAcc, err := r.SolanaClient.GetAccountInfo(r.Ctx, receiverAta)
require.Error(r, err)
require.Nil(r, receiverAtaAcc)
skosito marked this conversation as resolved.
Show resolved Hide resolved

// withdraw
r.WithdrawSPLZRC20(receiverPrivKey.PublicKey(), withdrawAmount, approvedAmount)

// get SPL ZRC20 balance after withdraw
zrc20BalanceAfter, err := r.SPLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress())
require.NoError(r, err)
r.Logger.Info("runner balance of SPL after withdraw: %d", zrc20BalanceAfter)

// verify receiver ata was created
receiverAtaAcc, err = r.SolanaClient.GetAccountInfo(r.Ctx, receiverAta)
require.NoError(r, err)
require.NotNil(r, receiverAtaAcc)

// verify balances are updated
receiverBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, receiverAta, rpc.CommitmentConfirmed)
Copy link
Contributor

Choose a reason for hiding this comment

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

same question

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this was fixed in this PR

require.NoError(r, err)
r.Logger.Info("receiver balance of SPL after withdraw: %s", receiverBalanceAfter.Value.Amount)

// verify amount is added to receiver ata
require.Zero(r, withdrawAmount.Cmp(parseBigInt(r, receiverBalanceAfter.Value.Amount)))

// verify amount is subtracted on zrc20
require.Zero(r, new(big.Int).Sub(zrc20BalanceBefore, withdrawAmount).Cmp(zrc20BalanceAfter))
}
24 changes: 24 additions & 0 deletions e2e/runner/setup_solana.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,30 @@ func (r *E2ERunner) SetupSolana(deployerPrivateKey string) {
require.NoError(r, err)
r.Logger.Info("initial PDA balance: %d lamports", balance.Value)

// initialize rent payer
var instRentPayer solana.GenericInstruction
rentPayerPdaComputed := r.ComputeRentPayerPdaAddress()
skosito marked this conversation as resolved.
Show resolved Hide resolved

// create 'initialize_rent_payer' instruction
accountSlice = []*solana.AccountMeta{}
accountSlice = append(accountSlice, solana.Meta(rentPayerPdaComputed).WRITE())
accountSlice = append(accountSlice, solana.Meta(privkey.PublicKey()).WRITE().SIGNER())
accountSlice = append(accountSlice, solana.Meta(solana.SystemProgramID))
instRentPayer.ProgID = r.GatewayProgram
instRentPayer.AccountValues = accountSlice

instRentPayer.DataBytes, err = borsh.Serialize(solanacontracts.InitializeRentPayerParams{
Discriminator: solanacontracts.DiscriminatorInitializeRentPayer,
})
require.NoError(r, err)

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

// broadcast the transaction and wait for finalization
_, out = r.BroadcastTxSync(signedTx)
r.Logger.Info("initialize_rent_payer logs: %v", out.Meta.LogMessages)
skosito marked this conversation as resolved.
Show resolved Hide resolved

err = r.ensureSolanaChainParams()
require.NoError(r, err)

Expand Down
43 changes: 42 additions & 1 deletion e2e/runner/solana.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ func (r *E2ERunner) ComputePdaAddress() solana.PublicKey {
return pdaComputed
}

// ComputePdaAddress computes the rent payer PDA address for the gateway program
skosito marked this conversation as resolved.
Show resolved Hide resolved
func (r *E2ERunner) ComputeRentPayerPdaAddress() solana.PublicKey {
skosito marked this conversation as resolved.
Show resolved Hide resolved
seed := []byte(solanacontract.RentPayerPDASeed)
GatewayProgramID := solana.MustPublicKeyFromBase58(solanacontract.SolanaGatewayProgramID)
pdaComputed, bump, err := solana.FindProgramAddress([][]byte{seed}, GatewayProgramID)
require.NoError(r, err)

r.Logger.Info("computed rent payer pda: %s, bump %d\n", pdaComputed, bump)

return pdaComputed
}

// CreateDepositInstruction creates a 'deposit' instruction
func (r *E2ERunner) CreateDepositInstruction(
signer solana.PublicKey,
Expand Down Expand Up @@ -250,7 +262,7 @@ func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey, whitelist bool) *so
// minting some tokens to deployer for testing
ata := r.FindOrCreateAssociatedTokenAccount(*privateKey, privateKey.PublicKey(), tokenAccount.PublicKey())

mintToInstruction := token.NewMintToInstruction(uint64(1_000_000), tokenAccount.PublicKey(), ata, privateKey.PublicKey(), []solana.PublicKey{}).
mintToInstruction := token.NewMintToInstruction(uint64(1_000_000_000), tokenAccount.PublicKey(), ata, privateKey.PublicKey(), []solana.PublicKey{}).
Build()
signedTx = r.CreateSignedTransaction(
[]solana.Instruction{mintToInstruction},
Expand Down Expand Up @@ -379,3 +391,32 @@ func (r *E2ERunner) WithdrawSOLZRC20(

return cctx
}

// WithdrawSPLZRC20 withdraws an amount of ZRC20 SPL tokens
func (r *E2ERunner) WithdrawSPLZRC20(
to solana.PublicKey,
amount *big.Int,
approveAmount *big.Int,
) *crosschaintypes.CrossChainTx {
// approve splzrc20 to spend gas tokens to pay gas fee
tx, err := r.SOLZRC20.Approve(r.ZEVMAuth, r.SPLZRC20Addr, 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.SPLZRC20.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, "withdraw")
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)
skosito marked this conversation as resolved.
Show resolved Hide resolved

return cctx
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,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-20241025181051-d8d49e4fc85b
github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20241108171442-e48d82f94892
)

require (
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1531,8 +1531,8 @@ github.com/zeta-chain/go-tss v0.0.0-20241031223543-18765295f992 h1:jpfOoQGHQo29C
github.com/zeta-chain/go-tss v0.0.0-20241031223543-18765295f992/go.mod h1:nqelgf4HKkqlXaVg8X38a61WfyYB+ivCt6nnjoTIgCc=
github.com/zeta-chain/protocol-contracts v1.0.2-athens3.0.20241021075719-d40d2e28467c h1:ZoFxMMZtivRLquXVq1sEVlT45UnTPMO1MSXtc88nDv4=
github.com/zeta-chain/protocol-contracts v1.0.2-athens3.0.20241021075719-d40d2e28467c/go.mod h1:SjT7QirtJE8stnAe1SlNOanxtfSfijJm3MGJ+Ax7w7w=
github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20241025181051-d8d49e4fc85b h1:w4YVBbWxk9TI+7HM8hTvK66IgOo5XvEFsmH7n6WgW50=
github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20241025181051-d8d49e4fc85b/go.mod h1:DcDY828o773soiU/h0XpC+naxitrIMFVZqEvq/EJxMA=
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/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
22 changes: 22 additions & 0 deletions pkg/contracts/solana/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
// PDASeed is the seed for the Solana gateway program derived address
PDASeed = "meta"

// RentPayerPDASeed is the seed for the Solana gateway program derived address
RentPayerPDASeed = "rent-payer"

// AccountsNumberOfDeposit is the number of accounts required for Solana gateway deposit instruction
// [signer, pda, system_program]
accountsNumDeposit = 3
Expand All @@ -26,6 +29,8 @@
var (
// DiscriminatorInitialize returns the discriminator for Solana gateway 'initialize' instruction
DiscriminatorInitialize = idlgateway.IDLGateway.GetDiscriminator("initialize")
// DiscriminatorInitializeRentPayer returns the discriminator for Solana gateway 'initialize_rent_payer' instruction
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: maybe add new lines between each constant?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i will update this in follow up PR

DiscriminatorInitializeRentPayer = idlgateway.IDLGateway.GetDiscriminator("initialize_rent_payer")
// DiscriminatorDeposit returns the discriminator for Solana gateway 'deposit' instruction
DiscriminatorDeposit = idlgateway.IDLGateway.GetDiscriminator("deposit")
// DiscriminatorDepositSPL returns the discriminator for Solana gateway 'deposit_spl_token' instruction
Expand Down Expand Up @@ -54,3 +59,20 @@

return gatewayID, pda, err
}

// ParseRentPayerPda parses the rent payer program derived address from the given string
func ParseRentPayerPda(address string) (solana.PublicKey, error) {
skosito marked this conversation as resolved.
Show resolved Hide resolved
var rentPayerPda solana.PublicKey

// decode gateway address
gatewayID, err := solana.PublicKeyFromBase58(address)
if err != nil {
return rentPayerPda, errors.Wrap(err, "unable to decode address")
}

Check warning on line 71 in pkg/contracts/solana/gateway.go

View check run for this annotation

Codecov / codecov/patch

pkg/contracts/solana/gateway.go#L64-L71

Added lines #L64 - L71 were not covered by tests

// compute gateway PDA
seed := []byte(RentPayerPDASeed)
rentPayerPda, _, err = solana.FindProgramAddress([][]byte{seed}, gatewayID)

return rentPayerPda, err

Check warning on line 77 in pkg/contracts/solana/gateway.go

View check run for this annotation

Codecov / codecov/patch

pkg/contracts/solana/gateway.go#L74-L77

Added lines #L74 - L77 were not covered by tests
}
skosito marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading