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(ton): adjacent TON tasks #3075

Merged
merged 13 commits into from
Nov 5, 2024
3 changes: 3 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

* [2984](https://github.com/zeta-chain/node/pull/2984) - add Whitelist message ability to whitelist SPL tokens on Solana

### Tests
* [3075](https://github.com/zeta-chain/node/pull/3075) - ton: withdraw concurrent, deposit & revert.

## v21.0.0

### Features
Expand Down
2 changes: 2 additions & 0 deletions cmd/zetae2e/local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,9 @@ func localE2ETest(cmd *cobra.Command, _ []string) {
tonTests := []string{
e2etests.TestTONDepositName,
e2etests.TestTONDepositAndCallName,
e2etests.TestTONDepositAndCallRefundName,
e2etests.TestTONWithdrawName,
e2etests.TestTONWithdrawConcurrentName,
}

eg.Go(tonTestRoutine(conf, deployerRunner, verbose, tonTests...))
Expand Down
22 changes: 19 additions & 3 deletions e2e/e2etests/e2etests.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,11 @@ const (
/**
* TON tests
*/
TestTONDepositName = "ton_deposit"
TestTONDepositAndCallName = "ton_deposit_and_call"
TestTONWithdrawName = "ton_withdraw"
TestTONDepositName = "ton_deposit"
TestTONDepositAndCallName = "ton_deposit_and_call"
TestTONDepositAndCallRefundName = "ton_deposit_refund"
TestTONWithdrawName = "ton_withdraw"
TestTONWithdrawConcurrentName = "ton_withdraw_concurrent"

/*
Bitcoin tests
Expand Down Expand Up @@ -479,6 +481,14 @@ var AllE2ETests = []runner.E2ETest{
},
TestTONDepositAndCall,
),
runner.NewE2ETest(
TestTONDepositAndCallRefundName,
"deposit TON into ZEVM and call a smart contract that reverts; expect refund",
[]runner.ArgDefinition{
{Description: "amount in nano tons", DefaultValue: "1000000000"}, // 1.0 TON
},
TestTONDepositAndCallRefund,
),
runner.NewE2ETest(
TestTONWithdrawName,
"withdraw TON from ZEVM",
Expand All @@ -487,6 +497,12 @@ var AllE2ETests = []runner.E2ETest{
},
TestTONWithdraw,
),
runner.NewE2ETest(
TestTONWithdrawConcurrentName,
"withdraw TON from ZEVM for several recipients simultaneously",
[]runner.ArgDefinition{},
TestTONWithdrawConcurrent,
),
swift1337 marked this conversation as resolved.
Show resolved Hide resolved
/*
Bitcoin tests
*/
Expand Down
50 changes: 50 additions & 0 deletions e2e/e2etests/test_ton_deposit_refund.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package e2etests

import (
"github.com/stretchr/testify/require"

"github.com/zeta-chain/node/e2e/runner"
"github.com/zeta-chain/node/e2e/utils"
testcontract "github.com/zeta-chain/node/testutil/contracts"
cctypes "github.com/zeta-chain/node/x/crosschain/types"
)

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

// Given amount and arbitrary call data
var (
amount = parseUint(r, args[0])
data = []byte("hello reverter")
)
swift1337 marked this conversation as resolved.
Show resolved Hide resolved

// Given deployer mock revert contract
// deploy a reverter contract in ZEVM
reverterAddr, _, _, err := testcontract.DeployReverter(r.ZEVMAuth, r.ZEVMClient)
require.NoError(r, err)
r.Logger.Info("Reverter contract deployed at: %s", reverterAddr.String())
swift1337 marked this conversation as resolved.
Show resolved Hide resolved

// ACT
// Send a deposit and call transaction from the deployer (faucet)
swift1337 marked this conversation as resolved.
Show resolved Hide resolved
// to the reverter contract
cctx, err := r.TONDepositAndCall(
&r.TONDeployer.Wallet,
amount,
reverterAddr,
data,
runner.TONExpectStatus(cctypes.CctxStatus_Reverted),
)

// ASSERT
require.NoError(r, err)
r.Logger.CCTX(*cctx, "ton_deposit_and_refund")

// Check the error carries the revert executed.
// tolerate the error in both the ErrorMessage field and the StatusMessage field
if cctx.CctxStatus.ErrorMessage != "" {
require.Contains(r, cctx.CctxStatus.ErrorMessage, "revert executed")
return
}

require.Contains(r, cctx.CctxStatus.StatusMessage, utils.ErrHashRevertFoo)
swift1337 marked this conversation as resolved.
Show resolved Hide resolved
}
7 changes: 2 additions & 5 deletions e2e/e2etests/test_ton_withdrawal.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ import (
"github.com/zeta-chain/node/zetaclient/chains/ton/liteapi"
)

// TODO: Add "withdraw_many_concurrent" test
// https://github.com/zeta-chain/node/issues/3044

func TestTONWithdraw(r *runner.E2ERunner, args []string) {
// ARRANGE
require.Len(r, args, 1)
Expand All @@ -34,7 +31,7 @@ func TestTONWithdraw(r *runner.E2ERunner, args []string) {
tonRecipient, err := deployer.CreateWallet(r.Ctx, toncontracts.Coins(1))
require.NoError(r, err)

tonRecipientBalanceBefore, err := deployer.GetBalanceOf(r.Ctx, tonRecipient.GetAddress())
tonRecipientBalanceBefore, err := deployer.GetBalanceOf(r.Ctx, tonRecipient.GetAddress(), true)
require.NoError(r, err)

r.Logger.Info("Recipient's TON balance before withdrawal: %s", toncontracts.FormatCoins(tonRecipientBalanceBefore))
Expand All @@ -61,7 +58,7 @@ func TestTONWithdraw(r *runner.E2ERunner, args []string) {
)

// Make sure that recipient's TON balance has increased
tonRecipientBalanceAfter, err := deployer.GetBalanceOf(r.Ctx, tonRecipient.GetAddress())
tonRecipientBalanceAfter, err := deployer.GetBalanceOf(r.Ctx, tonRecipient.GetAddress(), true)
require.NoError(r, err)

r.Logger.Info("Recipient's balance after withdrawal: %s", toncontracts.FormatCoins(tonRecipientBalanceAfter))
Expand Down
74 changes: 74 additions & 0 deletions e2e/e2etests/test_ton_withdrawal_concurrent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package e2etests

import (
"math/rand"
"sync"

"cosmossdk.io/math"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/stretchr/testify/require"
"github.com/tonkeeper/tongo/ton"

"github.com/zeta-chain/node/e2e/runner"
"github.com/zeta-chain/node/e2e/utils"
toncontracts "github.com/zeta-chain/node/pkg/contracts/ton"
"github.com/zeta-chain/node/testutil/sample"
cc "github.com/zeta-chain/node/x/crosschain/types"
)

// TestTONWithdrawConcurrent makes sure that multiple concurrent
// withdrawals will be eventually processed by sequentially increasing Gateway nonce
// and that zetaclient tolerates "invalid nonce" error from RPC.
func TestTONWithdrawConcurrent(r *runner.E2ERunner, _ []string) {
// ARRANGE
// Given a deployer
_, deployer := r.Ctx, r.TONDeployer

const recipientsCount = 10

// Fire withdrawals. Note that zevm sender is r.ZEVMAuth
var wg sync.WaitGroup
for i := 0; i < recipientsCount; i++ {
// ARRANGE
// Given multiple recipients WITHOUT deployed wallet-contracts
// and withdrawal amounts between 1 and 5 TON
var (
// #nosec G404: it's a test
amountCoins = 1 + rand.Intn(5)
// #nosec G115 test - always in range
amount = toncontracts.Coins(uint64(amountCoins))
recipient = sample.GenerateTONAccountID()
)

// ACT
r.Logger.Info(
"Withdrawal #%d: sending %s to %s",
i+1,
toncontracts.FormatCoins(amount),
recipient.ToRaw(),
)

approvedAmount := amount.Add(toncontracts.Coins(1))
tx := r.SendWithdrawTONZRC20(recipient, amount.BigInt(), approvedAmount.BigInt())

wg.Add(1)

go func(number int, recipient ton.AccountID, amount math.Uint, tx *ethtypes.Transaction) {
defer wg.Done()

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

// ASSERT
utils.RequireCCTXStatus(r, cctx, cc.CctxStatus_OutboundMined)
r.Logger.Info("Withdrawal #%d complete! cctx index: %s", number, cctx.Index)

// Check recipient's balance ON TON
balance, err := deployer.GetBalanceOf(r.Ctx, recipient, false)
require.NoError(r, err, "failed to get balance of %s", recipient.ToRaw())
require.Equal(r, amount.Uint64(), balance.Uint64())
}(i+1, recipient, amount, tx)
}

wg.Wait()
}
35 changes: 21 additions & 14 deletions e2e/runner/setup_ton.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/zeta-chain/node/pkg/chains"
"github.com/zeta-chain/node/pkg/constant"
toncontracts "github.com/zeta-chain/node/pkg/contracts/ton"
cctxtypes "github.com/zeta-chain/node/x/crosschain/types"
observertypes "github.com/zeta-chain/node/x/observer/types"
)

Expand Down Expand Up @@ -54,29 +55,35 @@ func (r *E2ERunner) SetupTON() error {
)

// 3. Check that the gateway indeed was deployed and has desired TON balance.
gwBalance, err := deployer.GetBalanceOf(ctx, gwAccount.ID)
if err != nil {
gwBalance, err := deployer.GetBalanceOf(ctx, gwAccount.ID, true)
switch {
case err != nil:
return errors.Wrap(err, "unable to get balance of TON gateway")
case gwBalance.IsZero():
return fmt.Errorf("TON gateway balance is zero")
}

if gwBalance.IsZero() {
return fmt.Errorf("TON gateway balance is zero")
// 4. Set chain params & chain nonce
if err := r.ensureTONChainParams(gwAccount); err != nil {
return errors.Wrap(err, "unable to ensure TON chain params")
}

// 4. Deposit 100 TON deployer to Zevm Auth
gw := toncontracts.NewGateway(gwAccount.ID)
veryFirstDeposit := toncontracts.Coins(1000)
r.TONDeployer = deployer
r.TONGateway = toncontracts.NewGateway(gwAccount.ID)

// 5. Deposit 10000 TON deployer to Zevm Auth
veryFirstDeposit := toncontracts.Coins(10000)
zevmRecipient := r.ZEVMAuth.From

err = gw.SendDeposit(ctx, &deployer.Wallet, veryFirstDeposit, zevmRecipient, 0)
if err != nil {
return errors.Wrap(err, "unable to send deposit to TON gateway")
gwDeposit, err := r.TONDeposit(&deployer.Wallet, veryFirstDeposit, zevmRecipient)
switch {
case err != nil:
return errors.Wrap(err, "unable to deposit TON to Zevm Auth")
case gwDeposit.CctxStatus.Status != cctxtypes.CctxStatus_OutboundMined:
return errors.New("gateway deposit CCTX is not mined")
}

r.TONDeployer = deployer
r.TONGateway = gw

return r.ensureTONChainParams(gwAccount)
return nil
}

func (r *E2ERunner) ensureTONChainParams(gw *ton.AccountInit) error {
Expand Down
47 changes: 42 additions & 5 deletions e2e/runner/ton.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package runner

import (
"encoding/hex"
"math/big"
"time"

"cosmossdk.io/math"
eth "github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
"github.com/tonkeeper/tongo/ton"
Expand All @@ -23,6 +25,20 @@ import (
// https://docs.ton.org/develop/smart-contracts/guidelines/message-modes-cookbook
const tonDepositSendCode = toncontracts.SendFlagSeparateFees + toncontracts.SendFlagIgnoreErrors

// currently implemented only for DepositAndCall,
// can be adopted for all TON ops
type tonOpts struct {
expectedStatus cctypes.CctxStatus
}

type TONOpt func(t *tonOpts)

func TONExpectStatus(status cctypes.CctxStatus) TONOpt {
return func(t *tonOpts) {
t.expectedStatus = status
}
}

// TONDeposit deposit TON to Gateway contract
func (r *E2ERunner) TONDeposit(
sender *wallet.Wallet,
Expand Down Expand Up @@ -56,7 +72,7 @@ func (r *E2ERunner) TONDeposit(
}

// Wait for cctx
cctx := r.WaitForSpecificCCTX(filter, time.Minute)
cctx := r.WaitForSpecificCCTX(filter, cctypes.CctxStatus_OutboundMined, time.Minute)

return cctx, nil
}
Expand All @@ -67,7 +83,13 @@ func (r *E2ERunner) TONDepositAndCall(
amount math.Uint,
zevmRecipient eth.Address,
callData []byte,
opts ...TONOpt,
) (*cctypes.CrossChainTx, error) {
cfg := &tonOpts{expectedStatus: cctypes.CctxStatus_OutboundMined}
for _, opt := range opts {
opt(cfg)
}

chain := chains.TONLocalnet

require.NotNil(r, r.TONGateway, "TON Gateway is not initialized")
Expand All @@ -91,18 +113,26 @@ func (r *E2ERunner) TONDepositAndCall(
}

filter := func(cctx *cctypes.CrossChainTx) bool {
memo := zevmRecipient.Bytes()
memo = append(memo, callData...)

return cctx.InboundParams.SenderChainId == chain.ChainId &&
cctx.InboundParams.Sender == sender.GetAddress().ToRaw()
cctx.InboundParams.Sender == sender.GetAddress().ToRaw() &&
cctx.RelayedMessage == hex.EncodeToString(memo)
}

// Wait for cctx
cctx := r.WaitForSpecificCCTX(filter, time.Minute)
cctx := r.WaitForSpecificCCTX(filter, cfg.expectedStatus, time.Minute)

return cctx, nil
}

// WithdrawTONZRC20 withdraws an amount of ZRC20 TON tokens
func (r *E2ERunner) WithdrawTONZRC20(to ton.AccountID, amount *big.Int, approveAmount *big.Int) *cctypes.CrossChainTx {
// SendWithdrawTONZRC20 sends withdraw tx of TON ZRC20 tokens
func (r *E2ERunner) SendWithdrawTONZRC20(
to ton.AccountID,
amount *big.Int,
approveAmount *big.Int,
) *ethtypes.Transaction {
// approve
tx, err := r.TONZRC20.Approve(r.ZEVMAuth, r.TONZRC20Addr, approveAmount)
require.NoError(r, err)
Expand All @@ -119,6 +149,13 @@ func (r *E2ERunner) WithdrawTONZRC20(to ton.AccountID, amount *big.Int, approveA
utils.RequireTxSuccessful(r, receipt, "withdraw")
r.Logger.Info("Receipt txhash %s status %d", receipt.TxHash, receipt.Status)

return tx
}

// WithdrawTONZRC20 withdraws an amount of ZRC20 TON tokens and waits for the cctx to be mined
func (r *E2ERunner) WithdrawTONZRC20(to ton.AccountID, amount *big.Int, approveAmount *big.Int) *cctypes.CrossChainTx {
tx := r.SendWithdrawTONZRC20(to, amount, approveAmount)

swift1337 marked this conversation as resolved.
Show resolved Hide resolved
// 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, cctypes.CctxStatus_OutboundMined)
Expand Down
11 changes: 7 additions & 4 deletions e2e/runner/ton/deployer.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,13 @@ func (d *Deployer) Seqno(ctx context.Context) (uint32, error) {
return d.blockchain.GetSeqno(ctx, d.GetAddress())
}

// GetBalanceOf returns the balance of the given account.
func (d *Deployer) GetBalanceOf(ctx context.Context, id ton.AccountID) (math.Uint, error) {
if err := d.waitForAccountActivation(ctx, id); err != nil {
return math.Uint{}, errors.Wrap(err, "failed to wait for account activation")
// GetBalanceOf returns the balance of a given account.
// wait=true waits for account activation.
func (d *Deployer) GetBalanceOf(ctx context.Context, id ton.AccountID, wait bool) (math.Uint, error) {
if wait {
if err := d.waitForAccountActivation(ctx, id); err != nil {
return math.Uint{}, errors.Wrap(err, "failed to wait for account activation")
}
}

state, err := d.blockchain.GetAccountState(ctx, id)
Expand Down
Loading
Loading