diff --git a/changelog.md b/changelog.md index 38bbe7aa54..45eb187472 100644 --- a/changelog.md +++ b/changelog.md @@ -19,7 +19,9 @@ * [3206](https://github.com/zeta-chain/node/pull/3206) - skip Solana unsupported transaction version to not block inbound observation * [3184](https://github.com/zeta-chain/node/pull/3184) - zetaclient should not retry if inbound vote message validation fails +* [3230](https://github.com/zeta-chain/node/pull/3230) - update pending nonces when aborting a cctx through MsgAbortStuckCCTX * [3225](https://github.com/zeta-chain/node/pull/3225) - use separate database file names for btc signet and testnet4 +* [3242](https://github.com/zeta-chain/node/pull/3242) - set the `Receiver` of `MsgVoteInbound` to the address pulled from solana memo * [3253](https://github.com/zeta-chain/node/pull/3253) - fix solana inbound version 0 queries and move tss keysign prior to relayer key checking ## v23.0.0 diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index aacaade9b3..d7297d4068 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -212,6 +212,9 @@ func localE2ETest(cmd *cobra.Command, _ []string) { })) } + e2eStartHeight, err := deployerRunner.Clients.Zetacore.GetBlockHeight(ctx) + noError(err) + // setting up the networks if !skipSetup { logger.Print("⚙️ setting up networks") @@ -250,8 +253,6 @@ func localE2ETest(cmd *cobra.Command, _ []string) { // Update the chain params to contains protocol contract addresses deployerRunner.UpdateProtocolContractsInChainParams() - deployerRunner.MintERC20OnEVM(1e10) - logger.Print("✅ setup completed in %s", time.Since(startTime)) } @@ -280,6 +281,9 @@ func localE2ETest(cmd *cobra.Command, _ []string) { deployerRunner.UpgradeGatewaysAndERC20Custody() } + // always mint ERC20 before every test execution + deployerRunner.MintERC20OnEVM(1e10) + // run tests var eg errgroup.Group @@ -317,6 +321,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) { e2etests.TestBitcoinWithdrawMultipleName, e2etests.TestBitcoinWithdrawRestrictedName, } + if !light { // if light is enabled, only the most basic tests are run and advanced are skipped bitcoinDepositTests = append(bitcoinDepositTests, bitcoinDepositTestsAdvanced...) @@ -335,19 +340,28 @@ func localE2ETest(cmd *cobra.Command, _ []string) { } if !skipPrecompiles { - eg.Go(statefulPrecompilesTestRoutine(conf, deployerRunner, verbose, + precompiledContractTests := []string{ e2etests.TestPrecompilesPrototypeName, e2etests.TestPrecompilesPrototypeThroughContractName, - e2etests.TestPrecompilesStakingName, // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. // e2etests.TestPrecompilesStakingThroughContractName, e2etests.TestPrecompilesBankName, e2etests.TestPrecompilesBankFailName, e2etests.TestPrecompilesBankThroughContractName, - e2etests.TestPrecompilesDistributeName, - e2etests.TestPrecompilesDistributeNonZRC20Name, - e2etests.TestPrecompilesDistributeThroughContractName, - )) + } + if e2eStartHeight < 100 { + // these tests require a clean system + // since unstaking has an unbonding period + precompiledContractTests = append(precompiledContractTests, + e2etests.TestPrecompilesStakingName, + e2etests.TestPrecompilesDistributeName, + e2etests.TestPrecompilesDistributeNonZRC20Name, + e2etests.TestPrecompilesDistributeThroughContractName, + ) + } else { + logger.Print("⚠️ partial precompiled run (unclean state)") + } + eg.Go(statefulPrecompilesTestRoutine(conf, deployerRunner, verbose, precompiledContractTests...)) } if testAdmin { diff --git a/e2e/e2etests/helpers.go b/e2e/e2etests/helpers.go index 662be02679..a06149139e 100644 --- a/e2e/e2etests/helpers.go +++ b/e2e/e2etests/helpers.go @@ -78,3 +78,13 @@ func withdrawBTCZRC20(r *runner.E2ERunner, to btcutil.Address, amount *big.Int) return rawTx } + +// bigAdd is shorthand for new(big.Int).Add(x, y) +func bigAdd(x *big.Int, y *big.Int) *big.Int { + return new(big.Int).Add(x, y) +} + +// bigSub is shorthand for new(big.Int).Sub(x, y) +func bigSub(x *big.Int, y *big.Int) *big.Int { + return new(big.Int).Sub(x, y) +} diff --git a/e2e/e2etests/test_precompiles_bank.go b/e2e/e2etests/test_precompiles_bank.go index 1745a9e0b5..c62837f935 100644 --- a/e2e/e2etests/test_precompiles_bank.go +++ b/e2e/e2etests/test_precompiles_bank.go @@ -46,6 +46,12 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { utils.RequireTxSuccessful(r, receipt, "Resetting balance failed") }() + // Ensure starting allowance is zero; this is needed when running the tests multiple times + tx, err := r.ERC20ZRC20.Approve(r.ZEVMAuth, bankAddress, big.NewInt(0)) + require.NoError(r, err) + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "Resetting allowance failed") + // Get ERC20ZRC20. txHash := r.LegacyDepositERC20WithAmountAndMessage(r.EVMAddress(), totalAmount, []byte{}) utils.WaitCctxMinedByInboundHash(r.Ctx, txHash.Hex(), r.CctxClient, r.Logger, r.CctxTimeout) @@ -54,15 +60,18 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { bankContract, err := bank.NewIBank(bank.ContractAddress, r.ZEVMClient) require.NoError(r, err, "Failed to create bank contract caller") - // Cosmos coin balance should be 0 at this point. - cosmosBalance, err := bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.ERC20ZRC20Addr, spender) + // get starting balances + startSpenderCosmosBalance, err := bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.ERC20ZRC20Addr, spender) require.NoError(r, err, "Call bank.BalanceOf()") - require.Equal(r, uint64(0), cosmosBalance.Uint64(), "spender cosmos coin balance should be 0") + startSpenderZRC20Balance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, spender) + require.NoError(r, err, "Call bank.BalanceOf()") + startBankZRC20Balance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, bankAddress) + require.NoError(r, err, "Call ERC20ZRC20.BalanceOf") // Approve allowance of 500 ERC20ZRC20 tokens for the bank contract. Should pass. - tx, err := r.ERC20ZRC20.Approve(r.ZEVMAuth, bankAddress, depositAmount) + tx, err = r.ERC20ZRC20.Approve(r.ZEVMAuth, bankAddress, depositAmount) require.NoError(r, err) - receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) utils.RequireTxSuccessful(r, receipt, "Approve ETHZRC20 bank allowance tx failed") // Deposit 501 ERC20ZRC20 tokens to the bank contract. @@ -99,17 +108,27 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { require.Equal(r, depositAmount, eventDeposit.Amount, "Deposit event amount should be 500") // Spender: cosmos coin balance should be 500 at this point. - cosmosBalance, err = bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.ERC20ZRC20Addr, spender) + spenderCosmosBalance, err := bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.ERC20ZRC20Addr, spender) require.NoError(r, err, "Call bank.BalanceOf()") - require.Equal(r, uint64(500), cosmosBalance.Uint64(), "spender cosmos coin balance should be 500") + require.Equal( + r, + startSpenderCosmosBalance.Int64()+500, + spenderCosmosBalance.Int64(), + "spender cosmos coin balance should be +500", + ) // Bank: ERC20ZRC20 balance should be 500 tokens locked. bankZRC20Balance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, bankAddress) require.NoError(r, err, "Call ERC20ZRC20.BalanceOf") - require.Equal(r, uint64(500), bankZRC20Balance.Uint64(), "bank ERC20ZRC20 balance should be 500") + require.Equal( + r, + startBankZRC20Balance.Int64()+500, + bankZRC20Balance.Int64(), + "bank ERC20ZRC20 balance should be +500", + ) - // Try to withdraw 501 ERC20ZRC20 tokens. Should fail. - tx, err = bankContract.Withdraw(r.ZEVMAuth, r.ERC20ZRC20Addr, big.NewInt(501)) + // Try to withdraw one more than current balance. Should fail. + tx, err = bankContract.Withdraw(r.ZEVMAuth, r.ERC20ZRC20Addr, new(big.Int).Add(spenderCosmosBalance, big.NewInt(1))) require.NoError(r, err, "Error calling bank.withdraw()") receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) utils.RequiredTxFailed(r, receipt, "Withdrawing more than cosmos coin balance amount should fail") @@ -118,7 +137,12 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { // No tokens should be unlocked with a failed withdraw. bankZRC20Balance, err = r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, bankAddress) require.NoError(r, err, "Call ERC20ZRC20.BalanceOf") - require.Equal(r, uint64(500), bankZRC20Balance.Uint64(), "bank ERC20ZRC20 balance should be 500") + require.Equal( + r, + startBankZRC20Balance.Int64()+500, + bankZRC20Balance.Int64(), + "bank ERC20ZRC20 balance should be +500", + ) // Try to withdraw 500 ERC20ZRC20 tokens. Should pass. tx, err = bankContract.Withdraw(r.ZEVMAuth, r.ERC20ZRC20Addr, depositAmount) @@ -133,20 +157,35 @@ func TestPrecompilesBank(r *runner.E2ERunner, args []string) { require.Equal(r, r.ERC20ZRC20Addr, eventWithdraw.Zrc20Token, "Withdraw event token should be ERC20ZRC20Addr") require.Equal(r, depositAmount, eventWithdraw.Amount, "Withdraw event amount should be 500") - // Spender: cosmos coin balance should be 0 at this point. - cosmosBalance, err = bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.ERC20ZRC20Addr, spender) + // Spender: cosmos coin balance should be +0 at this point. + spenderCosmosBalance, err = bankContract.BalanceOf(&bind.CallOpts{Context: r.Ctx}, r.ERC20ZRC20Addr, spender) require.NoError(r, err, "Call bank.BalanceOf()") - require.Equal(r, uint64(0), cosmosBalance.Uint64(), "spender cosmos coin balance should be 0") + require.Equal( + r, + startSpenderCosmosBalance.Int64(), + spenderCosmosBalance.Int64(), + "spender cosmos coin balance should match starting balance", + ) - // Spender: ERC20ZRC20 balance should be 1000 at this point. - zrc20Balance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, spender) - require.NoError(r, err, "Call bank.BalanceOf()") - require.Equal(r, uint64(1000), zrc20Balance.Uint64(), "spender ERC20ZRC20 balance should be 1000") + // Spender: ERC20ZRC20 balance should be +0 at this point. + spenderZRC20Balance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, spender) + require.NoError(r, err, "Call ERC20ZRC20.BalanceOf") + require.Equal( + r, + startSpenderZRC20Balance.Int64(), + spenderZRC20Balance.Int64(), + "spender ERC20ZRC20 balance should match starting balance", + ) - // Bank: ERC20ZRC20 balance should be 0 tokens locked. + // Bank: ERC20ZRC20 balance should be +0 tokens locked. bankZRC20Balance, err = r.ERC20ZRC20.BalanceOf(&bind.CallOpts{Context: r.Ctx}, bankAddress) require.NoError(r, err, "Call ERC20ZRC20.BalanceOf") - require.Equal(r, uint64(0), bankZRC20Balance.Uint64(), "bank ERC20ZRC20 balance should be 0") + require.Equal( + r, + startBankZRC20Balance.Int64(), + bankZRC20Balance.Int64(), + "bank ERC20ZRC20 balance should match starting balance", + ) } func TestPrecompilesBankNonZRC20(r *runner.E2ERunner, args []string) { diff --git a/e2e/e2etests/test_precompiles_bank_through_contract.go b/e2e/e2etests/test_precompiles_bank_through_contract.go index 9801325f41..d7f7af0491 100644 --- a/e2e/e2etests/test_precompiles_bank_through_contract.go +++ b/e2e/e2etests/test_precompiles_bank_through_contract.go @@ -47,7 +47,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { r.ZEVMAuth.GasLimit = previousGasLimit // Reset the allowance to 0; this is needed when running upgrade tests where this test runs twice. - approveAllowance(r, bank.ContractAddress, big.NewInt(0)) + approveAllowance(r, bank.ContractAddress, zero) // Reset balance to 0; this is needed when running upgrade tests where this test runs twice. tx, err = r.ERC20ZRC20.Transfer( @@ -60,19 +60,22 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { utils.RequireTxSuccessful(r, receipt, "Resetting balance failed") }() - // Check initial balances. - balanceShouldBe(r, zero, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) - balanceShouldBe(r, oneThousand, checkZRC20Balance(r, spender)) - balanceShouldBe(r, zero, checkZRC20Balance(r, bankAddress)) + // always ensure allowance is set to zero before test starts + approveAllowance(r, bank.ContractAddress, zero) + + // get starting balances + startSpenderCosmosBalance := checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender) + startSpenderZRC20Balance := checkZRC20Balance(r, spender) + startBankZRC20Balance := checkZRC20Balance(r, bankAddress) // Deposit without previous alllowance should fail. receipt = depositThroughTestBank(r, testBank, zrc20Address, oneThousand) utils.RequiredTxFailed(r, receipt, "Deposit ERC20ZRC20 without allowance should fail") // Check balances, should be the same. - balanceShouldBe(r, zero, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) - balanceShouldBe(r, oneThousand, checkZRC20Balance(r, spender)) - balanceShouldBe(r, zero, checkZRC20Balance(r, bankAddress)) + balanceShouldBe(r, startSpenderCosmosBalance, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, startSpenderZRC20Balance, checkZRC20Balance(r, spender)) + balanceShouldBe(r, startBankZRC20Balance, checkZRC20Balance(r, bankAddress)) // Allow 500 ZRC20 to bank precompile. approveAllowance(r, bankAddress, fiveHundred) @@ -83,9 +86,9 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { utils.RequiredTxFailed(r, receipt, "Depositting an amount higher than allowed should fail") // Balances shouldn't change. - balanceShouldBe(r, zero, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) - balanceShouldBe(r, oneThousand, checkZRC20Balance(r, spender)) - balanceShouldBe(r, zero, checkZRC20Balance(r, bankAddress)) + balanceShouldBe(r, startSpenderCosmosBalance, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, startSpenderZRC20Balance, checkZRC20Balance(r, spender)) + balanceShouldBe(r, startBankZRC20Balance, checkZRC20Balance(r, bankAddress)) // Allow 1000 ZRC20 to bank precompile. approveAllowance(r, bankAddress, oneThousand) @@ -96,18 +99,22 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { utils.RequiredTxFailed(r, receipt, "Depositting an amount higher than balance should fail") // Balances shouldn't change. - balanceShouldBe(r, zero, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) - balanceShouldBe(r, oneThousand, checkZRC20Balance(r, spender)) - balanceShouldBe(r, zero, checkZRC20Balance(r, bankAddress)) + balanceShouldBe(r, startSpenderCosmosBalance, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, startSpenderZRC20Balance, checkZRC20Balance(r, spender)) + balanceShouldBe(r, startBankZRC20Balance, checkZRC20Balance(r, bankAddress)) // Deposit 500 ERC20ZRC20 tokens to the bank contract, it's within allowance and balance. Should pass. receipt = depositThroughTestBank(r, testBank, zrc20Address, fiveHundred) utils.RequireTxSuccessful(r, receipt, "Depositting a correct amount should pass") // Balances should be transferred. Bank now locks 500 ZRC20 tokens. - balanceShouldBe(r, fiveHundred, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) - balanceShouldBe(r, fiveHundred, checkZRC20Balance(r, spender)) - balanceShouldBe(r, fiveHundred, checkZRC20Balance(r, bankAddress)) + balanceShouldBe( + r, + bigAdd(startSpenderCosmosBalance, fiveHundred), + checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender), + ) + balanceShouldBe(r, bigSub(startSpenderZRC20Balance, fiveHundred), checkZRC20Balance(r, spender)) + balanceShouldBe(r, bigAdd(startBankZRC20Balance, fiveHundred), checkZRC20Balance(r, bankAddress)) // Check the deposit event. eventDeposit, err := bankPrecompileCaller.ParseDeposit(*receipt.Logs[0]) @@ -117,22 +124,26 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { require.Equal(r, fiveHundred, eventDeposit.Amount, "Deposit event amount should be 500") // Should faild to withdraw more than cosmos balance. - receipt = withdrawThroughTestBank(r, testBank, zrc20Address, fiveHundredOne) + receipt = withdrawThroughTestBank(r, testBank, zrc20Address, bigAdd(startSpenderCosmosBalance, fiveHundredOne)) utils.RequiredTxFailed(r, receipt, "Withdrawing an amount higher than balance should fail") // Balances shouldn't change. - balanceShouldBe(r, fiveHundred, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) - balanceShouldBe(r, fiveHundred, checkZRC20Balance(r, spender)) - balanceShouldBe(r, fiveHundred, checkZRC20Balance(r, bankAddress)) + balanceShouldBe( + r, + bigAdd(startSpenderCosmosBalance, fiveHundred), + checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender), + ) + balanceShouldBe(r, bigSub(startSpenderZRC20Balance, fiveHundred), checkZRC20Balance(r, spender)) + balanceShouldBe(r, bigAdd(startBankZRC20Balance, fiveHundred), checkZRC20Balance(r, bankAddress)) // Try to withdraw 500 ERC20ZRC20 tokens. Should pass. receipt = withdrawThroughTestBank(r, testBank, zrc20Address, fiveHundred) utils.RequireTxSuccessful(r, receipt, "Withdraw correct amount should pass") // Balances should be reverted to initial state. - balanceShouldBe(r, zero, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) - balanceShouldBe(r, oneThousand, checkZRC20Balance(r, spender)) - balanceShouldBe(r, zero, checkZRC20Balance(r, bankAddress)) + balanceShouldBe(r, startSpenderCosmosBalance, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, startSpenderZRC20Balance, checkZRC20Balance(r, spender)) + balanceShouldBe(r, startBankZRC20Balance, checkZRC20Balance(r, bankAddress)) // Check the withdraw event. eventWithdraw, err := bankPrecompileCaller.ParseWithdraw(*receipt.Logs[0]) diff --git a/e2e/e2etests/test_solana_deposit.go b/e2e/e2etests/test_solana_deposit.go index d71b2cbdad..fba35a61ff 100644 --- a/e2e/e2etests/test_solana_deposit.go +++ b/e2e/e2etests/test_solana_deposit.go @@ -29,6 +29,7 @@ func TestSolanaDeposit(r *runner.E2ERunner, args []string) { cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, sig.String(), r.CctxClient, r.Logger, r.CctxTimeout) r.Logger.CCTX(*cctx, "solana_deposit") utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) + require.Equal(r, cctx.GetCurrentOutboundParam().Receiver, r.EVMAddress().Hex()) // get ERC20 SOL balance after deposit balanceAfter, err := r.SOLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) diff --git a/e2e/e2etests/test_solana_deposit_call.go b/e2e/e2etests/test_solana_deposit_call.go index 20b220ad32..8401b04e5d 100644 --- a/e2e/e2etests/test_solana_deposit_call.go +++ b/e2e/e2etests/test_solana_deposit_call.go @@ -29,6 +29,7 @@ func TestSolanaDepositAndCall(r *runner.E2ERunner, args []string) { cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, sig.String(), r.CctxClient, r.Logger, r.CctxTimeout) r.Logger.CCTX(*cctx, "solana_deposit_and_call") utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) + require.Equal(r, cctx.GetCurrentOutboundParam().Receiver, contractAddr.Hex()) // check if example contract has been called, bar value should be set to amount utils.MustHaveCalledExampleContract(r, contract, depositAmount) diff --git a/e2e/e2etests/test_spl_deposit.go b/e2e/e2etests/test_spl_deposit.go index 78a1847e14..28e0e3ef9f 100644 --- a/e2e/e2etests/test_spl_deposit.go +++ b/e2e/e2etests/test_spl_deposit.go @@ -42,6 +42,7 @@ func TestSPLDeposit(r *runner.E2ERunner, args []string) { cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, sig.String(), r.CctxClient, r.Logger, r.CctxTimeout) r.Logger.CCTX(*cctx, "solana_deposit_spl") utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) + require.Equal(r, cctx.GetCurrentOutboundParam().Receiver, r.EVMAddress().Hex()) // verify balances are updated pdaBalanceAfter, err := r.SolanaClient.GetTokenAccountBalance(r.Ctx, pdaAta, rpc.CommitmentFinalized) diff --git a/e2e/e2etests/test_spl_deposit_and_call.go b/e2e/e2etests/test_spl_deposit_and_call.go index 434141064c..498ec4f8b6 100644 --- a/e2e/e2etests/test_spl_deposit_and_call.go +++ b/e2e/e2etests/test_spl_deposit_and_call.go @@ -49,6 +49,7 @@ func TestSPLDepositAndCall(r *runner.E2ERunner, args []string) { cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, sig.String(), r.CctxClient, r.Logger, r.CctxTimeout) r.Logger.CCTX(*cctx, "solana_deposit_spl_and_call") utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) + require.Equal(r, cctx.GetCurrentOutboundParam().Receiver, contractAddr.Hex()) // check if example contract has been called, bar value should be set to amount utils.MustHaveCalledExampleContract(r, contract, big.NewInt(int64(amount))) diff --git a/e2e/utils/zetacore.go b/e2e/utils/zetacore.go index bf01d79ec8..681e3bca7d 100644 --- a/e2e/utils/zetacore.go +++ b/e2e/utils/zetacore.go @@ -113,7 +113,7 @@ func WaitCctxsMinedByInboundHash( allFound := true for j, cctx := range res.CrossChainTxs { cctx := cctx - if !IsTerminalStatus(cctx.CctxStatus.Status) { + if !cctx.CctxStatus.Status.IsTerminal() { // prevent spamming logs if i%20 == 0 { logger.Info( @@ -170,7 +170,7 @@ func WaitCCTXMinedByIndex( } cctx := res.CrossChainTx - if !IsTerminalStatus(cctx.CctxStatus.Status) { + if !cctx.CctxStatus.Status.IsTerminal() { // prevent spamming logs if i%20 == 0 { logger.Info( @@ -299,12 +299,6 @@ func WaitCctxByInboundHash( } } -func IsTerminalStatus(status crosschaintypes.CctxStatus) bool { - return status == crosschaintypes.CctxStatus_OutboundMined || - status == crosschaintypes.CctxStatus_Aborted || - status == crosschaintypes.CctxStatus_Reverted -} - // WaitForBlockHeight waits until the block height reaches the given height func WaitForBlockHeight( ctx context.Context, diff --git a/x/crosschain/genesis.go b/x/crosschain/genesis.go index 467f532a53..8ea365e059 100644 --- a/x/crosschain/genesis.go +++ b/x/crosschain/genesis.go @@ -1,6 +1,7 @@ package crosschain import ( + sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/zeta-chain/node/x/crosschain/keeper" @@ -10,7 +11,9 @@ import ( // InitGenesis initializes the crosschain module's state from a provided genesis // state. func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) { - k.SetZetaAccounting(ctx, genState.ZetaAccounting) + // Always set the zeta accounting to zero at genesis. + // ZetaAccounting value is build by iterating through all the cctxs and adding the amount to the zeta accounting. + k.SetZetaAccounting(ctx, types.ZetaAccounting{AbortedZetaAmount: sdkmath.ZeroUint()}) // Set all the outbound tracker for _, elem := range genState.OutboundTrackerList { k.SetOutboundTracker(ctx, elem) @@ -47,7 +50,7 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) if found { for _, elem := range genState.CrossChainTxs { if elem != nil { - k.SetCctxAndNonceToCctxAndInboundHashToCctx(ctx, *elem, tss.TssPubkey) + k.SaveCCTXUpdate(ctx, *elem, tss.TssPubkey) } } } diff --git a/x/crosschain/keeper/cctx.go b/x/crosschain/keeper/cctx.go index f2234362f0..19f71a78af 100644 --- a/x/crosschain/keeper/cctx.go +++ b/x/crosschain/keeper/cctx.go @@ -11,30 +11,41 @@ import ( observerTypes "github.com/zeta-chain/node/x/observer/types" ) -// SetCctxAndNonceToCctxAndInboundHashToCctx does the following things in one function: -// 1. set the cctx in the store -// 2. set the mapping inboundHash -> cctxIndex , one inboundHash can be connected to multiple cctxindex -// 3. set the mapping nonce => cctx +// SaveCCTXUpdate does the following things in one function: + +// 1. Set the Nonce to Cctx mapping +// A new mapping between a nonce and a cctx index should be created only when we add a new outbound to an existing cctx. +// When adding a new outbound , the only two conditions are +// - The cctx is in CctxStatus_PendingOutbound , which means the first outbound has been added, and we need to set the nonce for that +// - The cctx is in CctxStatus_PendingRevert , which means the second outbound has been added, and we need to set the nonce for that + +// 2. Set the cctx in the store + +// 3. Update the mapping inboundHash -> cctxIndex +// A new value is added to the mapping when a single inbound hash is connected to multiple cctx indexes +// If the inbound hash to cctx mapping does not exist, a new mapping is created and the cctx index is added to the list of cctx indexes + // 4. update the zeta accounting -func (k Keeper) SetCctxAndNonceToCctxAndInboundHashToCctx( +// Zeta-accounting is updated aborted cctxs of cointtype zeta.When a cctx is aborted it means that `GetAbortedAmount` +//of zeta is locked and cannot be used. + +func (k Keeper) SaveCCTXUpdate( ctx sdk.Context, cctx types.CrossChainTx, tssPubkey string, ) { - // set mapping nonce => cctxIndex - if cctx.CctxStatus.Status == types.CctxStatus_PendingOutbound || - cctx.CctxStatus.Status == types.CctxStatus_PendingRevert { - k.GetObserverKeeper().SetNonceToCctx(ctx, observerTypes.NonceToCctx{ - ChainId: cctx.GetCurrentOutboundParam().ReceiverChainId, - // #nosec G115 always in range - Nonce: int64(cctx.GetCurrentOutboundParam().TssNonce), - CctxIndex: cctx.Index, - Tss: tssPubkey, - }) - } - + k.setNonceToCCTX(ctx, cctx, tssPubkey) k.SetCrossChainTx(ctx, cctx) - // set mapping inboundHash -> cctxIndex + k.updateInboundHashToCCTX(ctx, cctx) + k.updateZetaAccounting(ctx, cctx) +} + +// updateInboundHashToCCTX updates the mapping between an inbound hash and a cctx index. +// A new index is added to the list of cctx indexes if it is not already present +func (k Keeper) updateInboundHashToCCTX( + ctx sdk.Context, + cctx types.CrossChainTx, +) { in, _ := k.GetInboundHashToCctx(ctx, cctx.InboundParams.ObservedHash) in.InboundHash = cctx.InboundParams.ObservedHash found := false @@ -48,15 +59,42 @@ func (k Keeper) SetCctxAndNonceToCctxAndInboundHashToCctx( in.CctxIndex = append(in.CctxIndex, cctx.Index) } k.SetInboundHashToCctx(ctx, in) +} - if cctx.CctxStatus.Status == types.CctxStatus_Aborted && cctx.InboundParams.CoinType == coin.CoinType_Zeta { +// updateZetaAccounting updates the zeta accounting with the amount of zeta that was locked in an aborted cctx +func (k Keeper) updateZetaAccounting( + ctx sdk.Context, + cctx types.CrossChainTx, +) { + if cctx.CctxStatus.Status == types.CctxStatus_Aborted && + cctx.InboundParams.CoinType == coin.CoinType_Zeta && + cctx.CctxStatus.IsAbortRefunded == false { k.AddZetaAbortedAmount(ctx, GetAbortedAmount(cctx)) } } +// setNonceToCCTX updates the mapping between a nonce and a cctx index if the cctx is in a PendingOutbound or PendingRevert state +func (k Keeper) setNonceToCCTX( + ctx sdk.Context, + cctx types.CrossChainTx, + tssPubkey string, +) { + // set mapping nonce => cctxIndex + if cctx.CctxStatus.Status == types.CctxStatus_PendingOutbound || + cctx.CctxStatus.Status == types.CctxStatus_PendingRevert { + k.GetObserverKeeper().SetNonceToCctx(ctx, observerTypes.NonceToCctx{ + ChainId: cctx.GetCurrentOutboundParam().ReceiverChainId, + // #nosec G115 always in range + Nonce: int64(cctx.GetCurrentOutboundParam().TssNonce), + CctxIndex: cctx.Index, + Tss: tssPubkey, + }) + } +} + // SetCrossChainTx set a specific cctx in the store from its index func (k Keeper) SetCrossChainTx(ctx sdk.Context, cctx types.CrossChainTx) { - // only set the update timestamp if the block height is >0 to allow + // only set the updated timestamp if the block height is >0 to allow // for a genesis import if cctx.CctxStatus != nil && ctx.BlockHeight() > 0 { cctx.CctxStatus.LastUpdateTimestamp = ctx.BlockHeader().Time.Unix() diff --git a/x/crosschain/keeper/cctx_orchestrator_validate_inbound.go b/x/crosschain/keeper/cctx_orchestrator_validate_inbound.go index 826e21eca5..d4764d9637 100644 --- a/x/crosschain/keeper/cctx_orchestrator_validate_inbound.go +++ b/x/crosschain/keeper/cctx_orchestrator_validate_inbound.go @@ -51,7 +51,7 @@ func (k Keeper) ValidateInbound( if ok { cctx.InboundParams.ObservedHash = inCctxIndex } - k.SetCctxAndNonceToCctxAndInboundHashToCctx(ctx, cctx, tss.TssPubkey) + k.SaveCCTXUpdate(ctx, cctx, tss.TssPubkey) return &cctx, nil } diff --git a/x/crosschain/keeper/cctx_orchestrator_validate_inbound_test.go b/x/crosschain/keeper/cctx_orchestrator_validate_inbound_test.go index 492ef14db0..3227ef4e5f 100644 --- a/x/crosschain/keeper/cctx_orchestrator_validate_inbound_test.go +++ b/x/crosschain/keeper/cctx_orchestrator_validate_inbound_test.go @@ -58,7 +58,7 @@ func TestKeeper_ValidateInbound(t *testing.T) { Return(observerTypes.PendingNonces{NonceHigh: 1}, true) observerMock.On("SetChainNonces", mock.Anything, mock.Anything).Return(nil) observerMock.On("SetPendingNonces", mock.Anything, mock.Anything).Return(nil) - // setup Mocks for SetCctxAndNonceToCctxAndInboundHashToCctx + // setup Mocks for SaveCCTXUpdate observerMock.On("SetNonceToCctx", mock.Anything, mock.Anything).Return(nil) k.SetGasPrice(ctx, types.GasPrice{ diff --git a/x/crosschain/keeper/cctx_test.go b/x/crosschain/keeper/cctx_test.go index d1e3f6fafc..e532db9310 100644 --- a/x/crosschain/keeper/cctx_test.go +++ b/x/crosschain/keeper/cctx_test.go @@ -44,7 +44,7 @@ func createNCctxWithStatus( items[i].OutboundParams = []*types.OutboundParams{{Amount: math.ZeroUint(), CallOptions: &types.CallOptions{}}} items[i].RevertOptions = types.NewEmptyRevertOptions() - keeper.SetCctxAndNonceToCctxAndInboundHashToCctx(ctx, items[i], tssPubkey) + keeper.SaveCCTXUpdate(ctx, items[i], tssPubkey) } return items } @@ -88,7 +88,7 @@ func createNCctx(keeper *keeper.Keeper, ctx sdk.Context, n int, tssPubkey string items[i].Index = fmt.Sprintf("%d", i) items[i].RevertOptions = types.NewEmptyRevertOptions() - keeper.SetCctxAndNonceToCctxAndInboundHashToCctx(ctx, items[i], tssPubkey) + keeper.SaveCCTXUpdate(ctx, items[i], tssPubkey) } return items } @@ -453,3 +453,234 @@ func Test_NewCCTX(t *testing.T) { require.Equal(t, types.ProtocolContractVersion_V1, cctx.ProtocolContractVersion) }) } + +func TestKeeper_UpdateNonceToCCTX(t *testing.T) { + t.Run("should set nonce to cctx if status is PendingOutbound", func(t *testing.T) { + // Arrange + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + chainID := chains.Ethereum.ChainId + nonce := uint64(10) + + cctx := types.CrossChainTx{Index: "test", + OutboundParams: []*types.OutboundParams{{ReceiverChainId: chainID, TssNonce: nonce}}, + CctxStatus: &types.Status{Status: types.CctxStatus_PendingOutbound}, + } + tssPubkey := "test-tss-pubkey" + + // Act + k.SetNonceToCCTX(ctx, cctx, tssPubkey) + + // Assert + nonceToCctx, found := k.GetObserverKeeper().GetNonceToCctx(ctx, tssPubkey, chainID, int64(nonce)) + require.True(t, found) + require.Equal(t, cctx.Index, nonceToCctx.CctxIndex) + require.Equal(t, tssPubkey, nonceToCctx.Tss) + require.Equal(t, chainID, nonceToCctx.ChainId) + }) + + t.Run("should set nonce to cctx if status is PendingRevert", func(t *testing.T) { + // Arrange + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + chainID := chains.Ethereum.ChainId + nonce := uint64(10) + + cctx := types.CrossChainTx{Index: "test", + OutboundParams: []*types.OutboundParams{{ReceiverChainId: chainID, TssNonce: nonce}}, + CctxStatus: &types.Status{Status: types.CctxStatus_PendingRevert}, + } + tssPubkey := "test-tss-pubkey" + + // Act + k.SetNonceToCCTX(ctx, cctx, tssPubkey) + + // Assert + nonceToCctx, found := k.GetObserverKeeper().GetNonceToCctx(ctx, tssPubkey, chainID, int64(nonce)) + require.True(t, found) + require.Equal(t, cctx.Index, nonceToCctx.CctxIndex) + require.Equal(t, tssPubkey, nonceToCctx.Tss) + require.Equal(t, chainID, nonceToCctx.ChainId) + }) + + t.Run("should not set nonce to cctx if status is not PendingOutbound or PendingRevert", func(t *testing.T) { + // Arrange + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + chainID := chains.Ethereum.ChainId + nonce := uint64(10) + + cctx := types.CrossChainTx{Index: "test", + OutboundParams: []*types.OutboundParams{{ReceiverChainId: chainID, TssNonce: nonce}}, + CctxStatus: &types.Status{Status: types.CctxStatus_Aborted}, + } + tssPubkey := "test-tss-pubkey" + + // Act + k.SetNonceToCCTX(ctx, cctx, tssPubkey) + + // Assert + _, found := k.GetObserverKeeper().GetNonceToCctx(ctx, tssPubkey, chainID, int64(nonce)) + require.False(t, found) + }) +} + +func TestKeeper_UpdateInboundHashToCCTX(t *testing.T) { + t.Run( + "should update inbound hash to cctx mapping if new cctx index is found for the same inbound hash", + func(t *testing.T) { + // Arrange + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + inboundHash := sample.Hash().String() + index1 := sample.ZetaIndex(t) + index2 := sample.ZetaIndex(t) + + inboundHashToCctx := types.InboundHashToCctx{ + InboundHash: inboundHash, + CctxIndex: []string{index1}, + } + k.SetInboundHashToCctx(ctx, inboundHashToCctx) + cctx := types.CrossChainTx{Index: index2, InboundParams: &types.InboundParams{ObservedHash: inboundHash}} + + // Act + k.UpdateInboundHashToCCTX(ctx, cctx) + + // Assert + inboundHashToCctx, found := k.GetInboundHashToCctx(ctx, inboundHash) + require.True(t, found) + require.Equal(t, inboundHash, inboundHashToCctx.InboundHash) + require.Equal(t, 2, len(inboundHashToCctx.CctxIndex)) + require.Contains(t, inboundHashToCctx.CctxIndex, index1) + require.Contains(t, inboundHashToCctx.CctxIndex, index2) + }, + ) + + t.Run("should do nothing if the cctx index is already in the mapping", func(t *testing.T) { + // Arrange + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + inboundHash := sample.Hash().String() + index := sample.ZetaIndex(t) + + inboundHashToCctx := types.InboundHashToCctx{ + InboundHash: inboundHash, + CctxIndex: []string{index}, + } + k.SetInboundHashToCctx(ctx, inboundHashToCctx) + cctx := types.CrossChainTx{Index: index, InboundParams: &types.InboundParams{ObservedHash: inboundHash}} + + // Act + k.UpdateInboundHashToCCTX(ctx, cctx) + + // Assert + inboundHashToCctx, found := k.GetInboundHashToCctx(ctx, inboundHash) + require.True(t, found) + require.Equal(t, inboundHash, inboundHashToCctx.InboundHash) + require.Equal(t, 1, len(inboundHashToCctx.CctxIndex)) + require.Contains(t, inboundHashToCctx.CctxIndex, index) + }) + + t.Run("should add cctx index to mapping if InboundHashToCctx is not found", func(t *testing.T) { + // Arrange + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + inboundHash := sample.Hash().String() + index := sample.ZetaIndex(t) + + cctx := types.CrossChainTx{Index: index, InboundParams: &types.InboundParams{ObservedHash: inboundHash}} + + // Act + k.UpdateInboundHashToCCTX(ctx, cctx) + + // Assert + inboundHashToCctx, found := k.GetInboundHashToCctx(ctx, inboundHash) + require.True(t, found) + require.Equal(t, inboundHash, inboundHashToCctx.InboundHash) + require.Equal(t, 1, len(inboundHashToCctx.CctxIndex)) + require.Contains(t, inboundHashToCctx.CctxIndex, index) + }) +} + +func TestKeeper_UpdateZetaAccounting(t *testing.T) { + t.Run("should update zeta accounting if cctx is aborted and coin type is zeta", func(t *testing.T) { + // Arrange + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + amount := sdkmath.NewUint(100) + cctx := types.CrossChainTx{ + InboundParams: &types.InboundParams{CoinType: coin.CoinType_Zeta}, + CctxStatus: &types.Status{ + IsAbortRefunded: false, + Status: types.CctxStatus_Aborted}, + OutboundParams: []*types.OutboundParams{{Amount: amount}}, + } + k.SetZetaAccounting(ctx, types.ZetaAccounting{AbortedZetaAmount: math.ZeroUint()}) + + // Act + k.UpdateZetaAccounting(ctx, cctx) + + // Assert + zetaAccounting, found := k.GetZetaAccounting(ctx) + require.True(t, found) + require.Equal(t, amount, zetaAccounting.AbortedZetaAmount) + }) + + t.Run("should not update zeta accounting if cctx is not aborted", func(t *testing.T) { + // Arrange + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + amount := sdkmath.NewUint(100) + cctx := types.CrossChainTx{ + InboundParams: &types.InboundParams{CoinType: coin.CoinType_Zeta}, + CctxStatus: &types.Status{ + IsAbortRefunded: false, + Status: types.CctxStatus_PendingOutbound}, + OutboundParams: []*types.OutboundParams{{Amount: amount}}, + } + k.SetZetaAccounting(ctx, types.ZetaAccounting{AbortedZetaAmount: math.ZeroUint()}) + + // Act + k.UpdateZetaAccounting(ctx, cctx) + + // Assert + zetaAccounting, found := k.GetZetaAccounting(ctx) + require.True(t, found) + require.Equal(t, math.ZeroUint(), zetaAccounting.AbortedZetaAmount) + }) + + t.Run("should update to amount if zeta accounting is not set", func(t *testing.T) { + // Arrange + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + amount := sdkmath.NewUint(100) + cctx := types.CrossChainTx{ + InboundParams: &types.InboundParams{CoinType: coin.CoinType_Zeta}, + CctxStatus: &types.Status{ + IsAbortRefunded: false, + Status: types.CctxStatus_Aborted}, + OutboundParams: []*types.OutboundParams{{Amount: amount}}, + } + + // Act + k.UpdateZetaAccounting(ctx, cctx) + + // Assert + zetaAccounting, found := k.GetZetaAccounting(ctx) + require.True(t, found) + require.Equal(t, amount, zetaAccounting.AbortedZetaAmount) + }) + + t.Run("should not update zeta accounting if the cctx is already refunded", func(t *testing.T) { + // Arrange + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + amount := sdkmath.NewUint(100) + cctx := types.CrossChainTx{ + InboundParams: &types.InboundParams{CoinType: coin.CoinType_Zeta}, + CctxStatus: &types.Status{ + IsAbortRefunded: true, + Status: types.CctxStatus_Aborted}, + OutboundParams: []*types.OutboundParams{{Amount: amount}}, + } + k.SetZetaAccounting(ctx, types.ZetaAccounting{AbortedZetaAmount: math.ZeroUint()}) + + // Act + k.UpdateZetaAccounting(ctx, cctx) + + // Assert + zetaAccounting, found := k.GetZetaAccounting(ctx) + require.True(t, found) + require.Equal(t, math.ZeroUint(), zetaAccounting.AbortedZetaAmount) + }) +} diff --git a/x/crosschain/keeper/export_private_functions_test.go b/x/crosschain/keeper/export_private_functions_test.go new file mode 100644 index 0000000000..0611ed995d --- /dev/null +++ b/x/crosschain/keeper/export_private_functions_test.go @@ -0,0 +1,20 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// These functions are exported for testing purposes + +func (k Keeper) UpdateZetaAccounting(ctx sdk.Context, cctx types.CrossChainTx) { + k.updateZetaAccounting(ctx, cctx) +} + +func (k Keeper) UpdateInboundHashToCCTX(ctx sdk.Context, cctx types.CrossChainTx) { + k.updateInboundHashToCCTX(ctx, cctx) +} + +func (k Keeper) SetNonceToCCTX(ctx sdk.Context, cctx types.CrossChainTx, tssPubkey string) { + k.setNonceToCCTX(ctx, cctx, tssPubkey) +} diff --git a/x/crosschain/keeper/grpc_query_inbound_hash_to_cctx_test.go b/x/crosschain/keeper/grpc_query_inbound_hash_to_cctx_test.go index 296b46a63b..87e81162f6 100644 --- a/x/crosschain/keeper/grpc_query_inbound_hash_to_cctx_test.go +++ b/x/crosschain/keeper/grpc_query_inbound_hash_to_cctx_test.go @@ -140,7 +140,7 @@ func createInTxHashToCctxWithCctxs( cctxs[i].InboundParams = &types.InboundParams{ObservedHash: fmt.Sprintf("%d", i), Amount: math.OneUint()} cctxs[i].CctxStatus = &types.Status{Status: types.CctxStatus_PendingInbound} cctxs[i].RevertOptions = types.NewEmptyRevertOptions() - keeper.SetCctxAndNonceToCctxAndInboundHashToCctx(ctx, cctxs[i], tssPubkey) + keeper.SaveCCTXUpdate(ctx, cctxs[i], tssPubkey) } var inboundHashToCctx types.InboundHashToCctx diff --git a/x/crosschain/keeper/msg_server_abort_stuck_cctx.go b/x/crosschain/keeper/msg_server_abort_stuck_cctx.go index 6ba922e804..7e2505b01e 100644 --- a/x/crosschain/keeper/msg_server_abort_stuck_cctx.go +++ b/x/crosschain/keeper/msg_server_abort_stuck_cctx.go @@ -36,19 +36,16 @@ func (k msgServer) AbortStuckCCTX( } // check if the cctx is pending - isPending := cctx.CctxStatus.Status == types.CctxStatus_PendingOutbound || - cctx.CctxStatus.Status == types.CctxStatus_PendingInbound || - cctx.CctxStatus.Status == types.CctxStatus_PendingRevert - if !isPending { + if !cctx.CctxStatus.Status.IsPending() { return nil, types.ErrStatusNotPending } - cctx.CctxStatus = &types.Status{ - Status: types.CctxStatus_Aborted, - StatusMessage: AbortMessage, - } + // update the status + cctx.CctxStatus.UpdateStatusAndErrorMessages(types.CctxStatus_Aborted, AbortMessage, "") - k.SetCrossChainTx(ctx, cctx) + // Save out outbound, + // We do not need to provide the tss-pubkey as NonceToCctx is not updated / New outbound is not added + k.SaveOutbound(ctx, &cctx, "") return &types.MsgAbortStuckCCTXResponse{}, nil } diff --git a/x/crosschain/keeper/msg_server_abort_stuck_cctx_test.go b/x/crosschain/keeper/msg_server_abort_stuck_cctx_test.go index a43d034b34..5b9b29cb8e 100644 --- a/x/crosschain/keeper/msg_server_abort_stuck_cctx_test.go +++ b/x/crosschain/keeper/msg_server_abort_stuck_cctx_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/require" + observertypes "github.com/zeta-chain/node/x/observer/types" keepertest "github.com/zeta-chain/node/testutil/keeper" "github.com/zeta-chain/node/testutil/sample" @@ -14,6 +15,7 @@ import ( func TestMsgServer_AbortStuckCCTX(t *testing.T) { t.Run("can abort a cctx in pending inbound", func(t *testing.T) { + // Arrange k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ UseAuthorityMock: true, }) @@ -28,7 +30,15 @@ func TestMsgServer_AbortStuckCCTX(t *testing.T) { Status: crosschaintypes.CctxStatus_PendingInbound, StatusMessage: "pending inbound", } + cctx.GetCurrentOutboundParam().TssNonce = 1 + k.SetCrossChainTx(ctx, *cctx) + k.GetObserverKeeper().SetPendingNonces(ctx, observertypes.PendingNonces{ + NonceLow: 0, + NonceHigh: 10, + ChainId: cctx.GetCurrentOutboundParam().ReceiverChainId, + Tss: cctx.GetCurrentOutboundParam().TssPubkey, + }) // abort the cctx msg := crosschaintypes.MsgAbortStuckCCTX{ @@ -36,16 +46,23 @@ func TestMsgServer_AbortStuckCCTX(t *testing.T) { CctxIndex: sample.GetCctxIndexFromString("cctx_index"), } keepertest.MockCheckAuthorization(&authorityMock.Mock, &msg, nil) + // Act _, err := msgServer.AbortStuckCCTX(ctx, &msg) + // Assert require.NoError(t, err) cctxFound, found := k.GetCrossChainTx(ctx, sample.GetCctxIndexFromString("cctx_index")) require.True(t, found) require.Equal(t, crosschaintypes.CctxStatus_Aborted, cctxFound.CctxStatus.Status) - require.Equal(t, crosschainkeeper.AbortMessage, cctxFound.CctxStatus.StatusMessage) + require.Contains(t, cctxFound.CctxStatus.StatusMessage, crosschainkeeper.AbortMessage) + pendingNonces, found := k.GetObserverKeeper(). + GetPendingNonces(ctx, cctx.GetCurrentOutboundParam().TssPubkey, cctx.GetCurrentOutboundParam().ReceiverChainId) + require.True(t, found) + require.Equal(t, pendingNonces.NonceLow, int64(cctx.GetCurrentOutboundParam().TssNonce+1)) }) t.Run("can abort a cctx in pending outbound", func(t *testing.T) { + // Arrange k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ UseAuthorityMock: true, }) @@ -59,26 +76,42 @@ func TestMsgServer_AbortStuckCCTX(t *testing.T) { Status: crosschaintypes.CctxStatus_PendingOutbound, StatusMessage: "pending outbound", } + cctx.GetCurrentOutboundParam().TssNonce = 1 + k.SetCrossChainTx(ctx, *cctx) + k.GetObserverKeeper().SetPendingNonces(ctx, observertypes.PendingNonces{ + NonceLow: 0, + NonceHigh: 10, + ChainId: cctx.GetCurrentOutboundParam().ReceiverChainId, + Tss: cctx.GetCurrentOutboundParam().TssPubkey, + }) - // abort the cctx msg := crosschaintypes.MsgAbortStuckCCTX{ Creator: admin, CctxIndex: sample.GetCctxIndexFromString("cctx_index"), } keepertest.MockCheckAuthorization(&authorityMock.Mock, &msg, nil) + + // Act _, err := msgServer.AbortStuckCCTX(ctx, &msg) + // Assert require.NoError(t, err) cctxFound, found := k.GetCrossChainTx(ctx, sample.GetCctxIndexFromString("cctx_index")) require.True(t, found) require.Equal(t, crosschaintypes.CctxStatus_Aborted, cctxFound.CctxStatus.Status) - require.Equal(t, crosschainkeeper.AbortMessage, cctxFound.CctxStatus.StatusMessage) + require.Contains(t, cctxFound.CctxStatus.StatusMessage, crosschainkeeper.AbortMessage) // ensure the last update timestamp is updated require.Equal(t, cctxFound.CctxStatus.LastUpdateTimestamp, ctx.BlockTime().Unix()) + pendingNonces, found := k.GetObserverKeeper(). + GetPendingNonces(ctx, cctx.GetCurrentOutboundParam().TssPubkey, cctx.GetCurrentOutboundParam().ReceiverChainId) + require.True(t, found) + require.Equal(t, pendingNonces.NonceLow, int64(cctx.GetCurrentOutboundParam().TssNonce+1)) + }) t.Run("can abort a cctx in pending revert", func(t *testing.T) { + // Arrange k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ UseAuthorityMock: true, }) @@ -93,7 +126,15 @@ func TestMsgServer_AbortStuckCCTX(t *testing.T) { Status: crosschaintypes.CctxStatus_PendingRevert, StatusMessage: "pending revert", } + cctx.GetCurrentOutboundParam().TssNonce = 1 + k.SetCrossChainTx(ctx, *cctx) + k.GetObserverKeeper().SetPendingNonces(ctx, observertypes.PendingNonces{ + NonceLow: 0, + NonceHigh: 10, + ChainId: cctx.GetCurrentOutboundParam().ReceiverChainId, + Tss: cctx.GetCurrentOutboundParam().TssPubkey, + }) // abort the cctx msg := crosschaintypes.MsgAbortStuckCCTX{ @@ -101,13 +142,19 @@ func TestMsgServer_AbortStuckCCTX(t *testing.T) { CctxIndex: sample.GetCctxIndexFromString("cctx_index"), } keepertest.MockCheckAuthorization(&authorityMock.Mock, &msg, nil) + // Act _, err := msgServer.AbortStuckCCTX(ctx, &msg) + // Assert require.NoError(t, err) cctxFound, found := k.GetCrossChainTx(ctx, sample.GetCctxIndexFromString("cctx_index")) require.True(t, found) require.Equal(t, crosschaintypes.CctxStatus_Aborted, cctxFound.CctxStatus.Status) - require.Equal(t, crosschainkeeper.AbortMessage, cctxFound.CctxStatus.StatusMessage) + require.Contains(t, cctxFound.CctxStatus.StatusMessage, crosschainkeeper.AbortMessage) + pendingNonces, found := k.GetObserverKeeper(). + GetPendingNonces(ctx, cctx.GetCurrentOutboundParam().TssPubkey, cctx.GetCurrentOutboundParam().ReceiverChainId) + require.True(t, found) + require.Equal(t, pendingNonces.NonceLow, int64(cctx.GetCurrentOutboundParam().TssNonce+1)) }) t.Run("cannot abort a cctx in pending outbound if not admin", func(t *testing.T) { diff --git a/x/crosschain/keeper/msg_server_migrate_erc20_custody_funds.go b/x/crosschain/keeper/msg_server_migrate_erc20_custody_funds.go index c94ccc089a..56fa655ddd 100644 --- a/x/crosschain/keeper/msg_server_migrate_erc20_custody_funds.go +++ b/x/crosschain/keeper/msg_server_migrate_erc20_custody_funds.go @@ -83,7 +83,7 @@ func (k msgServer) MigrateERC20CustodyFunds( if err != nil { return nil, err } - k.SetCctxAndNonceToCctxAndInboundHashToCctx(ctx, cctx, tss.TssPubkey) + k.SaveCCTXUpdate(ctx, cctx, tss.TssPubkey) err = ctx.EventManager().EmitTypedEvent( &types.EventERC20CustodyFundsMigration{ diff --git a/x/crosschain/keeper/msg_server_migrate_tss_funds.go b/x/crosschain/keeper/msg_server_migrate_tss_funds.go index 8ecc4c47ff..9ca7f2e7ab 100644 --- a/x/crosschain/keeper/msg_server_migrate_tss_funds.go +++ b/x/crosschain/keeper/msg_server_migrate_tss_funds.go @@ -133,7 +133,7 @@ func (k Keeper) initiateMigrateTSSFundsCCTX( } } - k.SetCctxAndNonceToCctxAndInboundHashToCctx(ctx, cctx, currentTss.TssPubkey) + k.SaveCCTXUpdate(ctx, cctx, currentTss.TssPubkey) k.zetaObserverKeeper.SetFundMigrator(ctx, observertypes.TssFundMigratorInfo{ ChainId: chainID, MigrationCctxIndex: cctx.Index, diff --git a/x/crosschain/keeper/msg_server_update_erc20_custody_pause_status.go b/x/crosschain/keeper/msg_server_update_erc20_custody_pause_status.go index ebd1b9cf1b..0d58f60711 100644 --- a/x/crosschain/keeper/msg_server_update_erc20_custody_pause_status.go +++ b/x/crosschain/keeper/msg_server_update_erc20_custody_pause_status.go @@ -81,7 +81,7 @@ func (k msgServer) UpdateERC20CustodyPauseStatus( if err != nil { return nil, err } - k.SetCctxAndNonceToCctxAndInboundHashToCctx(ctx, cctx, tss.TssPubkey) + k.SaveCCTXUpdate(ctx, cctx, tss.TssPubkey) err = ctx.EventManager().EmitTypedEvent( &types.EventERC20CustodyPausing{ diff --git a/x/crosschain/keeper/msg_server_vote_outbound_tx.go b/x/crosschain/keeper/msg_server_vote_outbound_tx.go index 4bb65a3c8f..a940c9c7db 100644 --- a/x/crosschain/keeper/msg_server_vote_outbound_tx.go +++ b/x/crosschain/keeper/msg_server_vote_outbound_tx.go @@ -100,6 +100,14 @@ func (k msgServer) VoteOutbound( return &types.MsgVoteOutboundResponse{}, nil } + // If the CCTX is in a terminal state, we do not need to process it. + if cctx.CctxStatus.Status.IsTerminal() { + return &types.MsgVoteOutboundResponse{}, cosmoserrors.Wrap( + types.ErrCCTXAlreadyFinalized, + fmt.Sprintf("CCTX status %s", cctx.CctxStatus.Status), + ) + } + // Set the finalized ballot to the current outbound params. cctx.SetOutboundBallotIndex(ballotIndex) @@ -216,7 +224,7 @@ func (k Keeper) SaveOutbound(ctx sdk.Context, cctx *types.CrossChainTx, tssPubke k.RemoveOutboundTrackerFromStore(ctx, outboundParams.ReceiverChainId, outboundParams.TssNonce) } // This should set nonce to cctx only if a new revert is created. - k.SetCctxAndNonceToCctxAndInboundHashToCctx(ctx, *cctx, tssPubkey) + k.SaveCCTXUpdate(ctx, *cctx, tssPubkey) } func (k Keeper) ValidateOutboundMessage(ctx sdk.Context, msg types.MsgVoteOutbound) (types.CrossChainTx, error) { diff --git a/x/crosschain/keeper/msg_server_vote_outbound_tx_test.go b/x/crosschain/keeper/msg_server_vote_outbound_tx_test.go index cadc6eeac8..5c1415d2df 100644 --- a/x/crosschain/keeper/msg_server_vote_outbound_tx_test.go +++ b/x/crosschain/keeper/msg_server_vote_outbound_tx_test.go @@ -173,6 +173,88 @@ func TestKeeper_VoteOutbound(t *testing.T) { require.Len(t, c.OutboundParams, expectedNumberOfOutboundParams) }) + t.Run("unable to finalize an outbound if the cctx has already been aborted ", func(t *testing.T) { + k, ctx, _, zk := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseObserverMock: true, + }) + + // Setup mock data + observerMock := keepertest.GetCrosschainObserverMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain() + asset := "" + observer := sample.AccAddress() + tss := sample.Tss() + zk.ObserverKeeper.SetObserverSet(ctx, observertypes.ObserverSet{ObserverList: []string{observer}}) + cctx := GetERC20Cctx(t, receiver, senderChain, asset, amount) + cctx.GetCurrentOutboundParam().TssPubkey = tss.TssPubkey + cctx.CctxStatus.Status = types.CctxStatus_Aborted + k.SetCrossChainTx(ctx, *cctx) + observerMock.On("GetTSS", ctx).Return(observertypes.TSS{}, true).Once() + + // Successfully mock VoteOnOutboundBallot + keepertest.MockVoteOnOutboundSuccessBallot(observerMock, ctx, cctx, senderChain, observer) + + msgServer := keeper.NewMsgServerImpl(*k) + msg := types.MsgVoteOutbound{ + CctxHash: cctx.Index, + OutboundTssNonce: cctx.GetCurrentOutboundParam().TssNonce, + OutboundChain: cctx.GetCurrentOutboundParam().ReceiverChainId, + Status: chains.ReceiveStatus_success, + Creator: observer, + ObservedOutboundHash: sample.Hash().String(), + ValueReceived: cctx.GetCurrentOutboundParam().Amount, + ObservedOutboundBlockHeight: 10, + ObservedOutboundEffectiveGasPrice: math.NewInt(21), + ObservedOutboundGasUsed: 21, + CoinType: cctx.InboundParams.CoinType, + } + _, err := msgServer.VoteOutbound(ctx, &msg) + require.ErrorIs(t, err, types.ErrCCTXAlreadyFinalized) + }) + + t.Run("unable to finalize an outbound if the cctx has already been outboundmined", func(t *testing.T) { + k, ctx, _, zk := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseObserverMock: true, + }) + + // Setup mock data + observerMock := keepertest.GetCrosschainObserverMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain() + asset := "" + observer := sample.AccAddress() + tss := sample.Tss() + zk.ObserverKeeper.SetObserverSet(ctx, observertypes.ObserverSet{ObserverList: []string{observer}}) + cctx := GetERC20Cctx(t, receiver, senderChain, asset, amount) + cctx.GetCurrentOutboundParam().TssPubkey = tss.TssPubkey + cctx.CctxStatus.Status = types.CctxStatus_OutboundMined + k.SetCrossChainTx(ctx, *cctx) + observerMock.On("GetTSS", ctx).Return(observertypes.TSS{}, true).Once() + + // Successfully mock VoteOnOutboundBallot + keepertest.MockVoteOnOutboundSuccessBallot(observerMock, ctx, cctx, senderChain, observer) + + msgServer := keeper.NewMsgServerImpl(*k) + msg := types.MsgVoteOutbound{ + CctxHash: cctx.Index, + OutboundTssNonce: cctx.GetCurrentOutboundParam().TssNonce, + OutboundChain: cctx.GetCurrentOutboundParam().ReceiverChainId, + Status: chains.ReceiveStatus_success, + Creator: observer, + ObservedOutboundHash: sample.Hash().String(), + ValueReceived: cctx.GetCurrentOutboundParam().Amount, + ObservedOutboundBlockHeight: 10, + ObservedOutboundEffectiveGasPrice: math.NewInt(21), + ObservedOutboundGasUsed: 21, + CoinType: cctx.InboundParams.CoinType, + } + _, err := msgServer.VoteOutbound(ctx, &msg) + require.ErrorIs(t, err, types.ErrCCTXAlreadyFinalized) + }) + t.Run("vote on outbound tx fails if tss is not found", func(t *testing.T) { k, ctx, _, zk := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ UseObserverMock: true, diff --git a/x/crosschain/keeper/msg_server_whitelist_erc20.go b/x/crosschain/keeper/msg_server_whitelist_erc20.go index 197310e16c..fff9feb92a 100644 --- a/x/crosschain/keeper/msg_server_whitelist_erc20.go +++ b/x/crosschain/keeper/msg_server_whitelist_erc20.go @@ -174,7 +174,7 @@ func (k msgServer) WhitelistERC20( GasLimit: uint64(msg.GasLimit), } k.fungibleKeeper.SetForeignCoins(ctx, foreignCoin) - k.SetCctxAndNonceToCctxAndInboundHashToCctx(ctx, cctx, tss.TssPubkey) + k.SaveCCTXUpdate(ctx, cctx, tss.TssPubkey) commit() diff --git a/x/crosschain/keeper/outbound_tracker_test.go b/x/crosschain/keeper/outbound_tracker_test.go index 7eddcc2cfe..ad535739d1 100644 --- a/x/crosschain/keeper/outbound_tracker_test.go +++ b/x/crosschain/keeper/outbound_tracker_test.go @@ -42,19 +42,29 @@ func TestOutboundTrackerGet(t *testing.T) { } } func TestOutboundTrackerRemove(t *testing.T) { - k, ctx, _, _ := keepertest.CrosschainKeeper(t) - items := createNOutboundTracker(k, ctx, 10) - for _, item := range items { - k.RemoveOutboundTrackerFromStore(ctx, - item.ChainId, - item.Nonce, - ) - _, found := k.GetOutboundTracker(ctx, - item.ChainId, - item.Nonce, - ) - require.False(t, found) - } + t.Run("Remove tracker if it exists", func(t *testing.T) { + keeper, ctx, _, _ := keepertest.CrosschainKeeper(t) + items := createNOutboundTracker(keeper, ctx, 10) + for _, item := range items { + keeper.RemoveOutboundTrackerFromStore(ctx, + item.ChainId, + item.Nonce, + ) + _, found := keeper.GetOutboundTracker(ctx, + item.ChainId, + item.Nonce, + ) + require.False(t, found) + } + }) + + t.Run("Do nothing if tracker doesn't exist", func(t *testing.T) { + keeper, ctx, _, _ := keepertest.CrosschainKeeper(t) + require.NotPanics(t, func() { + keeper.RemoveOutboundTrackerFromStore(ctx, 1, 1) + }) + }) + } func TestOutboundTrackerGetAll(t *testing.T) { diff --git a/x/crosschain/types/errors.go b/x/crosschain/types/errors.go index 40182fe313..ca217bd537 100644 --- a/x/crosschain/types/errors.go +++ b/x/crosschain/types/errors.go @@ -58,4 +58,5 @@ var ( ErrValidatingInbound = errorsmod.Register(ModuleName, 1157, "unable to validate inbound") ErrInvalidGasLimit = errorsmod.Register(ModuleName, 1158, "invalid gas limit") ErrUnableToSetOutboundInfo = errorsmod.Register(ModuleName, 1159, "unable to set outbound info") + ErrCCTXAlreadyFinalized = errorsmod.Register(ModuleName, 1160, "cctx already finalized") ) diff --git a/x/crosschain/types/status.go b/x/crosschain/types/status.go index d67ec08ac6..5ebdf5391c 100644 --- a/x/crosschain/types/status.go +++ b/x/crosschain/types/status.go @@ -86,3 +86,21 @@ func stateTransitionMap() map[CctxStatus][]CctxStatus { } return stateTransitionMap } + +// IsTerminal returns true if the status is terminal. +// The terminal states are +// CctxStatus_Aborted +// CctxStatus_Reverted +// CctxStatus_OutboundMined +func (c CctxStatus) IsTerminal() bool { + return c == CctxStatus_Aborted || c == CctxStatus_Reverted || c == CctxStatus_OutboundMined +} + +// IsPending returns true if the status is pending. +// The pending states are +// CctxStatus_PendingInbound +// CctxStatus_PendingOutbound +// CctxStatus_PendingRevert +func (c CctxStatus) IsPending() bool { + return !c.IsTerminal() +} diff --git a/x/crosschain/types/status_test.go b/x/crosschain/types/status_test.go index 5bb272d522..f808f6e296 100644 --- a/x/crosschain/types/status_test.go +++ b/x/crosschain/types/status_test.go @@ -174,3 +174,45 @@ func TestStatus_ChangeStatus(t *testing.T) { ) }) } + +func TestCctxStatus_IsTerminalStatus(t *testing.T) { + tests := []struct { + name string + status types.CctxStatus + expected bool + }{ + {"PendingInbound", types.CctxStatus_PendingInbound, false}, + {"PendingOutbound", types.CctxStatus_PendingOutbound, false}, + {"OutboundMined", types.CctxStatus_OutboundMined, true}, + {"Reverted", types.CctxStatus_Reverted, true}, + {"Aborted", types.CctxStatus_Aborted, true}, + {"PendingRevert", types.CctxStatus_PendingRevert, false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, tc.status.IsTerminal()) + }) + } +} + +func TestCctxStatus_IsPendingStatus(t *testing.T) { + tests := []struct { + name string + status types.CctxStatus + expected bool + }{ + {"PendingInbound", types.CctxStatus_PendingInbound, true}, + {"PendingOutbound", types.CctxStatus_PendingOutbound, true}, + {"OutboundMined", types.CctxStatus_OutboundMined, false}, + {"Reverted", types.CctxStatus_Reverted, false}, + {"Aborted", types.CctxStatus_Aborted, false}, + {"PendingRevert", types.CctxStatus_PendingRevert, true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, tc.status.IsPending()) + }) + } +} diff --git a/zetaclient/chains/solana/observer/inbound.go b/zetaclient/chains/solana/observer/inbound.go index 2b30b5818d..6eab1242ac 100644 --- a/zetaclient/chains/solana/observer/inbound.go +++ b/zetaclient/chains/solana/observer/inbound.go @@ -212,7 +212,7 @@ func (ob *Observer) FilterInboundEvents(txResult *rpc.GetTransactionResult) ([]* events = append(events, &clienttypes.InboundEvent{ SenderChainID: ob.Chain().ChainId, Sender: deposit.Sender, - Receiver: deposit.Sender, // receiver is pulled out from memo + Receiver: "", // receiver will be pulled out from memo later TxOrigin: deposit.Sender, Amount: deposit.Amount, Memo: deposit.Memo, @@ -240,7 +240,7 @@ func (ob *Observer) FilterInboundEvents(txResult *rpc.GetTransactionResult) ([]* events = append(events, &clienttypes.InboundEvent{ SenderChainID: ob.Chain().ChainId, Sender: deposit.Sender, - Receiver: deposit.Sender, // receiver is pulled out from memo + Receiver: "", // receiver will be pulled out from memo later TxOrigin: deposit.Sender, Amount: deposit.Amount, Memo: deposit.Memo, @@ -288,7 +288,7 @@ func (ob *Observer) BuildInboundVoteMsgFromEvent(event *clienttypes.InboundEvent event.Sender, event.SenderChainID, event.Sender, - event.Sender, + event.Receiver, ob.ZetacoreClient().Chain().ChainId, cosmosmath.NewUint(event.Amount), hex.EncodeToString(event.Memo), diff --git a/zetaclient/chains/solana/observer/inbound_test.go b/zetaclient/chains/solana/observer/inbound_test.go index 0b118ae55e..ff0ad101a2 100644 --- a/zetaclient/chains/solana/observer/inbound_test.go +++ b/zetaclient/chains/solana/observer/inbound_test.go @@ -81,7 +81,7 @@ func Test_FilterInboundEvents(t *testing.T) { eventExpected := &clienttypes.InboundEvent{ SenderChainID: chain.ChainId, Sender: sender, - Receiver: sender, + Receiver: "", TxOrigin: sender, Amount: 100000, Memo: []byte("0x7F8ae2ABb69A558CE6bAd546f25F0464D9e09e5B4955a3F38ff86ae92A914445099caa8eA2B9bA32"), @@ -123,11 +123,13 @@ func Test_BuildInboundVoteMsgFromEvent(t *testing.T) { t.Run("should return vote msg for valid event", func(t *testing.T) { sender := sample.SolanaAddress(t) - memo := sample.EthAddress().Bytes() - event := sample.InboundEvent(chain.ChainId, sender, sender, 1280, []byte(memo)) + receiver := sample.EthAddress() + event := sample.InboundEvent(chain.ChainId, sender, "", 1280, receiver.Bytes()) msg := ob.BuildInboundVoteMsgFromEvent(event) require.NotNil(t, msg) + require.Equal(t, sender, msg.Sender) + require.Equal(t, receiver.Hex(), msg.Receiver) }) t.Run("should return nil if failed to decode memo", func(t *testing.T) {