Skip to content

Commit

Permalink
feat(ton): adjacent TON tasks (#3075)
Browse files Browse the repository at this point in the history
* Implement InboundTracker; minor refactoring

* E2E: Concurrent withdrawals [WIP]

* E2E: Concurrent withdrawals

* Improve signer broadcasting & logging

* Add e2e for deposit & refund

* Update changelog

* Address PR comments

* Address PR comments [2]
  • Loading branch information
swift1337 authored Nov 5, 2024
1 parent 44c377a commit 041df59
Show file tree
Hide file tree
Showing 18 changed files with 546 additions and 111 deletions.
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,
),
/*
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")
)

// 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())

// ACT
// Send a deposit and call transaction from the deployer (faucet)
// 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)
}
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)

// 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

0 comments on commit 041df59

Please sign in to comment.