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 @@ -2,6 +2,9 @@

## Unreleased

### 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 @@ -422,7 +422,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 @@ -472,6 +474,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 @@ -480,6 +490,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
83 changes: 83 additions & 0 deletions e2e/e2etests/test_ton_withdrawal_concurrent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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
type withdrawal struct {
recipient ton.AccountID
amount math.Uint
}

var (
testCases []withdrawal
wg sync.WaitGroup
)

// Given multiple recipients WITHOUT deployed wallet-contracts
// and sample withdrawal amounts between 1 and 5 TON
for i := 0; i < recipientsCount; i++ {
// #nosec G404: it's a test
amount := 1 + rand.Intn(5)
testCases = append(testCases, withdrawal{
// #nosec G115 test - always in range
amount: toncontracts.Coins(uint64(amount)),
recipient: sample.GenerateTONAccountID(),
})
}

// ACT
// Fire withdrawals. Note that zevm sender is r.ZEVMAuth
for i, tc := range testCases {
r.Logger.Info(
"Withdrawal #%d: sending %s to %s",
i+1,
toncontracts.FormatCoins(tc.amount),
tc.recipient.ToRaw(),
)

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

wg.Add(1)

go func(number int, 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, tc.recipient, false)
require.NoError(r, err, "failed to get balance of %s", tc.recipient.ToRaw())
require.Equal(r, tc.amount.Uint64(), balance.Uint64())
}(i+1, 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
Loading
Loading