diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 2d404ba391..7e0c6e5a4d 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -409,6 +409,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) { tonTests := []string{ e2etests.TestTONDepositName, e2etests.TestTONDepositAndCallName, + e2etests.TestTONWithdrawName, } eg.Go(tonTestRoutine(conf, deployerRunner, verbose, tonTests...)) diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index 2d6eea998f..f027ccb773 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -67,6 +67,7 @@ const ( */ TestTONDepositName = "ton_deposit" TestTONDepositAndCallName = "ton_deposit_and_call" + TestTONWithdrawName = "ton_withdraw" /* Bitcoin tests @@ -458,6 +459,14 @@ var AllE2ETests = []runner.E2ETest{ }, TestTONDepositAndCall, ), + runner.NewE2ETest( + TestTONWithdrawName, + "withdraw TON from ZEVM", + []runner.ArgDefinition{ + {Description: "amount in nano tons", DefaultValue: "2000000000"}, // 2.0 TON + }, + TestTONWithdraw, + ), /* Bitcoin tests */ diff --git a/e2e/e2etests/test_ton_deposit.go b/e2e/e2etests/test_ton_deposit.go index f80d7f3825..4ee6b4a6fb 100644 --- a/e2e/e2etests/test_ton_deposit.go +++ b/e2e/e2etests/test_ton_deposit.go @@ -1,23 +1,19 @@ package e2etests import ( - "time" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/e2e/runner" - "github.com/zeta-chain/node/pkg/chains" toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" "github.com/zeta-chain/node/testutil/sample" - cctypes "github.com/zeta-chain/node/x/crosschain/types" ) func TestTONDeposit(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) // Given deployer - ctx, deployer, chain := r.Ctx, r.TONDeployer, chains.TONLocalnet + ctx, deployer := r.Ctx, r.TONDeployer // Given amount amount := parseUint(r, args[0]) @@ -34,19 +30,11 @@ func TestTONDeposit(r *runner.E2ERunner, args []string) { recipient := sample.EthAddress() // ACT - err = r.TONDeposit(sender, amount, recipient) + cctx, err := r.TONDeposit(sender, amount, recipient) // ASSERT require.NoError(r, err) - // Wait for CCTX mining - filter := func(cctx *cctypes.CrossChainTx) bool { - return cctx.InboundParams.SenderChainId == chain.ChainId && - cctx.InboundParams.Sender == sender.GetAddress().ToRaw() - } - - cctx := r.WaitForSpecificCCTX(filter, time.Minute) - // Check CCTX expectedDeposit := amount.Sub(depositFee) diff --git a/e2e/e2etests/test_ton_deposit_and_call.go b/e2e/e2etests/test_ton_deposit_and_call.go index 78452df088..d0b2cac9e0 100644 --- a/e2e/e2etests/test_ton_deposit_and_call.go +++ b/e2e/e2etests/test_ton_deposit_and_call.go @@ -1,24 +1,20 @@ package e2etests import ( - "time" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" - "github.com/zeta-chain/node/pkg/chains" toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" testcontract "github.com/zeta-chain/node/testutil/contracts" - cctypes "github.com/zeta-chain/node/x/crosschain/types" ) func TestTONDepositAndCall(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) // Given deployer - ctx, deployer, chain := r.Ctx, r.TONDeployer, chains.TONLocalnet + ctx, deployer := r.Ctx, r.TONDeployer // Given amount amount := parseUint(r, args[0]) @@ -40,19 +36,11 @@ func TestTONDepositAndCall(r *runner.E2ERunner, args []string) { callData := []byte("hello from TON!") // ACT - err = r.TONDepositAndCall(sender, amount, contractAddr, callData) + _, err = r.TONDepositAndCall(sender, amount, contractAddr, callData) // ASSERT require.NoError(r, err) - // Wait for CCTX mining - filter := func(cctx *cctypes.CrossChainTx) bool { - return cctx.InboundParams.SenderChainId == chain.ChainId && - cctx.InboundParams.Sender == sender.GetAddress().ToRaw() - } - - r.WaitForSpecificCCTX(filter, time.Minute) - expectedDeposit := amount.Sub(depositFee) // check if example contract has been called, bar value should be set to amount diff --git a/e2e/e2etests/test_ton_withdrawals.go b/e2e/e2etests/test_ton_withdrawals.go new file mode 100644 index 0000000000..2539f63528 --- /dev/null +++ b/e2e/e2etests/test_ton_withdrawals.go @@ -0,0 +1,95 @@ +package e2etests + +import ( + "math/big" + + "cosmossdk.io/math" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/runner" + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" + "github.com/zeta-chain/node/zetaclient/chains/ton/liteapi" +) + +func TestTONWithdraw(r *runner.E2ERunner, args []string) { + // ARRANGE + require.Len(r, args, 1) + + // Given a deployer + _, deployer := r.Ctx, r.TONDeployer + + // That donates 100 TON to some zEVM sender + zevmSender := r.ZEVMAuth.From + + _, err := r.TONDeposit(&deployer.Wallet, toncontracts.Coins(100), zevmSender) + require.NoError(r, err) + + // Given his ZRC-20 balance + senderZRC20BalanceBefore, err := r.TONZRC20.BalanceOf(&bind.CallOpts{}, zevmSender) + require.NoError(r, err) + r.Logger.Print("zEVM sender's ZRC20 TON balance before withdraw: %d", senderZRC20BalanceBefore) + + // Given another TON wallet + tonRecipient, err := deployer.CreateWallet(r.Ctx, toncontracts.Coins(1)) + require.NoError(r, err) + + tonRecipientBalanceBefore, err := deployer.GetBalanceOf(r.Ctx, tonRecipient.GetAddress()) + require.NoError(r, err) + + r.Logger.Print("Recipient's TON balance before withdrawal: %s", toncontracts.FormatCoins(tonRecipientBalanceBefore)) + + // Given amount to withdraw (and approved amount in TON ZRC20 to cover the gas fee) + amount := parseUint(r, args[0]) + approvedAmount := amount.Add(toncontracts.Coins(1)) + + // ACT + cctx := r.WithdrawTONZRC20(tonRecipient.GetAddress(), amount.BigInt(), approvedAmount.BigInt()) + + // ASSERT + r.Logger.Print( + "Withdraw TON ZRC20 transaction (with %s) sent: %+v", + toncontracts.FormatCoins(amount), + map[string]any{ + "zevm_sender": zevmSender.Hex(), + "ton_recipient": tonRecipient.GetAddress().ToRaw(), + "ton_amount": toncontracts.FormatCoins(amount), + "cctx_index": cctx.Index, + "ton_hash": cctx.GetCurrentOutboundParam().Hash, + "zevm_hash": cctx.InboundParams.ObservedHash, + }, + ) + + // Make sure that recipient's TON balance has increased + tonRecipientBalanceAfter, err := deployer.GetBalanceOf(r.Ctx, tonRecipient.GetAddress()) + require.NoError(r, err) + + r.Logger.Print("Recipient's balance after withdrawal: %s", toncontracts.FormatCoins(tonRecipientBalanceAfter)) + + // Make sure that sender's ZRC20 balance has decreased + senderZRC20BalanceAfter, err := r.TONZRC20.BalanceOf(&bind.CallOpts{}, zevmSender) + require.NoError(r, err) + r.Logger.Print("zEVM sender's ZRC20 TON balance after withdraw: %d", senderZRC20BalanceAfter) + r.Logger.Print( + "zEVM sender's ZRC20 TON balance diff: %d", + big.NewInt(0).Sub(senderZRC20BalanceBefore, senderZRC20BalanceAfter), + ) + + // Make sure that TON withdrawal CCTX contain outgoing message with exact withdrawal amount + lt, hash, err := liteapi.TransactionHashFromString(cctx.GetCurrentOutboundParam().Hash) + require.NoError(r, err) + + txs, err := r.Clients.TON.GetTransactions(r.Ctx, 1, r.TONGateway.AccountID(), lt, hash) + require.NoError(r, err) + require.Len(r, txs, 1) + + // TON coins that were withdrawn from GW to the recipient + inMsgAmount := math.NewUint( + uint64(txs[0].Msgs.OutMsgs.Values()[0].Value.Info.IntMsgInfo.Value.Grams), + ) + + require.Equal(r, int(amount.Uint64()), int(inMsgAmount.Uint64())) +} + +// TODO: Add "withdraw_many_concurrent" test +// https://github.com/zeta-chain/node/issues/3044 diff --git a/e2e/runner/setup_ton.go b/e2e/runner/setup_ton.go index 07d7939977..c7fe136d38 100644 --- a/e2e/runner/setup_ton.go +++ b/e2e/runner/setup_ton.go @@ -101,6 +101,11 @@ func (r *E2ERunner) ensureTONChainParams(gw *ton.AccountInit) error { return errors.Wrap(err, "unable to broadcast TON chain params tx") } + resetMsg := observertypes.NewMsgResetChainNonces(creator, chainID, 0, 0) + if _, err := r.ZetaTxServer.BroadcastTx(utils.OperationalPolicyName, resetMsg); err != nil { + return errors.Wrap(err, "unable to broadcast TON chain nonce reset tx") + } + r.Logger.Print("💎Voted for adding TON chain params (localnet). Waiting for confirmation") query := &observertypes.QueryGetChainParamsForChainRequest{ChainId: chainID} diff --git a/e2e/runner/ton.go b/e2e/runner/ton.go index 8746e25977..eb8e5346ac 100644 --- a/e2e/runner/ton.go +++ b/e2e/runner/ton.go @@ -1,12 +1,20 @@ package runner import ( + "math/big" + "time" + "cosmossdk.io/math" eth "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" "github.com/stretchr/testify/require" + "github.com/tonkeeper/tongo/ton" "github.com/tonkeeper/tongo/wallet" + "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/pkg/chains" toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" + cctypes "github.com/zeta-chain/node/x/crosschain/types" ) // we need to use this send mode due to how wallet V5 works @@ -16,7 +24,13 @@ import ( const tonDepositSendCode = toncontracts.SendFlagSeparateFees + toncontracts.SendFlagIgnoreErrors // TONDeposit deposit TON to Gateway contract -func (r *E2ERunner) TONDeposit(sender *wallet.Wallet, amount math.Uint, zevmRecipient eth.Address) error { +func (r *E2ERunner) TONDeposit( + sender *wallet.Wallet, + amount math.Uint, + zevmRecipient eth.Address, +) (*cctypes.CrossChainTx, error) { + chain := chains.TONLocalnet + require.NotNil(r, r.TONGateway, "TON Gateway is not initialized") require.NotNil(r, sender, "Sender wallet is nil") @@ -30,7 +44,21 @@ func (r *E2ERunner) TONDeposit(sender *wallet.Wallet, amount math.Uint, zevmReci zevmRecipient.Hex(), ) - return r.TONGateway.SendDeposit(r.Ctx, sender, amount, zevmRecipient, tonDepositSendCode) + // Send TX + err := r.TONGateway.SendDeposit(r.Ctx, sender, amount, zevmRecipient, tonDepositSendCode) + if err != nil { + return nil, errors.Wrap(err, "failed to send TON deposit") + } + + filter := func(cctx *cctypes.CrossChainTx) bool { + return cctx.InboundParams.SenderChainId == chain.ChainId && + cctx.InboundParams.Sender == sender.GetAddress().ToRaw() + } + + // Wait for cctx + cctx := r.WaitForSpecificCCTX(filter, time.Minute) + + return cctx, nil } // TONDepositAndCall deposit TON to Gateway contract with call data. @@ -39,7 +67,9 @@ func (r *E2ERunner) TONDepositAndCall( amount math.Uint, zevmRecipient eth.Address, callData []byte, -) error { +) (*cctypes.CrossChainTx, error) { + chain := chains.TONLocalnet + require.NotNil(r, r.TONGateway, "TON Gateway is not initialized") require.NotNil(r, sender, "Sender wallet is nil") @@ -55,5 +85,43 @@ func (r *E2ERunner) TONDepositAndCall( string(callData), ) - return r.TONGateway.SendDepositAndCall(r.Ctx, sender, amount, zevmRecipient, callData, tonDepositSendCode) + err := r.TONGateway.SendDepositAndCall(r.Ctx, sender, amount, zevmRecipient, callData, tonDepositSendCode) + if err != nil { + return nil, errors.Wrap(err, "failed to send TON deposit and call") + } + + filter := func(cctx *cctypes.CrossChainTx) bool { + return cctx.InboundParams.SenderChainId == chain.ChainId && + cctx.InboundParams.Sender == sender.GetAddress().ToRaw() + } + + // Wait for cctx + cctx := r.WaitForSpecificCCTX(filter, 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 { + // approve + tx, err := r.TONZRC20.Approve(r.ZEVMAuth, r.TONZRC20Addr, 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.TONZRC20.Withdraw(r.ZEVMAuth, []byte(to.ToRaw()), 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, cctypes.CctxStatus_OutboundMined) + + return cctx } diff --git a/pkg/chains/chain.go b/pkg/chains/chain.go index 665251eb39..0102e74517 100644 --- a/pkg/chains/chain.go +++ b/pkg/chains/chain.go @@ -81,6 +81,12 @@ func (chain Chain) EncodeAddress(b []byte) (string, error) { return "", err } return pk.String(), nil + case Consensus_catchain_consensus: + acc, err := ton.ParseAccountID(string(b)) + if err != nil { + return "", err + } + return acc.ToRaw(), nil default: return "", fmt.Errorf("chain id %d not supported", chain.ChainId) }