diff --git a/changelog.md b/changelog.md index 7ae80ba98c..ff001f7389 100644 --- a/changelog.md +++ b/changelog.md @@ -1,7 +1,6 @@ # CHANGELOG ## Unreleased - ### Breaking Changes * Admin policies have been moved from `observer` to a new module `authority`. @@ -22,6 +21,8 @@ * [1914](https://github.com/zeta-chain/node/pull/1914) - move crosschain flags to core context in zetaclient * [1948](https://github.com/zeta-chain/node/pull/1948) - remove deprecated GetTSSAddress query in crosschain module * [1936](https://github.com/zeta-chain/node/pull/1936) - refactor common package into subpackages and rename to pkg +* [1853](https://github.com/zeta-chain/node/pull/1853) - refactor vote inbound tx and vote outbound tx + ### Features diff --git a/proto/crosschain/cross_chain_tx.proto b/proto/crosschain/cross_chain_tx.proto index 4ed336ed8f..45ad6a7181 100644 --- a/proto/crosschain/cross_chain_tx.proto +++ b/proto/crosschain/cross_chain_tx.proto @@ -4,6 +4,8 @@ package zetachain.zetacore.crosschain; import "gogoproto/gogo.proto"; import "pkg/coin/coin.proto"; +//TODO : fix the descriptor numbers for the fields +// https://github.com/zeta-chain/node/issues/1951 option go_package = "github.com/zeta-chain/zetacore/x/crosschain/types"; enum CctxStatus { diff --git a/testutil/keeper/authority.go b/testutil/keeper/authority.go index 2c599e66a3..534f8b8dee 100644 --- a/testutil/keeper/authority.go +++ b/testutil/keeper/authority.go @@ -77,3 +77,14 @@ func AuthorityKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { func MockIsAuthorized(m *mock.Mock, address string, policyType types.PolicyType, isAuthorized bool) { m.On("IsAuthorized", mock.Anything, address, policyType).Return(isAuthorized).Once() } + +func SetAdminPolices(ctx sdk.Context, ak *keeper.Keeper) string { + admin := sample.AccAddress() + ak.SetPolicies(ctx, types.Policies{Items: []*types.Policy{ + { + Address: admin, + PolicyType: types.PolicyType_groupAdmin, + }, + }}) + return admin +} diff --git a/testutil/keeper/crosschain.go b/testutil/keeper/crosschain.go index 715383ee92..04bbcf7ac9 100644 --- a/testutil/keeper/crosschain.go +++ b/testutil/keeper/crosschain.go @@ -1,16 +1,25 @@ package keeper import ( + "math/big" "testing" "github.com/cosmos/cosmos-sdk/store" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" + ethcommon "github.com/ethereum/go-ethereum/common" + evmtypes "github.com/evmos/ethermint/x/evm/types" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" tmdb "github.com/tendermint/tm-db" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/pkg/coin" crosschainmocks "github.com/zeta-chain/zetacore/testutil/keeper/mocks/crosschain" + "github.com/zeta-chain/zetacore/testutil/sample" "github.com/zeta-chain/zetacore/x/crosschain/keeper" "github.com/zeta-chain/zetacore/x/crosschain/types" + fungibletypes "github.com/zeta-chain/zetacore/x/fungible/types" + observertypes "github.com/zeta-chain/zetacore/x/observer/types" ) type CrosschainMockOptions struct { @@ -183,3 +192,110 @@ func GetCrosschainFungibleMock(t testing.TB, keeper *keeper.Keeper) *crosschainm require.True(t, ok) return cfk } + +func MockGetSupportedChainFromChainID(m *crosschainmocks.CrosschainObserverKeeper, senderChain *chains.Chain) { + m.On("GetSupportedChainFromChainID", mock.Anything, senderChain.ChainId). + Return(senderChain).Once() + +} +func MockGetRevertGasLimitForERC20(m *crosschainmocks.CrosschainFungibleKeeper, asset string, senderChain chains.Chain) { + m.On("GetForeignCoinFromAsset", mock.Anything, asset, senderChain.ChainId). + Return(fungibletypes.ForeignCoins{ + Zrc20ContractAddress: sample.EthAddress().String(), + }, true).Once() + m.On("QueryGasLimit", mock.Anything, mock.Anything). + Return(big.NewInt(100), nil).Once() + +} +func MockPayGasAndUpdateCCTX(m *crosschainmocks.CrosschainFungibleKeeper, m2 *crosschainmocks.CrosschainObserverKeeper, ctx sdk.Context, k keeper.Keeper, senderChain chains.Chain, asset string) { + m2.On("GetSupportedChainFromChainID", mock.Anything, senderChain.ChainId). + Return(&senderChain).Twice() + m.On("GetForeignCoinFromAsset", mock.Anything, asset, senderChain.ChainId). + Return(fungibletypes.ForeignCoins{ + Zrc20ContractAddress: sample.EthAddress().String(), + }, true).Once() + m.On("QuerySystemContractGasCoinZRC20", mock.Anything, mock.Anything). + Return(ethcommon.Address{}, nil).Once() + m.On("QueryGasLimit", mock.Anything, mock.Anything). + Return(big.NewInt(100), nil).Once() + m.On("QueryProtocolFlatFee", mock.Anything, mock.Anything). + Return(big.NewInt(1), nil).Once() + k.SetGasPrice(ctx, types.GasPrice{ + ChainId: senderChain.ChainId, + MedianIndex: 0, + Prices: []uint64{1}, + }) + + m.On("QueryUniswapV2RouterGetZRC4ToZRC4AmountsIn", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(big.NewInt(0), nil).Once() + m.On("DepositZRC20", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(&evmtypes.MsgEthereumTxResponse{}, nil) + m.On("GetUniswapV2Router02Address", mock.Anything). + Return(ethcommon.Address{}, nil).Once() + m.On("CallZRC20Approve", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil).Once() + m.On("CallUniswapV2RouterSwapExactTokensForTokens", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return([]*big.Int{big.NewInt(0), big.NewInt(1), big.NewInt(1000)}, nil).Once() + m.On("CallZRC20Burn", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil).Once() + +} + +func MockUpdateNonce(m *crosschainmocks.CrosschainObserverKeeper, senderChain chains.Chain) (nonce uint64) { + nonce = uint64(1) + tss := sample.Tss() + m.On("GetSupportedChainFromChainID", mock.Anything, senderChain.ChainId). + Return(senderChain) + m.On("GetChainNonces", mock.Anything, senderChain.ChainName.String()). + Return(observertypes.ChainNonces{Nonce: nonce}, true) + m.On("GetTSS", mock.Anything). + Return(tss, true) + m.On("GetPendingNonces", mock.Anything, tss.TssPubkey, mock.Anything). + Return(observertypes.PendingNonces{NonceHigh: int64(nonce)}, true) + m.On("SetChainNonces", mock.Anything, mock.Anything) + m.On("SetPendingNonces", mock.Anything, mock.Anything) + return +} + +func MockRevertForHandleEVMDeposit(m *crosschainmocks.CrosschainFungibleKeeper, receiver ethcommon.Address, amount *big.Int, senderChainID int64, errDeposit error) { + m.On( + "ZRC20DepositAndCallContract", + mock.Anything, + mock.Anything, + receiver, + amount, + senderChainID, + mock.Anything, + coin.CoinType_ERC20, + mock.Anything, + ).Return(&evmtypes.MsgEthereumTxResponse{VmError: "reverted"}, false, errDeposit) +} + +func MockVoteOnOutboundSuccessBallot(m *crosschainmocks.CrosschainObserverKeeper, ctx sdk.Context, cctx *types.CrossChainTx, senderChain chains.Chain, observer string) { + m.On("VoteOnOutboundBallot", ctx, mock.Anything, cctx.GetCurrentOutTxParam().ReceiverChainId, chains.ReceiveStatus_Success, observer). + Return(true, true, observertypes.Ballot{BallotStatus: observertypes.BallotStatus_BallotFinalized_SuccessObservation}, senderChain.ChainName.String(), nil).Once() +} + +func MockVoteOnOutboundFailedBallot(m *crosschainmocks.CrosschainObserverKeeper, ctx sdk.Context, cctx *types.CrossChainTx, senderChain chains.Chain, observer string) { + m.On("VoteOnOutboundBallot", ctx, mock.Anything, cctx.GetCurrentOutTxParam().ReceiverChainId, chains.ReceiveStatus_Failed, observer). + Return(true, true, observertypes.Ballot{BallotStatus: observertypes.BallotStatus_BallotFinalized_FailureObservation}, senderChain.ChainName.String(), nil).Once() +} + +func MockGetOutBound(m *crosschainmocks.CrosschainObserverKeeper, ctx sdk.Context) { + m.On("GetTSS", ctx).Return(observertypes.TSS{}, true).Once() +} + +func MockSaveOutBound(m *crosschainmocks.CrosschainObserverKeeper, ctx sdk.Context, cctx *types.CrossChainTx, tss observertypes.TSS) { + m.On("RemoveFromPendingNonces", + ctx, tss.TssPubkey, cctx.GetCurrentOutTxParam().ReceiverChainId, mock.Anything). + Return().Once() + m.On("GetTSS", ctx).Return(observertypes.TSS{}, true) +} + +func MockSaveOutBoundNewRevertCreated(m *crosschainmocks.CrosschainObserverKeeper, ctx sdk.Context, cctx *types.CrossChainTx, tss observertypes.TSS) { + m.On("RemoveFromPendingNonces", + ctx, tss.TssPubkey, cctx.GetCurrentOutTxParam().ReceiverChainId, mock.Anything). + Return().Once() + m.On("GetTSS", ctx).Return(observertypes.TSS{}, true) + m.On("SetNonceToCctx", mock.Anything, mock.Anything).Return().Once() +} diff --git a/testutil/sample/crosschain.go b/testutil/sample/crosschain.go index 3fa5d04983..0fe91e0c2e 100644 --- a/testutil/sample/crosschain.go +++ b/testutil/sample/crosschain.go @@ -9,6 +9,7 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/x/crosschain/types" ) @@ -52,6 +53,20 @@ func InboundTxParams(r *rand.Rand) *types.InboundTxParams { } } +func InboundTxParamsValidChainID(r *rand.Rand) *types.InboundTxParams { + return &types.InboundTxParams{ + Sender: EthAddress().String(), + SenderChainId: chains.GoerliChain().ChainId, + TxOrigin: EthAddress().String(), + Asset: StringRandom(r, 32), + Amount: math.NewUint(uint64(r.Int63())), + InboundTxObservedHash: StringRandom(r, 32), + InboundTxObservedExternalHeight: r.Uint64(), + InboundTxBallotIndex: StringRandom(r, 32), + InboundTxFinalizedZetaHeight: r.Uint64(), + } +} + func OutboundTxParams(r *rand.Rand) *types.OutboundTxParams { return &types.OutboundTxParams{ Receiver: EthAddress().String(), @@ -69,6 +84,22 @@ func OutboundTxParams(r *rand.Rand) *types.OutboundTxParams { } } +func OutboundTxParamsValidChainID(r *rand.Rand) *types.OutboundTxParams { + return &types.OutboundTxParams{ + Receiver: EthAddress().String(), + ReceiverChainId: chains.GoerliChain().ChainId, + Amount: math.NewUint(uint64(r.Int63())), + OutboundTxTssNonce: r.Uint64(), + OutboundTxGasLimit: r.Uint64(), + OutboundTxGasPrice: math.NewUint(uint64(r.Int63())).String(), + OutboundTxHash: StringRandom(r, 32), + OutboundTxBallotIndex: StringRandom(r, 32), + OutboundTxObservedExternalHeight: r.Uint64(), + OutboundTxGasUsed: r.Uint64(), + OutboundTxEffectiveGasPrice: math.NewInt(r.Int63()), + } +} + func Status(t *testing.T, index string) *types.Status { r := newRandFromStringSeed(t, index) diff --git a/testutil/sample/sample.go b/testutil/sample/sample.go index 0ac8621411..71b9eac564 100644 --- a/testutil/sample/sample.go +++ b/testutil/sample/sample.go @@ -8,6 +8,7 @@ import ( "testing" sdkmath "cosmossdk.io/math" + ethcrypto "github.com/ethereum/go-ethereum/crypto" "github.com/zeta-chain/zetacore/cmd/zetacored/config" "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" @@ -111,6 +112,12 @@ func Hash() ethcommon.Hash { return EthAddress().Hash() } +func ZetaIndex(t *testing.T) string { + msg := CrossChainTx(t, "foo") + hash := ethcrypto.Keccak256Hash([]byte(msg.String())) + return hash.Hex() +} + // Bytes returns a sample byte array func Bytes() []byte { return []byte("sample") diff --git a/x/authority/types/genesis_test.go b/x/authority/types/genesis_test.go index ec53fc1fad..7b835c3ea4 100644 --- a/x/authority/types/genesis_test.go +++ b/x/authority/types/genesis_test.go @@ -9,7 +9,7 @@ import ( ) func TestGenesisState_Validate(t *testing.T) { - setConfig() + setConfig(t) tests := []struct { name string diff --git a/x/authority/types/policies_test.go b/x/authority/types/policies_test.go index 93981da11c..8db173eb86 100644 --- a/x/authority/types/policies_test.go +++ b/x/authority/types/policies_test.go @@ -11,14 +11,19 @@ import ( ) // setConfig sets the global config to use zeta chain's bech32 prefixes -func setConfig() { +func setConfig(t *testing.T) { + defer func(t *testing.T) { + if r := recover(); r != nil { + t.Log("config is already sealed", r) + } + }(t) cfg := sdk.GetConfig() cfg.SetBech32PrefixForAccount(app.Bech32PrefixAccAddr, app.Bech32PrefixAccPub) + cfg.Seal() } func TestPolicies_Validate(t *testing.T) { - setConfig() - + setConfig(t) // use table driven tests to test the validation of policies tests := []struct { name string diff --git a/x/crosschain/client/integrationtests/cli_helpers.go b/x/crosschain/client/integrationtests/cli_helpers.go deleted file mode 100644 index 9cc05b32d6..0000000000 --- a/x/crosschain/client/integrationtests/cli_helpers.go +++ /dev/null @@ -1,323 +0,0 @@ -package integrationtests - -import ( - "fmt" - "os" - "strconv" - "testing" - - "cosmossdk.io/math" - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/client/flags" - "github.com/cosmos/cosmos-sdk/crypto/keyring" - "github.com/cosmos/cosmos-sdk/testutil" - clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" - sdk "github.com/cosmos/cosmos-sdk/types" - authcli "github.com/cosmos/cosmos-sdk/x/auth/client/cli" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - "github.com/stretchr/testify/require" - tmcli "github.com/tendermint/tendermint/libs/cli" - "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/zeta-chain/zetacore/pkg/coin" - "github.com/zeta-chain/zetacore/testutil/network" - "github.com/zeta-chain/zetacore/x/crosschain/client/cli" - "github.com/zeta-chain/zetacore/x/crosschain/types" - fungiblecli "github.com/zeta-chain/zetacore/x/fungible/client/cli" -) - -func TxSignExec(clientCtx client.Context, from fmt.Stringer, filename string, extraArgs ...string) (testutil.BufferWriter, error) { - args := []string{ - fmt.Sprintf("--%s=%s", flags.FlagKeyringBackend, keyring.BackendTest), - fmt.Sprintf("--from=%s", from.String()), - fmt.Sprintf("--%s=%s", flags.FlagChainID, clientCtx.ChainID), - filename, - } - - cmd := authcli.GetSignCommand() - tmcli.PrepareBaseCmd(cmd, "", "") - - return clitestutil.ExecTestCLICmd(clientCtx, cmd, append(args, extraArgs...)) -} - -func WriteToNewTempFile(t testing.TB, s string) *os.File { - t.Helper() - - fp := TempFile(t) - _, err := fp.WriteString(s) - - require.Nil(t, err) - - return fp -} - -// TempFile returns a writable temporary file for the test to use. -func TempFile(t testing.TB) *os.File { - t.Helper() - - fp, err := os.CreateTemp(GetTempDir(t), "") - require.NoError(t, err) - - return fp -} - -// GetTempDir returns a writable temporary director for the test to use. -func GetTempDir(t testing.TB) string { - t.Helper() - // os.MkDir() is used instead of testing.T.TempDir() - // see https://github.com/cosmos/cosmos-sdk/pull/8475 and - // https://github.com/cosmos/cosmos-sdk/pull/10341 for - // this change's rationale. - tempdir, err := os.MkdirTemp("", "") - require.NoError(t, err) - t.Cleanup(func() { - err := os.RemoveAll(tempdir) - require.NoError(t, err) - }) - return tempdir -} - -func BuildSignedDeploySystemContract(t testing.TB, val *network.Validator, denom string, account authtypes.AccountI) *os.File { - cmd := fungiblecli.CmdDeploySystemContracts() - txArgs := []string{ - fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address), - fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), - fmt.Sprintf("--%s=true", flags.FlagGenerateOnly), - fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(denom, sdk.NewInt(100))).String()), - // gas limit - fmt.Sprintf("--%s=%d", flags.FlagGas, 4000000), - } - out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, txArgs) - require.NoError(t, err) - unsignerdTx := WriteToNewTempFile(t, out.String()) - res, err := TxSignExec(val.ClientCtx, val.Address, unsignerdTx.Name(), - "--offline", "--account-number", strconv.FormatUint(account.GetAccountNumber(), 10), "--sequence", strconv.FormatUint(account.GetSequence(), 10)) - require.NoError(t, err) - return WriteToNewTempFile(t, res.String()) -} - -func BuildSignedUpdateSystemContract( - t testing.TB, - val *network.Validator, - denom string, - account authtypes.AccountI, - systemContractAddress string, -) *os.File { - cmd := fungiblecli.CmdUpdateSystemContract() - txArgs := []string{ - fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address), - fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), - fmt.Sprintf("--%s=true", flags.FlagGenerateOnly), - fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(denom, sdk.NewInt(100))).String()), - // gas limit - fmt.Sprintf("--%s=%d", flags.FlagGas, 4000000), - } - args := append([]string{systemContractAddress}, txArgs...) - out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, args) - require.NoError(t, err) - unsignerdTx := WriteToNewTempFile(t, out.String()) - res, err := TxSignExec(val.ClientCtx, val.Address, unsignerdTx.Name(), - "--offline", "--account-number", strconv.FormatUint(account.GetAccountNumber(), 10), "--sequence", strconv.FormatUint(account.GetSequence(), 10)) - require.NoError(t, err) - return WriteToNewTempFile(t, res.String()) -} - -func BuildSignedDeployETHZRC20( - t testing.TB, - val *network.Validator, - denom string, - account authtypes.AccountI, -) *os.File { - cmd := fungiblecli.CmdDeployFungibleCoinZRC4() - txArgs := []string{ - fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address), - fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), - fmt.Sprintf("--%s=true", flags.FlagGenerateOnly), - fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(denom, sdk.NewInt(100))).String()), - // gas limit - fmt.Sprintf("--%s=%d", flags.FlagGas, 10000000), - } - args := append([]string{ - "", - strconv.FormatInt(chains.GoerliLocalnetChain().ChainId, 10), - "18", - "ETH", - "gETH", - strconv.FormatInt(int64(coin.CoinType_Gas), 10), - "1000000", - }, txArgs...) - out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, args) - require.NoError(t, err) - unsignerdTx := WriteToNewTempFile(t, out.String()) - res, err := TxSignExec(val.ClientCtx, val.Address, unsignerdTx.Name(), - "--offline", "--account-number", strconv.FormatUint(account.GetAccountNumber(), 10), "--sequence", strconv.FormatUint(account.GetSequence(), 10)) - require.NoError(t, err) - return WriteToNewTempFile(t, res.String()) -} - -func BuildSignedGasPriceVote(t testing.TB, val *network.Validator, denom string, account authtypes.AccountI) *os.File { - cmd := cli.CmdGasPriceVoter() - inboundVoterArgs := []string{ - strconv.FormatInt(chains.GoerliLocalnetChain().ChainId, 10), - "10000000000", - "100", - "100", - } - txArgs := []string{ - fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address), - fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), - fmt.Sprintf("--%s=true", flags.FlagGenerateOnly), - fmt.Sprintf("--%s=%s", flags.FlagGas, "400000"), - fmt.Sprintf("--%s=%s", flags.FlagGasAdjustment, "1.5"), - fmt.Sprintf("--%s=%s", flags.FlagGasPrices, fmt.Sprintf("%s%s", "10", denom)), - } - args := append(inboundVoterArgs, txArgs...) - out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, args) - require.NoError(t, err) - unsignerdTx := WriteToNewTempFile(t, out.String()) - res, err := TxSignExec(val.ClientCtx, val.Address, unsignerdTx.Name(), - "--offline", "--account-number", strconv.FormatUint(account.GetAccountNumber(), 10), "--sequence", strconv.FormatUint(account.GetSequence(), 10)) - require.NoError(t, err) - return WriteToNewTempFile(t, res.String()) -} - -func BuildSignedTssVote(t testing.TB, val *network.Validator, denom string, account authtypes.AccountI) *os.File { - cmd := cli.CmdCreateTSSVoter() - inboundVoterArgs := []string{ - "tsspubkey", - "1", - "0", - } - txArgs := []string{ - fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address), - fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), - fmt.Sprintf("--%s=true", flags.FlagGenerateOnly), - fmt.Sprintf("--%s=%s", flags.FlagGas, "400000"), - fmt.Sprintf("--%s=%s", flags.FlagGasAdjustment, "1.5"), - fmt.Sprintf("--%s=%s", flags.FlagGasPrices, fmt.Sprintf("%s%s", "10", denom)), - } - args := append(inboundVoterArgs, txArgs...) - out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, args) - require.NoError(t, err) - unsignerdTx := WriteToNewTempFile(t, out.String()) - res, err := TxSignExec(val.ClientCtx, val.Address, unsignerdTx.Name(), - "--offline", "--account-number", strconv.FormatUint(account.GetAccountNumber(), 10), "--sequence", strconv.FormatUint(account.GetSequence(), 10)) - require.NoError(t, err) - return WriteToNewTempFile(t, res.String()) -} - -func BuildSignedOutboundVote( - t testing.TB, - val *network.Validator, - denom string, - account authtypes.AccountI, - nonce uint64, - cctxIndex, - outTxHash, - valueReceived, - status string, -) *os.File { - cmd := cli.CmdCCTXOutboundVoter() - outboundVoterArgs := []string{ - cctxIndex, - outTxHash, - "1", - "0", - "0", - "0", - valueReceived, - status, - strconv.FormatInt(chains.GoerliLocalnetChain().ChainId, 10), - strconv.FormatUint(nonce, 10), - "Zeta", - } - txArgs := []string{ - fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address), - fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), - fmt.Sprintf("--%s=true", flags.FlagGenerateOnly), - fmt.Sprintf("--%s=%s", flags.FlagGas, "400000"), - fmt.Sprintf("--%s=%s", flags.FlagGasAdjustment, "1.5"), - fmt.Sprintf("--%s=%s", flags.FlagGasPrices, fmt.Sprintf("%s%s", "10", denom)), - } - args := append(outboundVoterArgs, txArgs...) - out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, args) - require.NoError(t, err) - - unsignerdTx := WriteToNewTempFile(t, out.String()) - res, err := TxSignExec(val.ClientCtx, val.Address, unsignerdTx.Name(), - "--offline", "--account-number", strconv.FormatUint(account.GetAccountNumber(), 10), "--sequence", strconv.FormatUint(account.GetSequence(), 10)) - require.NoError(t, err) - return WriteToNewTempFile(t, res.String()) -} - -func BuildSignedInboundVote(t testing.TB, val *network.Validator, denom string, account authtypes.AccountI, message string, eventIndex int) *os.File { - cmd := cli.CmdCCTXInboundVoter() - inboundVoterArgs := []string{ - "0x96B05C238b99768F349135de0653b687f9c13fEE", - strconv.FormatInt(chains.GoerliLocalnetChain().ChainId, 10), - "0x3b9Fe88DE29efD13240829A0c18E9EC7A44C3CA7", - "0x96B05C238b99768F349135de0653b687f9c13fEE", - strconv.FormatInt(chains.GoerliLocalnetChain().ChainId, 10), - "10000000000000000000", - message, - "0x19398991572a825894b34b904ac1e3692720895351466b5c9e6bb7ae1e21d680", - "100", - "Zeta", - "", - strconv.Itoa(eventIndex), - } - txArgs := []string{ - fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address), - fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), - fmt.Sprintf("--%s=true", flags.FlagGenerateOnly), - fmt.Sprintf("--%s=%s", flags.FlagGas, "4000000"), - fmt.Sprintf("--%s=%s", flags.FlagGasAdjustment, "1.5"), - fmt.Sprintf("--%s=%s", flags.FlagGasPrices, fmt.Sprintf("%s%s", "10", denom)), - } - args := append(inboundVoterArgs, txArgs...) - out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, args) - require.NoError(t, err) - unsignerdTx := WriteToNewTempFile(t, out.String()) - res, err := TxSignExec(val.ClientCtx, val.Address, unsignerdTx.Name(), - "--offline", "--account-number", strconv.FormatUint(account.GetAccountNumber(), 10), "--sequence", strconv.FormatUint(account.GetSequence(), 10)) - require.NoError(t, err) - return WriteToNewTempFile(t, res.String()) -} - -func GetBallotIdentifier(message string, eventIndex int) string { - msg := types.NewMsgVoteOnObservedInboundTx( - "", - "0x96B05C238b99768F349135de0653b687f9c13fEE", - chains.GoerliLocalnetChain().ChainId, - "0x3b9Fe88DE29efD13240829A0c18E9EC7A44C3CA7", - "0x96B05C238b99768F349135de0653b687f9c13fEE", - chains.GoerliLocalnetChain().ChainId, - sdk.NewUint(10000000000000000000), - message, - "0x19398991572a825894b34b904ac1e3692720895351466b5c9e6bb7ae1e21d680", - 100, - 250_000, - coin.CoinType_Zeta, - "", - // #nosec G701 always positive - uint(eventIndex), - ) - return msg.Digest() -} - -func GetBallotIdentifierOutBound(nonce uint64, cctxindex, outtxHash, valueReceived string) string { - msg := types.NewMsgVoteOnObservedOutboundTx( - "", - cctxindex, - outtxHash, - 1, - 0, - math.ZeroInt(), - 0, - math.NewUintFromString(valueReceived), - 0, - chains.GoerliLocalnetChain().ChainId, - nonce, - coin.CoinType_Zeta, - ) - return msg.Digest() -} diff --git a/x/crosschain/client/integrationtests/cli_test.go b/x/crosschain/client/integrationtests/cli_test.go deleted file mode 100644 index de594398ce..0000000000 --- a/x/crosschain/client/integrationtests/cli_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package integrationtests - -import ( - "testing" - - "github.com/stretchr/testify/suite" - "github.com/zeta-chain/zetacore/testutil/network" -) - -func TestIntegrationTestSuite(t *testing.T) { - cfg := network.DefaultConfig() - suite.Run(t, NewIntegrationTestSuite(cfg)) -} diff --git a/x/crosschain/client/integrationtests/inbound_voter_test.go b/x/crosschain/client/integrationtests/inbound_voter_test.go deleted file mode 100644 index 50cf041a64..0000000000 --- a/x/crosschain/client/integrationtests/inbound_voter_test.go +++ /dev/null @@ -1,304 +0,0 @@ -package integrationtests - -import ( - "encoding/json" - "fmt" - "strings" - - clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" - authcli "github.com/cosmos/cosmos-sdk/x/auth/client/cli" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - crosschaincli "github.com/zeta-chain/zetacore/x/crosschain/client/cli" - crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" - observercli "github.com/zeta-chain/zetacore/x/observer/client/cli" - observerTypes "github.com/zeta-chain/zetacore/x/observer/types" -) - -type messageLog struct { - Events []event `json:"events"` -} - -type event struct { - Type string `json:"type"` - Attributes []attribute `json:"attributes"` -} - -type attribute struct { - Key string `json:"key"` - Value string `json:"value"` -} - -// fetchAttribute fetches the attribute from the tx response -func fetchAttribute(rawLog string, key string) (string, error) { - var logs []messageLog - err := json.Unmarshal([]byte(rawLog), &logs) - if err != nil { - return "", err - } - - var attributes []string - for _, log := range logs { - for _, event := range log.Events { - for _, attr := range event.Attributes { - attributes = append(attributes, attr.Key) - if strings.EqualFold(attr.Key, key) { - address := attr.Value - - // trim the quotes - address = address[1 : len(address)-1] - - return address, nil - } - - } - } - } - - return "", fmt.Errorf("attribute %s not found, attributes: %+v", key, attributes) -} - -type txRes struct { - RawLog string `json:"raw_log"` -} - -func ExtractRawLog(str string) (string, error) { - var data txRes - - err := json.Unmarshal([]byte(str), &data) - if err != nil { - return "", err - } - - return data.RawLog, nil -} - -func (s *IntegrationTestSuite) TestCCTXInboundVoter() { - broadcaster := s.network.Validators[0] - - var systemContractAddr string - // Initialize system contract - { - out, err := clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, authcli.GetAccountCmd(), []string{broadcaster.Address.String(), "--output", "json"}) - s.Require().NoError(err) - var account authtypes.AccountI - s.NoError(broadcaster.ClientCtx.Codec.UnmarshalInterfaceJSON(out.Bytes(), &account)) - signedTx := BuildSignedDeploySystemContract(s.T(), broadcaster, s.cfg.BondDenom, account) - res, err := clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, authcli.GetBroadcastCommand(), []string{signedTx.Name(), "--broadcast-mode", "block"}) - s.Require().NoError(err) - - rawLog, err := ExtractRawLog(res.String()) - s.Require().NoError(err) - - systemContractAddr, err = fetchAttribute(rawLog, "system_contract") - s.Require().NoError(err) - - // update system contract - out, err = clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, authcli.GetAccountCmd(), []string{broadcaster.Address.String(), "--output", "json"}) - s.Require().NoError(err) - s.NoError(broadcaster.ClientCtx.Codec.UnmarshalInterfaceJSON(out.Bytes(), &account)) - signedTx = BuildSignedUpdateSystemContract(s.T(), broadcaster, s.cfg.BondDenom, account, systemContractAddr) - res, err = clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, authcli.GetBroadcastCommand(), []string{signedTx.Name(), "--broadcast-mode", "block"}) - s.Require().NoError(err) - } - - // Deploy ETH ZRC20 - { - out, err := clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, authcli.GetAccountCmd(), []string{broadcaster.Address.String(), "--output", "json"}) - s.Require().NoError(err) - var account authtypes.AccountI - s.NoError(broadcaster.ClientCtx.Codec.UnmarshalInterfaceJSON(out.Bytes(), &account)) - signedTx := BuildSignedDeployETHZRC20(s.T(), broadcaster, s.cfg.BondDenom, account) - _, err = clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, authcli.GetBroadcastCommand(), []string{signedTx.Name(), "--broadcast-mode", "block"}) - s.Require().NoError(err) - } - - tt := []struct { - name string - votes map[string]observerTypes.VoteType - ballotResult observerTypes.BallotStatus - cctxStatus crosschaintypes.CctxStatus - falseBallotIdentifier string - }{ - { - name: "All observers voted success", - votes: map[string]observerTypes.VoteType{ - "zeta13c7p3xrhd6q2rx3h235jpt8pjdwvacyw6twpax": observerTypes.VoteType_SuccessObservation, - "zeta1f203dypqg5jh9hqfx0gfkmmnkdfuat3jr45ep2": observerTypes.VoteType_SuccessObservation, - "zeta1szrskhdeleyt6wmn0nfxvcvt2l6f4fn06uaga4": observerTypes.VoteType_SuccessObservation, - "zeta16h3y7s7030l4chcznwq3n6uz2m9wvmzu5vwt7c": observerTypes.VoteType_SuccessObservation, - "zeta1xl2rfsrmx8nxryty3lsjuxwdxs59cn2q65e5ca": observerTypes.VoteType_SuccessObservation, - "zeta1ktmprjdvc72jq0mpu8tn8sqx9xwj685qx0q6kt": observerTypes.VoteType_SuccessObservation, - "zeta1ygeyr8pqfjvclxay5234gulnjzv2mkz6lph9y4": observerTypes.VoteType_SuccessObservation, - "zeta1zegyenj7xg5nck04ykkzndm2qxdzc6v83mklsy": observerTypes.VoteType_SuccessObservation, - "zeta1us2qpqdcctk6q7qv2c9d9jvjxlv88jscf68kav": observerTypes.VoteType_SuccessObservation, - "zeta1e9fyaulgntkrnqnl0es4nyxghp3petpn2ntu3t": observerTypes.VoteType_SuccessObservation, - }, - ballotResult: observerTypes.BallotStatus_BallotFinalized_SuccessObservation, - cctxStatus: crosschaintypes.CctxStatus_PendingOutbound, - }, - { - name: "5 votes only ballot does not get finalized", - votes: map[string]observerTypes.VoteType{ - "zeta13c7p3xrhd6q2rx3h235jpt8pjdwvacyw6twpax": observerTypes.VoteType_SuccessObservation, - "zeta1f203dypqg5jh9hqfx0gfkmmnkdfuat3jr45ep2": observerTypes.VoteType_SuccessObservation, - "zeta1szrskhdeleyt6wmn0nfxvcvt2l6f4fn06uaga4": observerTypes.VoteType_SuccessObservation, - "zeta16h3y7s7030l4chcznwq3n6uz2m9wvmzu5vwt7c": observerTypes.VoteType_SuccessObservation, - "zeta1xl2rfsrmx8nxryty3lsjuxwdxs59cn2q65e5ca": observerTypes.VoteType_SuccessObservation, - "zeta1ktmprjdvc72jq0mpu8tn8sqx9xwj685qx0q6kt": observerTypes.VoteType_NotYetVoted, - "zeta1ygeyr8pqfjvclxay5234gulnjzv2mkz6lph9y4": observerTypes.VoteType_NotYetVoted, - "zeta1zegyenj7xg5nck04ykkzndm2qxdzc6v83mklsy": observerTypes.VoteType_NotYetVoted, - "zeta1us2qpqdcctk6q7qv2c9d9jvjxlv88jscf68kav": observerTypes.VoteType_NotYetVoted, - "zeta1e9fyaulgntkrnqnl0es4nyxghp3petpn2ntu3t": observerTypes.VoteType_NotYetVoted, - }, - ballotResult: observerTypes.BallotStatus_BallotInProgress, - cctxStatus: crosschaintypes.CctxStatus_PendingRevert, - }, - { - name: "1 false vote but correct ballot is still finalized", - votes: map[string]observerTypes.VoteType{ - "zeta13c7p3xrhd6q2rx3h235jpt8pjdwvacyw6twpax": observerTypes.VoteType_SuccessObservation, - "zeta1f203dypqg5jh9hqfx0gfkmmnkdfuat3jr45ep2": observerTypes.VoteType_SuccessObservation, - "zeta1szrskhdeleyt6wmn0nfxvcvt2l6f4fn06uaga4": observerTypes.VoteType_SuccessObservation, - "zeta16h3y7s7030l4chcznwq3n6uz2m9wvmzu5vwt7c": observerTypes.VoteType_SuccessObservation, - "zeta1xl2rfsrmx8nxryty3lsjuxwdxs59cn2q65e5ca": observerTypes.VoteType_SuccessObservation, - "zeta1ktmprjdvc72jq0mpu8tn8sqx9xwj685qx0q6kt": observerTypes.VoteType_SuccessObservation, - "zeta1ygeyr8pqfjvclxay5234gulnjzv2mkz6lph9y4": observerTypes.VoteType_SuccessObservation, - "zeta1zegyenj7xg5nck04ykkzndm2qxdzc6v83mklsy": observerTypes.VoteType_FailureObservation, - "zeta1us2qpqdcctk6q7qv2c9d9jvjxlv88jscf68kav": observerTypes.VoteType_SuccessObservation, - "zeta1e9fyaulgntkrnqnl0es4nyxghp3petpn2ntu3t": observerTypes.VoteType_NotYetVoted, - }, - ballotResult: observerTypes.BallotStatus_BallotFinalized_SuccessObservation, - cctxStatus: crosschaintypes.CctxStatus_PendingOutbound, - }, - { - name: "2 ballots with 5 votes each no ballot gets finalized", - votes: map[string]observerTypes.VoteType{ - "zeta13c7p3xrhd6q2rx3h235jpt8pjdwvacyw6twpax": observerTypes.VoteType_SuccessObservation, - "zeta1f203dypqg5jh9hqfx0gfkmmnkdfuat3jr45ep2": observerTypes.VoteType_SuccessObservation, - "zeta1szrskhdeleyt6wmn0nfxvcvt2l6f4fn06uaga4": observerTypes.VoteType_SuccessObservation, - "zeta16h3y7s7030l4chcznwq3n6uz2m9wvmzu5vwt7c": observerTypes.VoteType_SuccessObservation, - "zeta1xl2rfsrmx8nxryty3lsjuxwdxs59cn2q65e5ca": observerTypes.VoteType_SuccessObservation, - "zeta1ktmprjdvc72jq0mpu8tn8sqx9xwj685qx0q6kt": observerTypes.VoteType_FailureObservation, - "zeta1ygeyr8pqfjvclxay5234gulnjzv2mkz6lph9y4": observerTypes.VoteType_FailureObservation, - "zeta1zegyenj7xg5nck04ykkzndm2qxdzc6v83mklsy": observerTypes.VoteType_FailureObservation, - "zeta1us2qpqdcctk6q7qv2c9d9jvjxlv88jscf68kav": observerTypes.VoteType_FailureObservation, - "zeta1e9fyaulgntkrnqnl0es4nyxghp3petpn2ntu3t": observerTypes.VoteType_FailureObservation, - }, - ballotResult: observerTypes.BallotStatus_BallotInProgress, - cctxStatus: crosschaintypes.CctxStatus_PendingRevert, - }, - { - name: "majority wrong votes incorrect ballot finalized / correct ballot still in progress", - votes: map[string]observerTypes.VoteType{ - "zeta13c7p3xrhd6q2rx3h235jpt8pjdwvacyw6twpax": observerTypes.VoteType_SuccessObservation, - "zeta1f203dypqg5jh9hqfx0gfkmmnkdfuat3jr45ep2": observerTypes.VoteType_SuccessObservation, - "zeta1szrskhdeleyt6wmn0nfxvcvt2l6f4fn06uaga4": observerTypes.VoteType_SuccessObservation, - "zeta16h3y7s7030l4chcznwq3n6uz2m9wvmzu5vwt7c": observerTypes.VoteType_FailureObservation, - "zeta1xl2rfsrmx8nxryty3lsjuxwdxs59cn2q65e5ca": observerTypes.VoteType_FailureObservation, - "zeta1ktmprjdvc72jq0mpu8tn8sqx9xwj685qx0q6kt": observerTypes.VoteType_FailureObservation, - "zeta1ygeyr8pqfjvclxay5234gulnjzv2mkz6lph9y4": observerTypes.VoteType_FailureObservation, - "zeta1zegyenj7xg5nck04ykkzndm2qxdzc6v83mklsy": observerTypes.VoteType_FailureObservation, - "zeta1us2qpqdcctk6q7qv2c9d9jvjxlv88jscf68kav": observerTypes.VoteType_FailureObservation, - "zeta1e9fyaulgntkrnqnl0es4nyxghp3petpn2ntu3t": observerTypes.VoteType_FailureObservation, - }, - ballotResult: observerTypes.BallotStatus_BallotInProgress, - cctxStatus: crosschaintypes.CctxStatus_PendingOutbound, - falseBallotIdentifier: "majority wrong votes incorrect ballot finalized / correct ballot still in progress" + "falseVote", - }, - { - name: "7 votes only just crossed threshold", - votes: map[string]observerTypes.VoteType{ - "zeta13c7p3xrhd6q2rx3h235jpt8pjdwvacyw6twpax": observerTypes.VoteType_SuccessObservation, - "zeta1f203dypqg5jh9hqfx0gfkmmnkdfuat3jr45ep2": observerTypes.VoteType_SuccessObservation, - "zeta1szrskhdeleyt6wmn0nfxvcvt2l6f4fn06uaga4": observerTypes.VoteType_SuccessObservation, - "zeta16h3y7s7030l4chcznwq3n6uz2m9wvmzu5vwt7c": observerTypes.VoteType_SuccessObservation, - "zeta1xl2rfsrmx8nxryty3lsjuxwdxs59cn2q65e5ca": observerTypes.VoteType_SuccessObservation, - "zeta1ktmprjdvc72jq0mpu8tn8sqx9xwj685qx0q6kt": observerTypes.VoteType_NotYetVoted, - "zeta1ygeyr8pqfjvclxay5234gulnjzv2mkz6lph9y4": observerTypes.VoteType_SuccessObservation, - "zeta1zegyenj7xg5nck04ykkzndm2qxdzc6v83mklsy": observerTypes.VoteType_NotYetVoted, - "zeta1us2qpqdcctk6q7qv2c9d9jvjxlv88jscf68kav": observerTypes.VoteType_NotYetVoted, - "zeta1e9fyaulgntkrnqnl0es4nyxghp3petpn2ntu3t": observerTypes.VoteType_SuccessObservation, - }, - ballotResult: observerTypes.BallotStatus_BallotFinalized_SuccessObservation, - cctxStatus: crosschaintypes.CctxStatus_PendingOutbound, - }, - } - for i, test := range tt { - test := test - s.Run(test.name, func() { - // Vote the gas price - for _, val := range s.network.Validators { - out, err := clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, authcli.GetAccountCmd(), []string{val.Address.String(), "--output", "json"}) - s.Require().NoError(err) - - var account authtypes.AccountI - s.NoError(val.ClientCtx.Codec.UnmarshalInterfaceJSON(out.Bytes(), &account)) - signedTx := BuildSignedGasPriceVote(s.T(), val, s.cfg.BondDenom, account) - _, err = clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, authcli.GetBroadcastCommand(), []string{signedTx.Name(), "--broadcast-mode", "sync"}) - s.Require().NoError(err) - } - - s.Require().NoError(s.network.WaitForNBlocks(2)) - out, err := clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, observercli.CmdListPendingNonces(), []string{"--output", "json"}) - s.Require().NoError(err) - out, err = clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, observercli.CmdGetSupportedChains(), []string{"--output", "json"}) - s.Require().NoError(err) - out, err = clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, crosschaincli.CmdListGasPrice(), []string{"--output", "json"}) - s.Require().NoError(err) - - // Vote the inbound tx - for _, val := range s.network.Validators { - vote := test.votes[val.Address.String()] - if vote == observerTypes.VoteType_NotYetVoted { - continue - } - out, err := clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, authcli.GetAccountCmd(), []string{val.Address.String(), "--output", "json"}) - var account authtypes.AccountI - s.NoError(val.ClientCtx.Codec.UnmarshalInterfaceJSON(out.Bytes(), &account)) - - message := test.name - if vote == observerTypes.VoteType_FailureObservation { - message = message + "falseVote" - } - signedTx := BuildSignedInboundVote(s.T(), val, s.cfg.BondDenom, account, message, i) - out, err = clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, authcli.GetBroadcastCommand(), []string{signedTx.Name(), "--broadcast-mode", "block"}) - s.Require().NoError(err) - fmt.Println(out.String()) - } - s.Require().NoError(s.network.WaitForNBlocks(2)) - - // Get the ballot - ballotIdentifier := GetBallotIdentifier(test.name, i) - out, err = clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, observercli.CmdBallotByIdentifier(), []string{ballotIdentifier, "--output", "json"}) - s.Require().NoError(err) - ballot := observerTypes.QueryBallotByIdentifierResponse{} - s.NoError(broadcaster.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &ballot)) - - // Check the vote in the ballot - s.Require().Equal(len(test.votes), len(ballot.Voters)) - for _, vote := range ballot.Voters { - if test.votes[vote.VoterAddress] == observerTypes.VoteType_FailureObservation { - s.Assert().Equal(observerTypes.VoteType_NotYetVoted.String(), vote.VoteType.String()) - continue - } - s.Assert().Equal(test.votes[vote.VoterAddress].String(), vote.VoteType.String(), "incorrect vote for voter: %s", vote.VoterAddress) - } - s.Require().Equal(test.ballotResult.String(), ballot.BallotStatus.String()) - - // Get the cctx and check its status - cctxIdentifier := ballotIdentifier - if test.falseBallotIdentifier != "" { - cctxIdentifier = GetBallotIdentifier(test.falseBallotIdentifier, i) - } - out, err = clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, crosschaincli.CmdShowSend(), []string{cctxIdentifier, "--output", "json"}) - cctx := crosschaintypes.QueryGetCctxResponse{} - if test.cctxStatus == crosschaintypes.CctxStatus_PendingRevert { - s.Require().Contains(out.String(), "not found") - } else { - s.NoError(broadcaster.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &cctx)) - s.Require().Equal(test.cctxStatus.String(), cctx.CrossChainTx.CctxStatus.Status.String(), cctx.CrossChainTx.CctxStatus.StatusMessage) - } - }) - } - -} diff --git a/x/crosschain/client/integrationtests/outbound_voter_test.go b/x/crosschain/client/integrationtests/outbound_voter_test.go deleted file mode 100644 index d388adc003..0000000000 --- a/x/crosschain/client/integrationtests/outbound_voter_test.go +++ /dev/null @@ -1,248 +0,0 @@ -package integrationtests - -import ( - clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" - authcli "github.com/cosmos/cosmos-sdk/x/auth/client/cli" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - crosschaincli "github.com/zeta-chain/zetacore/x/crosschain/client/cli" - crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" - observercli "github.com/zeta-chain/zetacore/x/observer/client/cli" - observerTypes "github.com/zeta-chain/zetacore/x/observer/types" -) - -func (s *IntegrationTestSuite) TestCCTXOutBoundVoter() { - type Vote struct { - voterAddress string - voteType observerTypes.VoteType - isFakeVote bool - } - tt := []struct { - name string - votes []Vote - valueReceived string // TODO : calculate this value - correctBallotResult observerTypes.BallotStatus - cctxStatus crosschaintypes.CctxStatus - falseBallotIdentifier string - }{ - { - name: "All observers voted success or not voted", - votes: []Vote{ - {voterAddress: "zeta13c7p3xrhd6q2rx3h235jpt8pjdwvacyw6twpax", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1f203dypqg5jh9hqfx0gfkmmnkdfuat3jr45ep2", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1szrskhdeleyt6wmn0nfxvcvt2l6f4fn06uaga4", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta16h3y7s7030l4chcznwq3n6uz2m9wvmzu5vwt7c", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1xl2rfsrmx8nxryty3lsjuxwdxs59cn2q65e5ca", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1ktmprjdvc72jq0mpu8tn8sqx9xwj685qx0q6kt", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1ygeyr8pqfjvclxay5234gulnjzv2mkz6lph9y4", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1zegyenj7xg5nck04ykkzndm2qxdzc6v83mklsy", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1us2qpqdcctk6q7qv2c9d9jvjxlv88jscf68kav", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1e9fyaulgntkrnqnl0es4nyxghp3petpn2ntu3t", voteType: observerTypes.VoteType_NotYetVoted, isFakeVote: false}, - }, - correctBallotResult: observerTypes.BallotStatus_BallotFinalized_SuccessObservation, - cctxStatus: crosschaintypes.CctxStatus_OutboundMined, - valueReceived: "7991636132140714751", - }, - { - name: "1 fake vote but ballot still success", - votes: []Vote{ - {voterAddress: "zeta13c7p3xrhd6q2rx3h235jpt8pjdwvacyw6twpax", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1f203dypqg5jh9hqfx0gfkmmnkdfuat3jr45ep2", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1szrskhdeleyt6wmn0nfxvcvt2l6f4fn06uaga4", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta16h3y7s7030l4chcznwq3n6uz2m9wvmzu5vwt7c", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1xl2rfsrmx8nxryty3lsjuxwdxs59cn2q65e5ca", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1ktmprjdvc72jq0mpu8tn8sqx9xwj685qx0q6kt", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1ygeyr8pqfjvclxay5234gulnjzv2mkz6lph9y4", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1zegyenj7xg5nck04ykkzndm2qxdzc6v83mklsy", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1us2qpqdcctk6q7qv2c9d9jvjxlv88jscf68kav", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: true}, - {voterAddress: "zeta1e9fyaulgntkrnqnl0es4nyxghp3petpn2ntu3t", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - }, - correctBallotResult: observerTypes.BallotStatus_BallotFinalized_SuccessObservation, - cctxStatus: crosschaintypes.CctxStatus_OutboundMined, - valueReceived: "7990439496224753106", - }, - { - name: "Half success and half false", - votes: []Vote{ - {voterAddress: "zeta13c7p3xrhd6q2rx3h235jpt8pjdwvacyw6twpax", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1f203dypqg5jh9hqfx0gfkmmnkdfuat3jr45ep2", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1szrskhdeleyt6wmn0nfxvcvt2l6f4fn06uaga4", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta16h3y7s7030l4chcznwq3n6uz2m9wvmzu5vwt7c", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1xl2rfsrmx8nxryty3lsjuxwdxs59cn2q65e5ca", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1ktmprjdvc72jq0mpu8tn8sqx9xwj685qx0q6kt", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: true}, - {voterAddress: "zeta1ygeyr8pqfjvclxay5234gulnjzv2mkz6lph9y4", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: true}, - {voterAddress: "zeta1zegyenj7xg5nck04ykkzndm2qxdzc6v83mklsy", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: true}, - {voterAddress: "zeta1us2qpqdcctk6q7qv2c9d9jvjxlv88jscf68kav", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: true}, - {voterAddress: "zeta1e9fyaulgntkrnqnl0es4nyxghp3petpn2ntu3t", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: true}, - }, - correctBallotResult: observerTypes.BallotStatus_BallotInProgress, - cctxStatus: crosschaintypes.CctxStatus_PendingOutbound, - valueReceived: "7990439496224753106", - }, - { - name: "Fake ballot has more votes outbound gets finalized", - votes: []Vote{ - {voterAddress: "zeta13c7p3xrhd6q2rx3h235jpt8pjdwvacyw6twpax", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1f203dypqg5jh9hqfx0gfkmmnkdfuat3jr45ep2", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1szrskhdeleyt6wmn0nfxvcvt2l6f4fn06uaga4", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: true}, - {voterAddress: "zeta16h3y7s7030l4chcznwq3n6uz2m9wvmzu5vwt7c", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: true}, - {voterAddress: "zeta1xl2rfsrmx8nxryty3lsjuxwdxs59cn2q65e5ca", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: true}, - {voterAddress: "zeta1ktmprjdvc72jq0mpu8tn8sqx9xwj685qx0q6kt", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: true}, - {voterAddress: "zeta1ygeyr8pqfjvclxay5234gulnjzv2mkz6lph9y4", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: true}, - {voterAddress: "zeta1zegyenj7xg5nck04ykkzndm2qxdzc6v83mklsy", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: true}, - {voterAddress: "zeta1us2qpqdcctk6q7qv2c9d9jvjxlv88jscf68kav", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: true}, - {voterAddress: "zeta1e9fyaulgntkrnqnl0es4nyxghp3petpn2ntu3t", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: true}, - }, - correctBallotResult: observerTypes.BallotStatus_BallotInProgress, - cctxStatus: crosschaintypes.CctxStatus_OutboundMined, - valueReceived: "7987124742653889020", - }, - { - name: "5 success 5 Failed votes ", - votes: []Vote{ - {voterAddress: "zeta13c7p3xrhd6q2rx3h235jpt8pjdwvacyw6twpax", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1f203dypqg5jh9hqfx0gfkmmnkdfuat3jr45ep2", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1szrskhdeleyt6wmn0nfxvcvt2l6f4fn06uaga4", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta16h3y7s7030l4chcznwq3n6uz2m9wvmzu5vwt7c", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1xl2rfsrmx8nxryty3lsjuxwdxs59cn2q65e5ca", voteType: observerTypes.VoteType_SuccessObservation, isFakeVote: false}, - {voterAddress: "zeta1ktmprjdvc72jq0mpu8tn8sqx9xwj685qx0q6kt", voteType: observerTypes.VoteType_FailureObservation, isFakeVote: false}, - {voterAddress: "zeta1ygeyr8pqfjvclxay5234gulnjzv2mkz6lph9y4", voteType: observerTypes.VoteType_FailureObservation, isFakeVote: false}, - {voterAddress: "zeta1zegyenj7xg5nck04ykkzndm2qxdzc6v83mklsy", voteType: observerTypes.VoteType_FailureObservation, isFakeVote: false}, - {voterAddress: "zeta1us2qpqdcctk6q7qv2c9d9jvjxlv88jscf68kav", voteType: observerTypes.VoteType_FailureObservation, isFakeVote: false}, - {voterAddress: "zeta1e9fyaulgntkrnqnl0es4nyxghp3petpn2ntu3t", voteType: observerTypes.VoteType_FailureObservation, isFakeVote: false}, - }, - correctBallotResult: observerTypes.BallotStatus_BallotInProgress, - cctxStatus: crosschaintypes.CctxStatus_PendingOutbound, - valueReceived: "7991636132140714751", - }, - } - for i, test := range tt { - // Buffer event index so that it does not clash with the inbound voter test - eventIndex := i + 100 - test := test - s.Run(test.name, func() { - broadcaster := s.network.Validators[0] - - // Vote the gas price - for _, val := range s.network.Validators { - out, err := clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, authcli.GetAccountCmd(), []string{val.Address.String(), "--output", "json"}) - var account authtypes.AccountI - s.NoError(val.ClientCtx.Codec.UnmarshalInterfaceJSON(out.Bytes(), &account)) - signedTx := BuildSignedGasPriceVote(s.T(), val, s.cfg.BondDenom, account) - _, err = clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, authcli.GetBroadcastCommand(), []string{signedTx.Name(), "--broadcast-mode", "sync"}) - s.Require().NoError(err) - } - s.Require().NoError(s.network.WaitForNBlocks(2)) - - // Vote the tss - for _, val := range s.network.Validators { - out, err := clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, authcli.GetAccountCmd(), []string{val.Address.String(), "--output", "json"}) - var account authtypes.AccountI - s.NoError(val.ClientCtx.Codec.UnmarshalInterfaceJSON(out.Bytes(), &account)) - signedTx := BuildSignedTssVote(s.T(), val, s.cfg.BondDenom, account) - out, err = clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, authcli.GetBroadcastCommand(), []string{signedTx.Name(), "--broadcast-mode", "sync"}) - s.Require().NoError(err) - } - s.Require().NoError(s.network.WaitForNBlocks(2)) - - // Vote the inbound tx - for _, val := range s.network.Validators { - out, err := clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, authcli.GetAccountCmd(), []string{val.Address.String(), "--output", "json"}) - var account authtypes.AccountI - s.NoError(val.ClientCtx.Codec.UnmarshalInterfaceJSON(out.Bytes(), &account)) - message := test.name - signedTx := BuildSignedInboundVote(s.T(), val, s.cfg.BondDenom, account, message, eventIndex) - out, err = clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, authcli.GetBroadcastCommand(), []string{signedTx.Name(), "--broadcast-mode", "sync"}) - s.Require().NoError(err) - } - s.Require().NoError(s.network.WaitForNBlocks(2)) - - // Get the ballot - cctxIdentifier := GetBallotIdentifier(test.name, eventIndex) - out, err := clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, crosschaincli.CmdShowSend(), []string{cctxIdentifier, "--output", "json"}) - cctx := crosschaintypes.QueryGetCctxResponse{} - s.NoError(broadcaster.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &cctx)) - s.Assert().Equal(crosschaintypes.CctxStatus_PendingOutbound.String(), cctx.CrossChainTx.CctxStatus.Status.String(), cctx.CrossChainTx.CctxStatus.StatusMessage) - nonce := cctx.CrossChainTx.GetCurrentOutTxParam().OutboundTxTssNonce - // Check the vote in the ballot and vote the outbound tx - fakeVotes := []string{} - for _, val := range s.network.Validators { - valVote := Vote{} - for _, vote := range test.votes { - if vote.voterAddress == val.Address.String() { - valVote = vote - } - } - if valVote.voteType == observerTypes.VoteType_NotYetVoted { - continue - } - out, err = clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, authcli.GetAccountCmd(), []string{val.Address.String(), "--output", "json"}) - var account authtypes.AccountI - s.NoError(val.ClientCtx.Codec.UnmarshalInterfaceJSON(out.Bytes(), &account)) - - outTxhash := test.name - if valVote.isFakeVote { - outTxhash = outTxhash + "falseVote" - fakeVotes = append(fakeVotes, val.Address.String()) - } - votestring := "" - switch valVote.voteType { - case observerTypes.VoteType_SuccessObservation: - votestring = "0" - case observerTypes.VoteType_FailureObservation: - votestring = "1" - } - - // Vote the outbound tx - signedTx := BuildSignedOutboundVote(s.T(), val, s.cfg.BondDenom, account, nonce, cctxIdentifier, outTxhash, test.valueReceived, votestring) - out, err = clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, authcli.GetBroadcastCommand(), []string{signedTx.Name(), "--broadcast-mode", "sync", "--output", "json"}) - s.Require().NoError(err) - } - s.Require().NoError(s.network.WaitForNBlocks(2)) - - // Get the cctx - out, err = clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, crosschaincli.CmdShowSend(), []string{cctxIdentifier, "--output", "json"}) - cctx = crosschaintypes.QueryGetCctxResponse{} - s.NoError(broadcaster.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &cctx)) - s.Assert().Equal(test.cctxStatus.String(), cctx.CrossChainTx.CctxStatus.Status.String(), cctx.CrossChainTx.CctxStatus.StatusMessage) - - outboundBallotIdentifier := GetBallotIdentifierOutBound(nonce, cctxIdentifier, test.name, test.valueReceived) - - out, err = clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, observercli.CmdBallotByIdentifier(), []string{outboundBallotIdentifier, "--output", "json"}) - s.Require().NoError(err) - ballot := observerTypes.QueryBallotByIdentifierResponse{} - s.NoError(broadcaster.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &ballot)) - - // Check the votes - s.Require().Equal(test.correctBallotResult.String(), ballot.BallotStatus.String()) - for _, vote := range test.votes { - for _, ballotvote := range ballot.Voters { - if vote.voterAddress == ballotvote.VoterAddress { - if !vote.isFakeVote { - s.Assert().Equal(vote.voteType.String(), ballotvote.VoteType.String()) - } else { - s.Assert().Equal(observerTypes.VoteType_NotYetVoted.String(), ballotvote.VoteType.String()) - } - break - } - } - } - if len(fakeVotes) > 0 { - outboundFakeBallotIdentifier := GetBallotIdentifierOutBound(nonce, cctxIdentifier, test.name+"falseVote", test.valueReceived) - out, err = clitestutil.ExecTestCLICmd(broadcaster.ClientCtx, observercli.CmdBallotByIdentifier(), []string{outboundFakeBallotIdentifier, "--output", "json"}) - s.Require().NoError(err) - fakeBallot := observerTypes.QueryBallotByIdentifierResponse{} - s.NoError(broadcaster.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &fakeBallot)) - for _, vote := range test.votes { - if vote.isFakeVote { - for _, ballotVote := range fakeBallot.Voters { - if vote.voterAddress == ballotVote.VoterAddress { - s.Assert().Equal(vote.voteType.String(), ballotVote.VoteType.String()) - break - } - } - } - } - } - }) - } -} diff --git a/x/crosschain/client/integrationtests/suite.go b/x/crosschain/client/integrationtests/suite.go deleted file mode 100644 index b5f16a683d..0000000000 --- a/x/crosschain/client/integrationtests/suite.go +++ /dev/null @@ -1,65 +0,0 @@ -package integrationtests - -import ( - "time" - - sdk "github.com/cosmos/cosmos-sdk/types" - ethcfg "github.com/evmos/ethermint/cmd/config" - "github.com/stretchr/testify/suite" - "github.com/zeta-chain/zetacore/app" - cmdcfg "github.com/zeta-chain/zetacore/cmd/zetacored/config" - "github.com/zeta-chain/zetacore/testutil/network" -) - -type IntegrationTestSuite struct { - suite.Suite - - cfg network.Config - network *network.Network -} - -func NewIntegrationTestSuite(cfg network.Config) *IntegrationTestSuite { - return &IntegrationTestSuite{cfg: cfg} -} - -func (s *IntegrationTestSuite) Setconfig() { - config := sdk.GetConfig() - cmdcfg.SetBech32Prefixes(config) - ethcfg.SetBip44CoinType(config) - // Make sure address is compatible with ethereum - config.SetAddressVerifier(app.VerifyAddressFormat) - config.Seal() -} -func (s *IntegrationTestSuite) SetupSuite() { - s.T().Log("setting up integration test suite") - s.Setconfig() - minOBsDel, ok := sdk.NewIntFromString("100000000000000000000") - s.Require().True(ok) - s.cfg.StakingTokens = minOBsDel.Mul(sdk.NewInt(int64(10))) - s.cfg.BondedTokens = minOBsDel - observerList := []string{"zeta13c7p3xrhd6q2rx3h235jpt8pjdwvacyw6twpax", - "zeta1f203dypqg5jh9hqfx0gfkmmnkdfuat3jr45ep2", - "zeta1szrskhdeleyt6wmn0nfxvcvt2l6f4fn06uaga4", - "zeta16h3y7s7030l4chcznwq3n6uz2m9wvmzu5vwt7c", - "zeta1xl2rfsrmx8nxryty3lsjuxwdxs59cn2q65e5ca", - "zeta1ktmprjdvc72jq0mpu8tn8sqx9xwj685qx0q6kt", - "zeta1ygeyr8pqfjvclxay5234gulnjzv2mkz6lph9y4", - "zeta1zegyenj7xg5nck04ykkzndm2qxdzc6v83mklsy", - "zeta1us2qpqdcctk6q7qv2c9d9jvjxlv88jscf68kav", - "zeta1e9fyaulgntkrnqnl0es4nyxghp3petpn2ntu3t", - } - network.SetupZetaGenesisState(s.T(), s.cfg.GenesisState, s.cfg.Codec, observerList, true) - network.AddCrosschainData(s.T(), 0, s.cfg.GenesisState, s.cfg.Codec) - network.AddObserverData(s.T(), 0, s.cfg.GenesisState, s.cfg.Codec, nil) - net, err := network.New(s.T(), app.NodeDir, s.cfg) - s.Require().NoError(err) - s.network = net - time.Sleep(3 * time.Second) - _, err = s.network.WaitForHeight(1) - s.Require().NoError(err) -} - -func (s *IntegrationTestSuite) TearDownSuite() { - s.T().Log("tearing down integration test suite") - s.network.Cleanup() -} diff --git a/x/crosschain/keeper/cctx.go b/x/crosschain/keeper/cctx.go index c689e0cc00..10613e2d6c 100644 --- a/x/crosschain/keeper/cctx.go +++ b/x/crosschain/keeper/cctx.go @@ -3,7 +3,6 @@ package keeper import ( "fmt" - "cosmossdk.io/math" "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/zeta-chain/zetacore/pkg/coin" @@ -48,7 +47,7 @@ func (k Keeper) SetCctxAndNonceToCctxAndInTxHashToCctx(ctx sdk.Context, cctx typ Tss: tss.TssPubkey, }) } - if cctx.CctxStatus.Status == types.CctxStatus_Aborted && cctx.GetCurrentOutTxParam().CoinType == coin.CoinType_Zeta { + if cctx.CctxStatus.Status == types.CctxStatus_Aborted && cctx.InboundTxParams.CoinType == coin.CoinType_Zeta { k.AddZetaAbortedAmount(ctx, GetAbortedAmount(cctx)) } } @@ -98,59 +97,3 @@ func (k Keeper) RemoveCrossChainTx(ctx sdk.Context, index string) { store := prefix.NewStore(ctx.KVStore(k.storeKey), p) store.Delete(types.KeyPrefix(index)) } - -func (k Keeper) CreateNewCCTX( - ctx sdk.Context, - msg *types.MsgVoteOnObservedInboundTx, - index string, - tssPubkey string, - s types.CctxStatus, - senderChainID, - receiverChainID int64, -) types.CrossChainTx { - if msg.TxOrigin == "" { - msg.TxOrigin = msg.Sender - } - inboundParams := &types.InboundTxParams{ - Sender: msg.Sender, - SenderChainId: senderChainID, - TxOrigin: msg.TxOrigin, - Asset: msg.Asset, - Amount: msg.Amount, - CoinType: msg.CoinType, - InboundTxObservedHash: msg.InTxHash, - InboundTxObservedExternalHeight: msg.InBlockHeight, - InboundTxFinalizedZetaHeight: 0, - InboundTxBallotIndex: index, - } - - outBoundParams := &types.OutboundTxParams{ - Receiver: msg.Receiver, - ReceiverChainId: receiverChainID, - OutboundTxHash: "", - OutboundTxTssNonce: 0, - OutboundTxGasLimit: msg.GasLimit, - OutboundTxGasPrice: "", - OutboundTxBallotIndex: "", - OutboundTxObservedExternalHeight: 0, - CoinType: msg.CoinType, // FIXME: is this correct? - Amount: sdk.NewUint(0), - TssPubkey: tssPubkey, - } - status := &types.Status{ - Status: s, - StatusMessage: "", - LastUpdateTimestamp: ctx.BlockHeader().Time.Unix(), - IsAbortRefunded: false, - } - newCctx := types.CrossChainTx{ - Creator: msg.Creator, - Index: index, - ZetaFees: math.ZeroUint(), - RelayedMessage: msg.Message, - CctxStatus: status, - InboundTxParams: inboundParams, - OutboundTxParams: []*types.OutboundTxParams{outBoundParams}, - } - return newCctx -} diff --git a/x/crosschain/keeper/cctx_test.go b/x/crosschain/keeper/cctx_test.go index 7f51dca642..3f8c3fe4d4 100644 --- a/x/crosschain/keeper/cctx_test.go +++ b/x/crosschain/keeper/cctx_test.go @@ -62,7 +62,7 @@ func createNCctx(keeper *keeper.Keeper, ctx sdk.Context, n int) []types.CrossCha OutboundTxGasPrice: fmt.Sprintf("%d", i), OutboundTxBallotIndex: fmt.Sprintf("%d", i), OutboundTxObservedExternalHeight: uint64(i), - CoinType: 0, + CoinType: coin.CoinType_Zeta, }} items[i].CctxStatus = &types.Status{ Status: types.CctxStatus_PendingInbound, @@ -73,6 +73,7 @@ func createNCctx(keeper *keeper.Keeper, ctx sdk.Context, n int) []types.CrossCha items[i].ZetaFees = math.OneUint() items[i].Index = fmt.Sprintf("%d", i) + keeper.SetCctxAndNonceToCctxAndInTxHashToCctx(ctx, items[i]) } return items diff --git a/x/crosschain/keeper/cctx_utils_test.go b/x/crosschain/keeper/cctx_utils_test.go index c637cc4cc9..888b3bf445 100644 --- a/x/crosschain/keeper/cctx_utils_test.go +++ b/x/crosschain/keeper/cctx_utils_test.go @@ -1,6 +1,7 @@ package keeper_test import ( + "fmt" "math/big" "testing" @@ -46,6 +47,7 @@ func TestGetRevertGasLimit(t *testing.T) { require.NoError(t, err) gasLimit, err := k.GetRevertGasLimit(ctx, types.CrossChainTx{ + InboundTxParams: &types.InboundTxParams{ CoinType: coin.CoinType_Gas, SenderChainId: chainID, @@ -76,6 +78,7 @@ func TestGetRevertGasLimit(t *testing.T) { require.NoError(t, err) gasLimit, err := k.GetRevertGasLimit(ctx, types.CrossChainTx{ + InboundTxParams: &types.InboundTxParams{ CoinType: coin.CoinType_ERC20, SenderChainId: chainID, @@ -89,6 +92,7 @@ func TestGetRevertGasLimit(t *testing.T) { k, ctx, _, _ := keepertest.CrosschainKeeper(t) _, err := k.GetRevertGasLimit(ctx, types.CrossChainTx{ + InboundTxParams: &types.InboundTxParams{ CoinType: coin.CoinType_Gas, SenderChainId: 999999, @@ -110,6 +114,7 @@ func TestGetRevertGasLimit(t *testing.T) { // no contract deployed therefore will fail _, err := k.GetRevertGasLimit(ctx, types.CrossChainTx{ + InboundTxParams: &types.InboundTxParams{ CoinType: coin.CoinType_Gas, SenderChainId: chainID, @@ -121,6 +126,7 @@ func TestGetRevertGasLimit(t *testing.T) { k, ctx, _, _ := keepertest.CrosschainKeeper(t) _, err := k.GetRevertGasLimit(ctx, types.CrossChainTx{ + InboundTxParams: &types.InboundTxParams{ CoinType: coin.CoinType_ERC20, SenderChainId: 999999, @@ -191,3 +197,22 @@ func TestGetAbortedAmount(t *testing.T) { require.Equal(t, sdkmath.ZeroUint(), a) }) } + +func Test_IsPending(t *testing.T) { + tt := []struct { + status types.CctxStatus + expected bool + }{ + {types.CctxStatus_PendingInbound, false}, + {types.CctxStatus_PendingOutbound, true}, + {types.CctxStatus_PendingRevert, true}, + {types.CctxStatus_Reverted, false}, + {types.CctxStatus_Aborted, false}, + {types.CctxStatus_OutboundMined, false}, + } + for _, tc := range tt { + t.Run(fmt.Sprintf("status %s", tc.status), func(t *testing.T) { + require.Equal(t, tc.expected, crosschainkeeper.IsPending(types.CrossChainTx{CctxStatus: &types.Status{Status: tc.status}})) + }) + } +} diff --git a/x/crosschain/keeper/events.go b/x/crosschain/keeper/events.go index 6943d77723..766631f156 100644 --- a/x/crosschain/keeper/events.go +++ b/x/crosschain/keeper/events.go @@ -59,11 +59,11 @@ func EmitZetaWithdrawCreated(ctx sdk.Context, cctx types.CrossChainTx) { } -func EmitOutboundSuccess(ctx sdk.Context, msg *types.MsgVoteOnObservedOutboundTx, oldStatus string, newStatus string, cctx types.CrossChainTx) { +func EmitOutboundSuccess(ctx sdk.Context, valueReceived string, oldStatus string, newStatus string, cctxIndex string) { err := ctx.EventManager().EmitTypedEvents(&types.EventOutboundSuccess{ MsgTypeUrl: sdk.MsgTypeURL(&types.MsgVoteOnObservedOutboundTx{}), - CctxIndex: cctx.Index, - ValueReceived: msg.ValueReceived.String(), + CctxIndex: cctxIndex, + ValueReceived: valueReceived, OldStatus: oldStatus, NewStatus: newStatus, }) @@ -73,11 +73,11 @@ func EmitOutboundSuccess(ctx sdk.Context, msg *types.MsgVoteOnObservedOutboundTx } -func EmitOutboundFailure(ctx sdk.Context, msg *types.MsgVoteOnObservedOutboundTx, oldStatus string, newStatus string, cctx types.CrossChainTx) { +func EmitOutboundFailure(ctx sdk.Context, valueReceived string, oldStatus string, newStatus string, cctxIndex string) { err := ctx.EventManager().EmitTypedEvents(&types.EventOutboundFailure{ MsgTypeUrl: sdk.MsgTypeURL(&types.MsgVoteOnObservedOutboundTx{}), - CctxIndex: cctx.Index, - ValueReceived: msg.ValueReceived.String(), + CctxIndex: cctxIndex, + ValueReceived: valueReceived, OldStatus: oldStatus, NewStatus: newStatus, }) diff --git a/x/crosschain/keeper/evm_deposit.go b/x/crosschain/keeper/evm_deposit.go index 96be91a5d4..0ce56a829c 100644 --- a/x/crosschain/keeper/evm_deposit.go +++ b/x/crosschain/keeper/evm_deposit.go @@ -19,14 +19,13 @@ import ( // HandleEVMDeposit handles a deposit from an inbound tx // returns (isContractReverted, err) // (true, non-nil) means CallEVM() reverted -func (k Keeper) HandleEVMDeposit( - ctx sdk.Context, - cctx *types.CrossChainTx, - msg types.MsgVoteOnObservedInboundTx, - senderChainID int64, -) (bool, error) { - to := ethcommon.HexToAddress(msg.Receiver) +func (k Keeper) HandleEVMDeposit(ctx sdk.Context, cctx *types.CrossChainTx) (bool, error) { + to := ethcommon.HexToAddress(cctx.GetCurrentOutTxParam().Receiver) var ethTxHash ethcommon.Hash + inboundAmount := cctx.GetInboundTxParams().Amount.BigInt() + inboundSender := cctx.GetInboundTxParams().Sender + inboundSenderChainID := cctx.GetInboundTxParams().SenderChainId + inboundCoinType := cctx.InboundTxParams.CoinType if len(ctx.TxBytes()) > 0 { // add event for tendermint transaction hash format hash := tmbytes.HexBytes(tmtypes.Tx(ctx.TxBytes()).Hash()) @@ -36,15 +35,15 @@ func (k Keeper) HandleEVMDeposit( cctx.GetCurrentOutTxParam().OutboundTxObservedExternalHeight = uint64(ctx.BlockHeight()) } - if msg.CoinType == coin.CoinType_Zeta { + if inboundCoinType == coin.CoinType_Zeta { // if coin type is Zeta, this is a deposit ZETA to zEVM cctx. - err := k.fungibleKeeper.DepositCoinZeta(ctx, to, msg.Amount.BigInt()) + err := k.fungibleKeeper.DepositCoinZeta(ctx, to, inboundAmount) if err != nil { return false, err } } else { // cointype is Gas or ERC20; then it could be a ZRC20 deposit/depositAndCall cctx. - parsedAddress, data, err := chains.ParseAddressAndData(msg.Message) + parsedAddress, data, err := chains.ParseAddressAndData(cctx.RelayedMessage) if err != nil { return false, errors.Wrap(types.ErrUnableToParseAddress, err.Error()) } @@ -52,7 +51,7 @@ func (k Keeper) HandleEVMDeposit( to = parsedAddress } - from, err := chains.DecodeAddressFromChainID(senderChainID, msg.Sender) + from, err := chains.DecodeAddressFromChainID(inboundSenderChainID, inboundSender) if err != nil { return false, fmt.Errorf("HandleEVMDeposit: unable to decode address: %s", err.Error()) } @@ -61,11 +60,11 @@ func (k Keeper) HandleEVMDeposit( ctx, from, to, - msg.Amount.BigInt(), - senderChainID, + inboundAmount, + inboundSenderChainID, data, - msg.CoinType, - msg.Asset, + inboundCoinType, + cctx.InboundTxParams.Asset, ) if fungibletypes.IsContractReverted(evmTxResponse, err) || errShouldRevertCctx(err) { return true, err @@ -79,9 +78,9 @@ func (k Keeper) HandleEVMDeposit( logs := evmtypes.LogsToEthereum(evmTxResponse.Logs) if len(logs) > 0 { ctx = ctx.WithValue("inCctxIndex", cctx.Index) - txOrigin := msg.TxOrigin + txOrigin := cctx.InboundTxParams.TxOrigin if txOrigin == "" { - txOrigin = msg.Sender + txOrigin = inboundSender } err = k.ProcessLogs(ctx, logs, to, txOrigin) diff --git a/x/crosschain/keeper/evm_deposit_test.go b/x/crosschain/keeper/evm_deposit_test.go index 15ca00a62a..bcb7b684e8 100644 --- a/x/crosschain/keeper/evm_deposit_test.go +++ b/x/crosschain/keeper/evm_deposit_test.go @@ -31,15 +31,14 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { fungibleMock.On("DepositCoinZeta", ctx, receiver, amount).Return(nil) // call HandleEVMDeposit + cctx := sample.CrossChainTx(t, "foo") + cctx.GetCurrentOutTxParam().Receiver = receiver.String() + cctx.GetInboundTxParams().Amount = math.NewUintFromBigInt(amount) + cctx.GetInboundTxParams().CoinType = coin.CoinType_Zeta + cctx.GetInboundTxParams().SenderChainId = 0 reverted, err := k.HandleEVMDeposit( ctx, - sample.CrossChainTx(t, "foo"), - types.MsgVoteOnObservedInboundTx{ - Receiver: receiver.String(), - Amount: math.NewUintFromBigInt(amount), - CoinType: coin.CoinType_Zeta, - }, - 0, + cctx, ) require.NoError(t, err) require.False(t, reverted) @@ -60,15 +59,15 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { fungibleMock.On("DepositCoinZeta", ctx, receiver, amount).Return(errDeposit) // call HandleEVMDeposit + + cctx := sample.CrossChainTx(t, "foo") + cctx.GetCurrentOutTxParam().Receiver = receiver.String() + cctx.GetInboundTxParams().Amount = math.NewUintFromBigInt(amount) + cctx.GetInboundTxParams().CoinType = coin.CoinType_Zeta + cctx.GetInboundTxParams().SenderChainId = 0 reverted, err := k.HandleEVMDeposit( ctx, - sample.CrossChainTx(t, "foo"), - types.MsgVoteOnObservedInboundTx{ - Receiver: receiver.String(), - Amount: math.NewUintFromBigInt(amount), - CoinType: coin.CoinType_Zeta, - }, - 0, + cctx, ) require.ErrorIs(t, err, errDeposit) require.False(t, reverted) @@ -101,18 +100,17 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { ).Return(&evmtypes.MsgEthereumTxResponse{}, false, nil) // call HandleEVMDeposit + cctx := sample.CrossChainTx(t, "foo") + cctx.GetCurrentOutTxParam().Receiver = receiver.String() + cctx.GetInboundTxParams().Amount = math.NewUintFromBigInt(amount) + cctx.GetInboundTxParams().CoinType = coin.CoinType_ERC20 + cctx.GetInboundTxParams().Sender = sample.EthAddress().String() + cctx.GetInboundTxParams().SenderChainId = senderChain + cctx.RelayedMessage = "" + cctx.GetInboundTxParams().Asset = "" reverted, err := k.HandleEVMDeposit( ctx, - sample.CrossChainTx(t, "foo"), - types.MsgVoteOnObservedInboundTx{ - Sender: sample.EthAddress().String(), - Receiver: receiver.String(), - Amount: math.NewUintFromBigInt(amount), - CoinType: coin.CoinType_ERC20, - Message: "", - Asset: "", - }, - senderChain, + cctx, ) require.NoError(t, err) require.False(t, reverted) @@ -146,18 +144,17 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { ).Return(&evmtypes.MsgEthereumTxResponse{}, false, errDeposit) // call HandleEVMDeposit + cctx := sample.CrossChainTx(t, "foo") + cctx.GetCurrentOutTxParam().Receiver = receiver.String() + cctx.GetInboundTxParams().Amount = math.NewUintFromBigInt(amount) + cctx.GetInboundTxParams().CoinType = coin.CoinType_ERC20 + cctx.GetInboundTxParams().Sender = sample.EthAddress().String() + cctx.GetInboundTxParams().SenderChainId = senderChain + cctx.RelayedMessage = "" + cctx.GetInboundTxParams().Asset = "" reverted, err := k.HandleEVMDeposit( ctx, - sample.CrossChainTx(t, "foo"), - types.MsgVoteOnObservedInboundTx{ - Sender: sample.EthAddress().String(), - Receiver: receiver.String(), - Amount: math.NewUintFromBigInt(amount), - CoinType: coin.CoinType_ERC20, - Message: "", - Asset: "", - }, - senderChain, + cctx, ) require.ErrorIs(t, err, errDeposit) require.False(t, reverted) @@ -191,18 +188,17 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { ).Return(&evmtypes.MsgEthereumTxResponse{VmError: "reverted"}, false, errDeposit) // call HandleEVMDeposit + cctx := sample.CrossChainTx(t, "foo") + cctx.GetCurrentOutTxParam().Receiver = receiver.String() + cctx.GetInboundTxParams().Amount = math.NewUintFromBigInt(amount) + cctx.InboundTxParams.CoinType = coin.CoinType_ERC20 + cctx.GetInboundTxParams().Sender = sample.EthAddress().String() + cctx.GetInboundTxParams().SenderChainId = senderChain + cctx.RelayedMessage = "" + cctx.GetInboundTxParams().Asset = "" reverted, err := k.HandleEVMDeposit( ctx, - sample.CrossChainTx(t, "foo"), - types.MsgVoteOnObservedInboundTx{ - Sender: sample.EthAddress().String(), - Receiver: receiver.String(), - Amount: math.NewUintFromBigInt(amount), - CoinType: coin.CoinType_ERC20, - Message: "", - Asset: "", - }, - senderChain, + cctx, ) require.ErrorIs(t, err, errDeposit) require.True(t, reverted) @@ -235,18 +231,17 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { ).Return(&evmtypes.MsgEthereumTxResponse{}, false, fungibletypes.ErrForeignCoinCapReached) // call HandleEVMDeposit + cctx := sample.CrossChainTx(t, "foo") + cctx.GetCurrentOutTxParam().Receiver = receiver.String() + cctx.GetInboundTxParams().Amount = math.NewUintFromBigInt(amount) + cctx.GetInboundTxParams().CoinType = coin.CoinType_ERC20 + cctx.GetInboundTxParams().Sender = sample.EthAddress().String() + cctx.GetInboundTxParams().SenderChainId = senderChain + cctx.RelayedMessage = "" + cctx.GetInboundTxParams().Asset = "" reverted, err := k.HandleEVMDeposit( ctx, - sample.CrossChainTx(t, "foo"), - types.MsgVoteOnObservedInboundTx{ - Sender: sample.EthAddress().String(), - Receiver: receiver.String(), - Amount: math.NewUintFromBigInt(amount), - CoinType: coin.CoinType_ERC20, - Message: "", - Asset: "", - }, - senderChain, + cctx, ) require.ErrorIs(t, err, fungibletypes.ErrForeignCoinCapReached) require.True(t, reverted) @@ -279,18 +274,17 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { ).Return(&evmtypes.MsgEthereumTxResponse{}, false, fungibletypes.ErrPausedZRC20) // call HandleEVMDeposit + cctx := sample.CrossChainTx(t, "foo") + cctx.GetCurrentOutTxParam().Receiver = receiver.String() + cctx.GetInboundTxParams().Amount = math.NewUintFromBigInt(amount) + cctx.GetInboundTxParams().CoinType = coin.CoinType_ERC20 + cctx.GetInboundTxParams().Sender = sample.EthAddress().String() + cctx.GetInboundTxParams().SenderChainId = senderChain + cctx.RelayedMessage = "" + cctx.GetInboundTxParams().Asset = "" reverted, err := k.HandleEVMDeposit( ctx, - sample.CrossChainTx(t, "foo"), - types.MsgVoteOnObservedInboundTx{ - Sender: sample.EthAddress().String(), - Receiver: receiver.String(), - Amount: math.NewUintFromBigInt(amount), - CoinType: coin.CoinType_ERC20, - Message: "", - Asset: "", - }, - senderChain, + cctx, ) require.ErrorIs(t, err, fungibletypes.ErrPausedZRC20) require.True(t, reverted) @@ -321,18 +315,17 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { ).Return(&evmtypes.MsgEthereumTxResponse{}, false, fungibletypes.ErrCallNonContract) // call HandleEVMDeposit + cctx := sample.CrossChainTx(t, "foo") + cctx.GetCurrentOutTxParam().Receiver = receiver.String() + cctx.GetInboundTxParams().Amount = math.NewUintFromBigInt(amount) + cctx.GetInboundTxParams().CoinType = coin.CoinType_ERC20 + cctx.GetInboundTxParams().Sender = sample.EthAddress().String() + cctx.GetInboundTxParams().SenderChainId = senderChain + cctx.RelayedMessage = "" + cctx.GetInboundTxParams().Asset = "" reverted, err := k.HandleEVMDeposit( ctx, - sample.CrossChainTx(t, "foo"), - types.MsgVoteOnObservedInboundTx{ - Sender: sample.EthAddress().String(), - Receiver: receiver.String(), - Amount: math.NewUintFromBigInt(amount), - CoinType: coin.CoinType_ERC20, - Message: "", - Asset: "", - }, - senderChain, + cctx, ) require.ErrorIs(t, err, fungibletypes.ErrCallNonContract) require.True(t, reverted) @@ -345,18 +338,17 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { }) senderChain := getValidEthChainID(t) + cctx := sample.CrossChainTx(t, "foo") + cctx.GetCurrentOutTxParam().Receiver = sample.EthAddress().String() + cctx.GetInboundTxParams().Amount = math.NewUint(42) + cctx.GetInboundTxParams().CoinType = coin.CoinType_Gas + cctx.GetInboundTxParams().Sender = sample.EthAddress().String() + cctx.GetInboundTxParams().SenderChainId = senderChain + cctx.RelayedMessage = "not_hex" + cctx.GetInboundTxParams().Asset = "" _, err := k.HandleEVMDeposit( ctx, - sample.CrossChainTx(t, "foo"), - types.MsgVoteOnObservedInboundTx{ - Sender: sample.EthAddress().String(), - Receiver: sample.EthAddress().String(), - Amount: math.NewUint(42), - CoinType: coin.CoinType_Gas, - Message: "not_hex", - Asset: "", - }, - senderChain, + cctx, ) require.ErrorIs(t, err, types.ErrUnableToParseAddress) }) @@ -386,18 +378,17 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { mock.Anything, ).Return(&evmtypes.MsgEthereumTxResponse{}, false, nil) + cctx := sample.CrossChainTx(t, "foo") + cctx.GetCurrentOutTxParam().Receiver = sample.EthAddress().String() + cctx.GetInboundTxParams().Amount = math.NewUintFromBigInt(amount) + cctx.GetInboundTxParams().CoinType = coin.CoinType_ERC20 + cctx.GetInboundTxParams().Sender = sample.EthAddress().String() + cctx.GetInboundTxParams().SenderChainId = senderChain + cctx.RelayedMessage = receiver.Hex()[2:] + "DEADBEEF" + cctx.GetInboundTxParams().Asset = "" reverted, err := k.HandleEVMDeposit( ctx, - sample.CrossChainTx(t, "foo"), - types.MsgVoteOnObservedInboundTx{ - Sender: sample.EthAddress().String(), - Receiver: sample.EthAddress().String(), - Amount: math.NewUintFromBigInt(amount), - CoinType: coin.CoinType_ERC20, - Message: receiver.Hex()[2:] + "DEADBEEF", - Asset: "", - }, - senderChain, + cctx, ) require.NoError(t, err) require.False(t, reverted) @@ -429,18 +420,17 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { mock.Anything, ).Return(&evmtypes.MsgEthereumTxResponse{}, false, nil) + cctx := sample.CrossChainTx(t, "foo") + cctx.GetCurrentOutTxParam().Receiver = receiver.String() + cctx.GetInboundTxParams().Amount = math.NewUintFromBigInt(amount) + cctx.GetInboundTxParams().CoinType = coin.CoinType_ERC20 + cctx.GetInboundTxParams().Sender = sample.EthAddress().String() + cctx.GetInboundTxParams().SenderChainId = senderChain + cctx.RelayedMessage = "DEADBEEF" + cctx.GetInboundTxParams().Asset = "" reverted, err := k.HandleEVMDeposit( ctx, - sample.CrossChainTx(t, "foo"), - types.MsgVoteOnObservedInboundTx{ - Sender: sample.EthAddress().String(), - Receiver: receiver.String(), - Amount: math.NewUintFromBigInt(amount), - CoinType: coin.CoinType_ERC20, - Message: "DEADBEEF", - Asset: "", - }, - senderChain, + cctx, ) require.NoError(t, err) require.False(t, reverted) diff --git a/x/crosschain/keeper/evm_hooks.go b/x/crosschain/keeper/evm_hooks.go index e5d85aba2a..a662352532 100644 --- a/x/crosschain/keeper/evm_hooks.go +++ b/x/crosschain/keeper/evm_hooks.go @@ -169,18 +169,13 @@ func (k Keeper) ProcessZRC20WithdrawalEvent(ctx sdk.Context, event *zrc20.ZRC20W foreignCoin.Asset, event.Raw.Index, ) - sendHash := msg.Digest() - - cctx := k.CreateNewCCTX( - ctx, - msg, - sendHash, - tss.TssPubkey, - types.CctxStatus_PendingOutbound, - senderChain.ChainId, - receiverChain.ChainId, - ) + // Create a new cctx with status as pending Inbound, this is created directly from the event without waiting for any observer votes + cctx, err := types.NewCCTX(ctx, *msg, tss.TssPubkey) + if err != nil { + return fmt.Errorf("ProcessZRC20WithdrawalEvent: failed to initialize cctx: %s", err.Error()) + } + cctx.SetPendingOutbound("ZRC20 withdrawal event setting to pending outbound directly") // Get gas price and amount gasprice, found := k.GetGasPrice(ctx, receiverChain.ChainId) if !found { @@ -206,7 +201,7 @@ func (k Keeper) ProcessZetaSentEvent(ctx sdk.Context, event *connectorzevm.ZetaC fungibletypes.ModuleName, sdk.NewCoins(sdk.NewCoin(config.BaseDenom, sdk.NewIntFromBigInt(event.ZetaValueAndGas))), ); err != nil { - fmt.Printf("burn coins failed: %s\n", err.Error()) + ctx.Logger().Error(fmt.Sprintf("ProcessZetaSentEvent: failed to burn coins from fungible: %s", err.Error())) return fmt.Errorf("ProcessZetaSentEvent: failed to burn coins from fungible: %s", err.Error()) } @@ -247,18 +242,14 @@ func (k Keeper) ProcessZetaSentEvent(ctx sdk.Context, event *connectorzevm.ZetaC "", event.Raw.Index, ) - sendHash := msg.Digest() - // Create the CCTX - cctx := k.CreateNewCCTX( - ctx, - msg, - sendHash, - tss.TssPubkey, - types.CctxStatus_PendingOutbound, - senderChain.ChainId, - receiverChain.ChainId, - ) + // create a new cctx with status as pending Inbound, + // this is created directly from the event without waiting for any observer votes + cctx, err := types.NewCCTX(ctx, *msg, tss.TssPubkey) + if err != nil { + return fmt.Errorf("ProcessZetaSentEvent: failed to initialize cctx: %s", err.Error()) + } + cctx.SetPendingOutbound("ZetaSent event setting to pending outbound directly") if err := k.PayGasAndUpdateCctx( ctx, diff --git a/x/crosschain/keeper/evm_hooks_test.go b/x/crosschain/keeper/evm_hooks_test.go index 85676457cf..c75f638d3f 100644 --- a/x/crosschain/keeper/evm_hooks_test.go +++ b/x/crosschain/keeper/evm_hooks_test.go @@ -22,7 +22,11 @@ import ( observertypes "github.com/zeta-chain/zetacore/x/observer/types" ) -func SetupStateForProcessLogsZetaSent(t *testing.T, ctx sdk.Context, k *crosschainkeeper.Keeper, zk keepertest.ZetaKeepers, sdkk keepertest.SDKKeepers, chain chains.Chain) { +// SetupStateForProcessLogsZetaSent sets up additional state required for processing logs for ZetaSent events +// This sets up the gas coin, zrc20 contract, gas price, zrc20 pool. +// This should be used in conjunction with SetupStateForProcessLogs for processing ZetaSent events +func SetupStateForProcessLogsZetaSent(t *testing.T, ctx sdk.Context, k *crosschainkeeper.Keeper, zk keepertest.ZetaKeepers, sdkk keepertest.SDKKeepers, chain chains.Chain, admin string) { + assetAddress := sample.EthAddress().String() gasZRC20 := setupGasCoin(t, ctx, zk.FungibleKeeper, sdkk.EvmKeeper, chain.ChainId, "ethereum", "ETH") zrc20Addr := deployZRC20( @@ -38,6 +42,8 @@ func SetupStateForProcessLogsZetaSent(t *testing.T, ctx sdk.Context, k *crosscha _, err := zk.FungibleKeeper.UpdateZRC20ProtocolFlatFee(ctx, gasZRC20, big.NewInt(withdrawFee)) require.NoError(t, err) + _, err = zk.FungibleKeeper.UpdateZRC20ProtocolFlatFee(ctx, zrc20Addr, big.NewInt(withdrawFee)) + require.NoError(t, err) k.SetGasPrice(ctx, crosschaintypes.GasPrice{ ChainId: chain.ChainId, @@ -53,6 +59,8 @@ func SetupStateForProcessLogsZetaSent(t *testing.T, ctx sdk.Context, k *crosscha ) } +// SetupStateForProcessLogs sets up observer state for required for processing logs +// It deploys system contracts, sets up TSS, gas price, chain nonce's, pending nonce's.These are all required to create a cctx from a log func SetupStateForProcessLogs(t *testing.T, ctx sdk.Context, k *crosschainkeeper.Keeper, zk keepertest.ZetaKeepers, sdkk keepertest.SDKKeepers, chain chains.Chain) { deploySystemContracts(t, ctx, zk.FungibleKeeper, sdkk.EvmKeeper) @@ -428,6 +436,7 @@ func TestKeeper_ParseZetaSentEvent(t *testing.T) { func TestKeeper_ProcessZetaSentEvent(t *testing.T) { t.Run("successfully process ZetaSentEvent", func(t *testing.T) { k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) + k.GetAuthKeeper().GetModuleAccount(ctx, fungibletypes.ModuleName) chain := chains.EthChain() @@ -435,7 +444,9 @@ func TestKeeper_ProcessZetaSentEvent(t *testing.T) { setSupportedChain(ctx, zk, chainID) SetupStateForProcessLogs(t, ctx, k, zk, sdkk, chain) - SetupStateForProcessLogsZetaSent(t, ctx, k, zk, sdkk, chain) + admin := keepertest.SetAdminPolices(ctx, zk.AuthorityKeeper) + SetupStateForProcessLogsZetaSent(t, ctx, k, zk, sdkk, chain, admin) + amount, ok := sdkmath.NewIntFromString("20000000000000000000000") require.True(t, ok) err := sdkk.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sdk.NewCoins(sdk.NewCoin(config.BaseDenom, amount))) @@ -465,7 +476,8 @@ func TestKeeper_ProcessZetaSentEvent(t *testing.T) { chainID := chain.ChainId setSupportedChain(ctx, zk, chainID) SetupStateForProcessLogs(t, ctx, k, zk, sdkk, chain) - SetupStateForProcessLogsZetaSent(t, ctx, k, zk, sdkk, chain) + admin := keepertest.SetAdminPolices(ctx, zk.AuthorityKeeper) + SetupStateForProcessLogsZetaSent(t, ctx, k, zk, sdkk, chain, admin) event, err := crosschainkeeper.ParseZetaSentEvent(*sample.GetValidZetaSentDestinationExternal(t).Logs[4], sample.GetValidZetaSentDestinationExternal(t).Logs[4].Address) require.NoError(t, err) @@ -483,7 +495,8 @@ func TestKeeper_ProcessZetaSentEvent(t *testing.T) { chain := chains.EthChain() SetupStateForProcessLogs(t, ctx, k, zk, sdkk, chain) - SetupStateForProcessLogsZetaSent(t, ctx, k, zk, sdkk, chain) + admin := keepertest.SetAdminPolices(ctx, zk.AuthorityKeeper) + SetupStateForProcessLogsZetaSent(t, ctx, k, zk, sdkk, chain, admin) amount, ok := sdkmath.NewIntFromString("20000000000000000000000") require.True(t, ok) @@ -507,7 +520,8 @@ func TestKeeper_ProcessZetaSentEvent(t *testing.T) { chainID := chain.ChainId setSupportedChain(ctx, zk, chainID) SetupStateForProcessLogs(t, ctx, k, zk, sdkk, chain) - SetupStateForProcessLogsZetaSent(t, ctx, k, zk, sdkk, chain) + admin := keepertest.SetAdminPolices(ctx, zk.AuthorityKeeper) + SetupStateForProcessLogsZetaSent(t, ctx, k, zk, sdkk, chain, admin) amount, ok := sdkmath.NewIntFromString("20000000000000000000000") require.True(t, ok) @@ -556,7 +570,8 @@ func TestKeeper_ProcessZetaSentEvent(t *testing.T) { setSupportedChain(ctx, zk, chainID) SetupStateForProcessLogs(t, ctx, k, zk, sdkk, chain) - SetupStateForProcessLogsZetaSent(t, ctx, k, zk, sdkk, chain) + admin := keepertest.SetAdminPolices(ctx, zk.AuthorityKeeper) + SetupStateForProcessLogsZetaSent(t, ctx, k, zk, sdkk, chain, admin) zk.ObserverKeeper.SetChainNonces(ctx, observertypes.ChainNonces{ Index: chain.ChainName.String(), @@ -613,7 +628,8 @@ func TestKeeper_ProcessLogs(t *testing.T) { chainID := chain.ChainId setSupportedChain(ctx, zk, chainID) SetupStateForProcessLogs(t, ctx, k, zk, sdkk, chain) - SetupStateForProcessLogsZetaSent(t, ctx, k, zk, sdkk, chain) + admin := keepertest.SetAdminPolices(ctx, zk.AuthorityKeeper) + SetupStateForProcessLogsZetaSent(t, ctx, k, zk, sdkk, chain, admin) amount, ok := sdkmath.NewIntFromString("20000000000000000000000") require.True(t, ok) diff --git a/x/crosschain/keeper/gas_payment.go b/x/crosschain/keeper/gas_payment.go index 6a9b957f16..fe51187199 100644 --- a/x/crosschain/keeper/gas_payment.go +++ b/x/crosschain/keeper/gas_payment.go @@ -14,7 +14,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/x/crosschain/types" fungibletypes "github.com/zeta-chain/zetacore/x/fungible/types" - zetaObserverTypes "github.com/zeta-chain/zetacore/x/observer/types" + observertypes "github.com/zeta-chain/zetacore/x/observer/types" ) // PayGasAndUpdateCctx updates the outbound tx with the new amount after paying the gas fee @@ -94,7 +94,7 @@ func (k Keeper) PayGasNativeAndUpdateCctx( return cosmoserrors.Wrapf(types.ErrInvalidCoinType, "can't pay gas in native gas with %s", cctx.InboundTxParams.CoinType.String()) } if chain := k.zetaObserverKeeper.GetSupportedChainFromChainID(ctx, chainID); chain == nil { - return zetaObserverTypes.ErrSupportedChains + return observertypes.ErrSupportedChains } // get gas params @@ -142,16 +142,14 @@ func (k Keeper) PayGasInERC20AndUpdateCctx( } if chain := k.zetaObserverKeeper.GetSupportedChainFromChainID(ctx, chainID); chain == nil { - return zetaObserverTypes.ErrSupportedChains + return observertypes.ErrSupportedChains } - // get gas params gasZRC20, gasLimit, gasPrice, protocolFlatFee, err := k.ChainGasParams(ctx, chainID) if err != nil { return cosmoserrors.Wrap(types.ErrCannotFindGasParams, err.Error()) } outTxGasFee := gasLimit.Mul(gasPrice).Add(protocolFlatFee) - // get address of the zrc20 fc, found := k.fungibleKeeper.GetForeignCoinFromAsset(ctx, cctx.InboundTxParams.Asset, chainID) if !found { @@ -269,7 +267,7 @@ func (k Keeper) PayGasInZetaAndUpdateCctx( } if chain := k.zetaObserverKeeper.GetSupportedChainFromChainID(ctx, chainID); chain == nil { - return zetaObserverTypes.ErrSupportedChains + return observertypes.ErrSupportedChains } gasZRC20, err := k.fungibleKeeper.QuerySystemContractGasCoinZRC20(ctx, big.NewInt(chainID)) diff --git a/x/crosschain/keeper/gas_payment_test.go b/x/crosschain/keeper/gas_payment_test.go index 1ccb31c937..569a360327 100644 --- a/x/crosschain/keeper/gas_payment_test.go +++ b/x/crosschain/keeper/gas_payment_test.go @@ -53,6 +53,7 @@ func TestKeeper_PayGasNativeAndUpdateCctx(t *testing.T) { OutboundTxParams: []*types.OutboundTxParams{ { ReceiverChainId: chains.ZetaPrivnetChain().ChainId, + CoinType: coin.CoinType_Gas, }, { ReceiverChainId: chainID, @@ -142,7 +143,9 @@ func TestKeeper_PayGasNativeAndUpdateCctx(t *testing.T) { cctx := types.CrossChainTx{ InboundTxParams: &types.InboundTxParams{ - CoinType: coin.CoinType_Gas, + SenderChainId: chainID, + Sender: sample.EthAddress().String(), + CoinType: coin.CoinType_Gas, }, OutboundTxParams: []*types.OutboundTxParams{ { @@ -202,9 +205,10 @@ func TestKeeper_PayGasInERC20AndUpdateCctx(t *testing.T) { // create a cctx reverted from zeta cctx := types.CrossChainTx{ + InboundTxParams: &types.InboundTxParams{ - CoinType: coin.CoinType_ERC20, Asset: assetAddress, + CoinType: coin.CoinType_ERC20, }, OutboundTxParams: []*types.OutboundTxParams{ { @@ -359,8 +363,8 @@ func TestKeeper_PayGasInERC20AndUpdateCctx(t *testing.T) { // create a cctx reverted from zeta cctx := types.CrossChainTx{ InboundTxParams: &types.InboundTxParams{ - CoinType: coin.CoinType_ERC20, Asset: assetAddress, + CoinType: coin.CoinType_ERC20, }, OutboundTxParams: []*types.OutboundTxParams{ { @@ -546,7 +550,9 @@ func TestKeeper_PayGasInZetaAndUpdateCctx(t *testing.T) { // create a cctx reverted from zeta cctx := types.CrossChainTx{ InboundTxParams: &types.InboundTxParams{ - CoinType: coin.CoinType_Zeta, + SenderChainId: chainID, + Sender: sample.EthAddress().String(), + CoinType: coin.CoinType_Zeta, }, OutboundTxParams: []*types.OutboundTxParams{ { @@ -578,7 +584,9 @@ func TestKeeper_PayGasInZetaAndUpdateCctx(t *testing.T) { // create a cctx reverted from zeta cctx := types.CrossChainTx{ InboundTxParams: &types.InboundTxParams{ - CoinType: coin.CoinType_Zeta, + SenderChainId: chainID, + Sender: sample.EthAddress().String(), + CoinType: coin.CoinType_Zeta, }, OutboundTxParams: []*types.OutboundTxParams{ { diff --git a/x/crosschain/keeper/msg_server_migrate_tss_funds.go b/x/crosschain/keeper/msg_server_migrate_tss_funds.go index 02ce1bf879..7fd1228400 100644 --- a/x/crosschain/keeper/msg_server_migrate_tss_funds.go +++ b/x/crosschain/keeper/msg_server_migrate_tss_funds.go @@ -82,6 +82,9 @@ func (k Keeper) MigrateTSSFundsForChain(ctx sdk.Context, chainID int64, amount s hash := crypto.Keccak256Hash([]byte(indexString)) index := hash.Hex() + + // TODO : Use the `NewCCTX` method to create the cctx + // https://github.com/zeta-chain/node/issues/1909 cctx := types.CrossChainTx{ Creator: "", Index: index, diff --git a/x/crosschain/keeper/msg_server_vote_inbound_tx.go b/x/crosschain/keeper/msg_server_vote_inbound_tx.go index 4fee3a2c9d..5d36965647 100644 --- a/x/crosschain/keeper/msg_server_vote_inbound_tx.go +++ b/x/crosschain/keeper/msg_server_vote_inbound_tx.go @@ -6,7 +6,6 @@ import ( cosmoserrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/x/crosschain/types" ) @@ -85,127 +84,44 @@ func (k msgServer) VoteOnObservedInboundTx(goCtx context.Context, msg *types.Msg } } commit() - // If the ballot is not finalized return nil here to add vote to commit state if !finalized { return &types.MsgVoteOnObservedInboundTxResponse{}, nil } - - // get the latest TSS to set the TSS public key in the CCTX - tssPub := "" tss, tssFound := k.zetaObserverKeeper.GetTSS(ctx) - if tssFound { - tssPub = tss.TssPubkey + if !tssFound { + return nil, types.ErrCannotFindTSSKeys } - - // create the CCTX - cctx := k.CreateNewCCTX( - ctx, - msg, - index, - tssPub, - types.CctxStatus_PendingInbound, - msg.SenderChainId, - msg.ReceiverChain, - ) - - defer func() { - EmitEventInboundFinalized(ctx, &cctx) - k.AddFinalizedInbound(ctx, msg.InTxHash, msg.SenderChainId, msg.EventIndex) - // #nosec G701 always positive - cctx.InboundTxParams.InboundTxFinalizedZetaHeight = uint64(ctx.BlockHeight()) - cctx.InboundTxParams.TxFinalizationStatus = types.TxFinalizationStatus_Executed - k.RemoveInTxTrackerIfExists(ctx, cctx.InboundTxParams.SenderChainId, cctx.InboundTxParams.InboundTxObservedHash) - k.SetCctxAndNonceToCctxAndInTxHashToCctx(ctx, cctx) - }() - - // FinalizeInbound updates CCTX Prices and Nonce - // Aborts is any of the updates fail - if chains.IsZetaChain(msg.ReceiverChain) { - tmpCtx, commit := ctx.CacheContext() - isContractReverted, err := k.HandleEVMDeposit(tmpCtx, &cctx, *msg, msg.SenderChainId) - - if err != nil && !isContractReverted { // exceptional case; internal error; should abort CCTX - cctx.CctxStatus.ChangeStatus(types.CctxStatus_Aborted, err.Error()) - return &types.MsgVoteOnObservedInboundTxResponse{}, nil - } else if err != nil && isContractReverted { // contract call reverted; should refund - revertMessage := err.Error() - chain := k.zetaObserverKeeper.GetSupportedChainFromChainID(ctx, cctx.InboundTxParams.SenderChainId) - if chain == nil { - cctx.CctxStatus.ChangeStatus(types.CctxStatus_Aborted, "invalid sender chain") - return &types.MsgVoteOnObservedInboundTxResponse{}, nil - } - - gasLimit, err := k.GetRevertGasLimit(ctx, cctx) - if err != nil { - cctx.CctxStatus.ChangeStatus(types.CctxStatus_Aborted, "can't get revert tx gas limit"+err.Error()) - return &types.MsgVoteOnObservedInboundTxResponse{}, nil - } - if gasLimit == 0 { - // use same gas limit of outbound as a fallback -- should not happen - gasLimit = msg.GasLimit - } - - // create new OutboundTxParams for the revert - revertTxParams := &types.OutboundTxParams{ - Receiver: cctx.InboundTxParams.Sender, - ReceiverChainId: cctx.InboundTxParams.SenderChainId, - Amount: cctx.InboundTxParams.Amount, - CoinType: cctx.InboundTxParams.CoinType, - OutboundTxGasLimit: gasLimit, - } - cctx.OutboundTxParams = append(cctx.OutboundTxParams, revertTxParams) - - // we create a new cached context, and we don't commit the previous one with EVM deposit - tmpCtx, commit := ctx.CacheContext() - err = func() error { - err := k.PayGasAndUpdateCctx( - tmpCtx, - chain.ChainId, - &cctx, - cctx.InboundTxParams.Amount, - false, - ) - if err != nil { - return err - } - return k.UpdateNonce(tmpCtx, chain.ChainId, &cctx) - }() - if err != nil { - cctx.CctxStatus.ChangeStatus(types.CctxStatus_Aborted, err.Error()+" deposit revert message: "+revertMessage) - return &types.MsgVoteOnObservedInboundTxResponse{}, nil - } - commit() - cctx.CctxStatus.ChangeStatus(types.CctxStatus_PendingRevert, revertMessage) - return &types.MsgVoteOnObservedInboundTxResponse{}, nil - } - // successful HandleEVMDeposit; - commit() - cctx.CctxStatus.ChangeStatus(types.CctxStatus_OutboundMined, "Remote omnichain contract call completed") - return &types.MsgVoteOnObservedInboundTxResponse{}, nil - } - - // Receiver is not ZetaChain: Cross Chain SWAP - tmpCtx, commit = ctx.CacheContext() - err = func() error { - err := k.PayGasAndUpdateCctx( - tmpCtx, - msg.ReceiverChain, - &cctx, - cctx.InboundTxParams.Amount, - false, - ) - if err != nil { - return err - } - return k.UpdateNonce(tmpCtx, msg.ReceiverChain, &cctx) - }() + // create a new CCTX from the inbound message.The status of the new CCTX is set to PendingInbound. + cctx, err := types.NewCCTX(ctx, *msg, tss.TssPubkey) if err != nil { - // do not commit anything here as the CCTX should be aborted - cctx.CctxStatus.ChangeStatus(types.CctxStatus_Aborted, err.Error()) - return &types.MsgVoteOnObservedInboundTxResponse{}, nil + return nil, err } - commit() - cctx.CctxStatus.ChangeStatus(types.CctxStatus_PendingOutbound, "") + // Process the inbound CCTX, the process function manages the state commit and cctx status change. + // If the process fails, the changes to the evm state are rolled back. + k.ProcessInbound(ctx, &cctx) + // Save the inbound CCTX to the store. This is called irrespective of the status of the CCTX or the outcome of the process function. + k.SaveInbound(ctx, &cctx, msg.EventIndex) return &types.MsgVoteOnObservedInboundTxResponse{}, nil } + +/* SaveInbound saves the inbound CCTX to the store.It does the following: + - Emits an event for the finalized inbound CCTX. + - Adds the inbound CCTX to the finalized inbound CCTX store.This is done to prevent double spending, using the same inbound tx hash and event index. + - Updates the CCTX with the finalized height and finalization status. + - Removes the inbound CCTX from the inbound transaction tracker store.This is only for inbounds created via InTx tracker suggestions + - Sets the CCTX and nonce to the CCTX and inbound transaction hash to CCTX store. +*/ + +func (k Keeper) SaveInbound(ctx sdk.Context, cctx *types.CrossChainTx, eventIndex uint64) { + EmitEventInboundFinalized(ctx, cctx) + k.AddFinalizedInbound(ctx, + cctx.GetInboundTxParams().InboundTxObservedHash, + cctx.GetInboundTxParams().SenderChainId, + eventIndex) + // #nosec G701 always positive + cctx.InboundTxParams.InboundTxFinalizedZetaHeight = uint64(ctx.BlockHeight()) + cctx.InboundTxParams.TxFinalizationStatus = types.TxFinalizationStatus_Executed + k.RemoveInTxTrackerIfExists(ctx, cctx.InboundTxParams.SenderChainId, cctx.InboundTxParams.InboundTxObservedHash) + k.SetCctxAndNonceToCctxAndInTxHashToCctx(ctx, *cctx) +} diff --git a/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go b/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go index fa51c66bfa..25d5615056 100644 --- a/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go +++ b/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go @@ -2,15 +2,19 @@ package keeper_test import ( "encoding/hex" + "math/big" "testing" sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" + ethcommon "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/pkg/coin" keepertest "github.com/zeta-chain/zetacore/testutil/keeper" "github.com/zeta-chain/zetacore/testutil/sample" "github.com/zeta-chain/zetacore/x/crosschain/keeper" + "github.com/zeta-chain/zetacore/x/crosschain/types" observertypes "github.com/zeta-chain/zetacore/x/observer/types" ) @@ -51,6 +55,8 @@ func TestKeeper_VoteOnObservedInboundTx(t *testing.T) { to = chain.ChainId } } + zk.ObserverKeeper.SetTSS(ctx, sample.Tss()) + msg := sample.InboundVote(0, from, to) for _, validatorAddr := range validatorList { msg.Creator = validatorAddr @@ -97,6 +103,9 @@ func TestKeeper_VoteOnObservedInboundTx(t *testing.T) { ObserverList: []string{validatorAddr}, }) + // Add tss to the observer keeper + zk.ObserverKeeper.SetTSS(ctx, sample.Tss()) + // Vote on the FIRST message. msg := &types.MsgVoteOnObservedInboundTx{ Creator: validatorAddr, @@ -199,3 +208,79 @@ func TestStatus_ChangeStatus(t *testing.T) { }) } } + +func TestKeeper_SaveInbound(t *testing.T) { + t.Run("should save the cctx", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain(t) + cctx := GetERC20Cctx(t, receiver, *senderChain, "", amount) + eventIndex := sample.Uint64InRange(1, 100) + k.SaveInbound(ctx, cctx, eventIndex) + require.Equal(t, types.TxFinalizationStatus_Executed, cctx.InboundTxParams.TxFinalizationStatus) + require.True(t, k.IsFinalizedInbound(ctx, cctx.GetInboundTxParams().InboundTxObservedHash, cctx.GetInboundTxParams().SenderChainId, eventIndex)) + _, found := k.GetCrossChainTx(ctx, cctx.Index) + require.True(t, found) + }) + + t.Run("should save the cctx and remove tracker", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain(t) + cctx := GetERC20Cctx(t, receiver, *senderChain, "", amount) + hash := sample.Hash() + cctx.InboundTxParams.InboundTxObservedHash = hash.String() + k.SetInTxTracker(ctx, types.InTxTracker{ + ChainId: senderChain.ChainId, + TxHash: hash.String(), + CoinType: 0, + }) + eventIndex := sample.Uint64InRange(1, 100) + + k.SaveInbound(ctx, cctx, eventIndex) + require.Equal(t, types.TxFinalizationStatus_Executed, cctx.InboundTxParams.TxFinalizationStatus) + require.True(t, k.IsFinalizedInbound(ctx, cctx.GetInboundTxParams().InboundTxObservedHash, cctx.GetInboundTxParams().SenderChainId, eventIndex)) + _, found := k.GetCrossChainTx(ctx, cctx.Index) + require.True(t, found) + _, found = k.GetInTxTracker(ctx, senderChain.ChainId, hash.String()) + require.False(t, found) + }) +} + +// GetERC20Cctx returns a sample CrossChainTx with ERC20 params. This is used for testing Inbound and Outbound voting transactions +func GetERC20Cctx(t *testing.T, receiver ethcommon.Address, senderChain chains.Chain, asset string, amount *big.Int) *types.CrossChainTx { + r := sample.Rand() + cctx := &types.CrossChainTx{ + Creator: sample.AccAddress(), + Index: sample.ZetaIndex(t), + ZetaFees: sample.UintInRange(0, 100), + RelayedMessage: "", + CctxStatus: &types.Status{Status: types.CctxStatus_PendingInbound}, + InboundTxParams: sample.InboundTxParams(r), + OutboundTxParams: []*types.OutboundTxParams{sample.OutboundTxParams(r)}, + } + + cctx.GetInboundTxParams().Amount = sdkmath.NewUintFromBigInt(amount) + cctx.GetInboundTxParams().SenderChainId = senderChain.ChainId + cctx.GetInboundTxParams().InboundTxObservedHash = sample.Hash().String() + cctx.GetInboundTxParams().InboundTxBallotIndex = sample.ZetaIndex(t) + + cctx.GetCurrentOutTxParam().ReceiverChainId = senderChain.ChainId + cctx.GetCurrentOutTxParam().Receiver = receiver.String() + cctx.GetCurrentOutTxParam().OutboundTxHash = sample.Hash().String() + cctx.GetCurrentOutTxParam().OutboundTxBallotIndex = sample.ZetaIndex(t) + + cctx.InboundTxParams.CoinType = coin.CoinType_ERC20 + for _, outboundTxParam := range cctx.OutboundTxParams { + outboundTxParam.CoinType = coin.CoinType_ERC20 + } + + cctx.GetInboundTxParams().Asset = asset + cctx.GetInboundTxParams().Sender = sample.EthAddress().String() + cctx.GetCurrentOutTxParam().OutboundTxTssNonce = 42 + cctx.GetCurrentOutTxParam().OutboundTxGasUsed = 100 + cctx.GetCurrentOutTxParam().OutboundTxEffectiveGasLimit = 100 + return cctx +} diff --git a/x/crosschain/keeper/msg_server_vote_outbound_tx.go b/x/crosschain/keeper/msg_server_vote_outbound_tx.go index 9286f0865a..22afff1218 100644 --- a/x/crosschain/keeper/msg_server_vote_outbound_tx.go +++ b/x/crosschain/keeper/msg_server_vote_outbound_tx.go @@ -2,21 +2,15 @@ package keeper import ( "context" - "errors" "fmt" "math/big" cosmoserrors "cosmossdk.io/errors" "cosmossdk.io/math" - sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - "github.com/rs/zerolog/log" - "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/x/crosschain/types" observerkeeper "github.com/zeta-chain/zetacore/x/observer/keeper" - observertypes "github.com/zeta-chain/zetacore/x/observer/types" ) // VoteOnObservedOutboundTx casts a vote on an outbound transaction observed on a connected chain (after @@ -63,20 +57,15 @@ import ( func (k msgServer) VoteOnObservedOutboundTx(goCtx context.Context, msg *types.MsgVoteOnObservedOutboundTx) (*types.MsgVoteOnObservedOutboundTxResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - // check if CCTX exists and if the nonce matches - cctx, found := k.GetCrossChainTx(ctx, msg.CctxHash) - if !found { - return nil, cosmoserrors.Wrap(sdkerrors.ErrInvalidRequest, fmt.Sprintf("CCTX %s does not exist", msg.CctxHash)) - } - if cctx.GetCurrentOutTxParam().OutboundTxTssNonce != msg.OutTxTssNonce { - return nil, cosmoserrors.Wrap(sdkerrors.ErrInvalidRequest, fmt.Sprintf("OutTxTssNonce %d does not match CCTX OutTxTssNonce %d", msg.OutTxTssNonce, cctx.GetCurrentOutTxParam().OutboundTxTssNonce)) + // Validate the message params to verify it against an existing cctx + cctx, err := k.ValidateOutboundMessage(ctx, *msg) + if err != nil { + return nil, err } - // get ballot index ballotIndex := msg.Digest() - // vote on outbound ballot - isFinalized, isNew, ballot, observationChain, err := k.zetaObserverKeeper.VoteOnOutboundBallot( + isFinalizingVote, isNew, ballot, observationChain, err := k.zetaObserverKeeper.VoteOnOutboundBallot( ctx, ballotIndex, msg.OutTxChain, @@ -85,141 +74,41 @@ func (k msgServer) VoteOnObservedOutboundTx(goCtx context.Context, msg *types.Ms if err != nil { return nil, err } - // if the ballot is new, set the index to the CCTX if isNew { observerkeeper.EmitEventBallotCreated(ctx, ballot, msg.ObservedOutTxHash, observationChain) - // Set this the first time when the ballot is created - // The ballot might change if there are more votes in a different outbound ballot for this cctx hash - cctx.GetCurrentOutTxParam().OutboundTxBallotIndex = ballotIndex } - // if not finalized commit state here - if !isFinalized { + if !isFinalizingVote { return &types.MsgVoteOnObservedOutboundTxResponse{}, nil } // if ballot successful, the value received should be the out tx amount - if ballot.BallotStatus != observertypes.BallotStatus_BallotFinalized_FailureObservation { - if !msg.ValueReceived.Equal(cctx.GetCurrentOutTxParam().Amount) { - log.Error().Msgf("VoteOnObservedOutboundTx: Mint mismatch: %s value received vs %s cctx amount", - msg.ValueReceived, - cctx.GetCurrentOutTxParam().Amount) - return nil, cosmoserrors.Wrap(sdkerrors.ErrInvalidRequest, fmt.Sprintf("ValueReceived %s does not match sent value %s", msg.ValueReceived, cctx.GetCurrentOutTxParam().Amount)) - } + err = cctx.AddOutbound(ctx, *msg, ballot.BallotStatus) + if err != nil { + return nil, err } - - // Update CCTX values - cctx.GetCurrentOutTxParam().OutboundTxHash = msg.ObservedOutTxHash - cctx.GetCurrentOutTxParam().OutboundTxGasUsed = msg.ObservedOutTxGasUsed - cctx.GetCurrentOutTxParam().OutboundTxEffectiveGasPrice = msg.ObservedOutTxEffectiveGasPrice - cctx.GetCurrentOutTxParam().OutboundTxEffectiveGasLimit = msg.ObservedOutTxEffectiveGasLimit - cctx.CctxStatus.LastUpdateTimestamp = ctx.BlockHeader().Time.Unix() - // Fund the gas stability pool with the remaining funds - if err := k.FundGasStabilityPoolFromRemainingFees(ctx, *cctx.GetCurrentOutTxParam(), msg.OutTxChain); err != nil { - log.Error().Msgf( - "VoteOnObservedOutboundTx: CCTX: %s Can't fund the gas stability pool with remaining fees %s", cctx.Index, err.Error(), - ) - } + k.FundStabilityPool(ctx, &cctx) - tss, found := k.zetaObserverKeeper.GetTSS(ctx) - if !found { - return nil, types.ErrCannotFindTSSKeys - } - - tmpCtx, commit := ctx.CacheContext() - err = func() error { //err = FinalizeOutbound(k, ctx, &cctx, msg, ballot.BallotStatus) - cctx.GetCurrentOutTxParam().OutboundTxObservedExternalHeight = msg.ObservedOutTxBlockHeight - oldStatus := cctx.CctxStatus.Status - switch ballot.BallotStatus { - case observertypes.BallotStatus_BallotFinalized_SuccessObservation: - switch oldStatus { - case types.CctxStatus_PendingRevert: - cctx.CctxStatus.ChangeStatus(types.CctxStatus_Reverted, "") - case types.CctxStatus_PendingOutbound: - cctx.CctxStatus.ChangeStatus(types.CctxStatus_OutboundMined, "") - } - newStatus := cctx.CctxStatus.Status.String() - EmitOutboundSuccess(tmpCtx, msg, oldStatus.String(), newStatus, cctx) - case observertypes.BallotStatus_BallotFinalized_FailureObservation: - if msg.CoinType == coin.CoinType_Cmd || chains.IsZetaChain(cctx.InboundTxParams.SenderChainId) { - // if the cctx is of coin type cmd or the sender chain is zeta chain, then we do not revert, the cctx is aborted - cctx.CctxStatus.ChangeStatus(types.CctxStatus_Aborted, "") - } else { - switch oldStatus { - case types.CctxStatus_PendingOutbound: - - gasLimit, err := k.GetRevertGasLimit(ctx, cctx) - if err != nil { - return errors.New("can't get revert tx gas limit" + err.Error()) - } - if gasLimit == 0 { - // use same gas limit of outbound as a fallback -- should not happen - gasLimit = cctx.OutboundTxParams[0].OutboundTxGasLimit - } - - // create new OutboundTxParams for the revert - revertTxParams := &types.OutboundTxParams{ - Receiver: cctx.InboundTxParams.Sender, - ReceiverChainId: cctx.InboundTxParams.SenderChainId, - Amount: cctx.InboundTxParams.Amount, - CoinType: cctx.InboundTxParams.CoinType, - OutboundTxGasLimit: gasLimit, - } - cctx.OutboundTxParams = append(cctx.OutboundTxParams, revertTxParams) - - err = k.PayGasAndUpdateCctx( - tmpCtx, - cctx.InboundTxParams.SenderChainId, - &cctx, - cctx.OutboundTxParams[0].Amount, - false, - ) - if err != nil { - return err - } - err = k.UpdateNonce(tmpCtx, cctx.InboundTxParams.SenderChainId, &cctx) - if err != nil { - return err - } - cctx.CctxStatus.ChangeStatus(types.CctxStatus_PendingRevert, "Outbound failed, start revert") - case types.CctxStatus_PendingRevert: - cctx.CctxStatus.ChangeStatus(types.CctxStatus_Aborted, "Outbound failed: revert failed; abort TX") - } - } - newStatus := cctx.CctxStatus.Status.String() - EmitOutboundFailure(ctx, msg, oldStatus.String(), newStatus, cctx) - } - return nil - }() + err = k.ProcessOutbound(ctx, &cctx, ballot.BallotStatus, msg.ValueReceived.String()) if err != nil { - // do not commit tmpCtx - cctx.CctxStatus.ChangeStatus(types.CctxStatus_Aborted, err.Error()) - cctx.GetCurrentOutTxParam().TxFinalizationStatus = types.TxFinalizationStatus_Executed - ctx.Logger().Error(err.Error()) - // #nosec G701 always in range - k.GetObserverKeeper().RemoveFromPendingNonces(ctx, tss.TssPubkey, msg.OutTxChain, int64(msg.OutTxTssNonce)) - k.RemoveOutTxTracker(ctx, msg.OutTxChain, msg.OutTxTssNonce) - k.SetCctxAndNonceToCctxAndInTxHashToCctx(ctx, cctx) + k.SaveFailedOutbound(ctx, &cctx, err.Error(), ballotIndex) return &types.MsgVoteOnObservedOutboundTxResponse{}, nil } - commit() - // Set the ballot index to the finalized ballot - cctx.GetCurrentOutTxParam().OutboundTxBallotIndex = ballotIndex - cctx.GetCurrentOutTxParam().TxFinalizationStatus = types.TxFinalizationStatus_Executed - // #nosec G701 always in range - k.GetObserverKeeper().RemoveFromPendingNonces(ctx, tss.TssPubkey, msg.OutTxChain, int64(msg.OutTxTssNonce)) - k.RemoveOutTxTracker(ctx, msg.OutTxChain, msg.OutTxTssNonce) - ctx.Logger().Info(fmt.Sprintf("Remove tracker %s: , Block Height : %d ", getOutTrackerIndex(msg.OutTxChain, msg.OutTxTssNonce), ctx.BlockHeight())) - k.SetCctxAndNonceToCctxAndInTxHashToCctx(ctx, cctx) + k.SaveSuccessfulOutbound(ctx, &cctx, ballotIndex) return &types.MsgVoteOnObservedOutboundTxResponse{}, nil } -func percentOf(n *big.Int, percent int64) *big.Int { - n = n.Mul(n, big.NewInt(percent)) - n = n.Div(n, big.NewInt(100)) - return n +// FundStabilityPool funds the stability pool with the remaining fees of an outbound tx +// The funds are sent to the gas stability pool associated with the receiver chain +// This wraps the FundGasStabilityPoolFromRemainingFees function and logs an error if it fails.We do not return an error here. +// Event if the funding fails, the outbound tx is still processed. +func (k Keeper) FundStabilityPool(ctx sdk.Context, cctx *types.CrossChainTx) { + // Fund the gas stability pool with the remaining funds + if err := k.FundGasStabilityPoolFromRemainingFees(ctx, *cctx.GetCurrentOutTxParam(), cctx.GetCurrentOutTxParam().ReceiverChainId); err != nil { + ctx.Logger().Error(fmt.Sprintf("VoteOnObservedOutboundTx: CCTX: %s Can't fund the gas stability pool with remaining fees %s", cctx.Index, err.Error())) + } } // FundGasStabilityPoolFromRemainingFees funds the gas stability pool with the remaining fees of an outbound tx @@ -240,7 +129,6 @@ func (k Keeper) FundGasStabilityPoolFromRemainingFees(ctx sdk.Context, outboundT // We fund the stability pool with a portion of the remaining fees remainingFees = percentOf(remainingFees, RemainingFeesToStabilityPoolPercent) - // Fund the gas stability pool if err := k.fungibleKeeper.FundGasStabilityPool(ctx, chainID, remainingFees); err != nil { return err @@ -251,3 +139,74 @@ func (k Keeper) FundGasStabilityPoolFromRemainingFees(ctx sdk.Context, outboundT } return nil } + +// percentOf returns the percentage of a number +func percentOf(n *big.Int, percent int64) *big.Int { + n = n.Mul(n, big.NewInt(percent)) + n = n.Div(n, big.NewInt(100)) + return n +} + +/* +SaveFailedOutbound saves a failed outbound transaction.It does the following things in one function: + + 1. Change the status of the CCTX to Aborted + + 2. Save the outbound +*/ +func (k Keeper) SaveFailedOutbound(ctx sdk.Context, cctx *types.CrossChainTx, errMessage string, ballotIndex string) { + cctx.SetAbort(errMessage) + ctx.Logger().Error(errMessage) + + k.SaveOutbound(ctx, cctx, ballotIndex) +} + +// SaveSuccessfulOutbound saves a successful outbound transaction. +func (k Keeper) SaveSuccessfulOutbound(ctx sdk.Context, cctx *types.CrossChainTx, ballotIndex string) { + k.SaveOutbound(ctx, cctx, ballotIndex) +} + +/* +SaveOutbound saves the outbound transaction.It does the following things in one function: + + 1. Set the ballot index for the outbound vote to the cctx + + 2. Remove the nonce from the pending nonces + + 3. Remove the outbound tx tracker + + 4. Set the cctx and nonce to cctx and inTxHash to cctx +*/ +func (k Keeper) SaveOutbound(ctx sdk.Context, cctx *types.CrossChainTx, ballotIndex string) { + receiverChain := cctx.GetCurrentOutTxParam().ReceiverChainId + tssPubkey := cctx.GetCurrentOutTxParam().TssPubkey + outTxTssNonce := cctx.GetCurrentOutTxParam().OutboundTxTssNonce + + cctx.GetCurrentOutTxParam().OutboundTxBallotIndex = ballotIndex + // #nosec G701 always in range + k.GetObserverKeeper().RemoveFromPendingNonces(ctx, tssPubkey, receiverChain, int64(outTxTssNonce)) + k.RemoveOutTxTracker(ctx, receiverChain, outTxTssNonce) + ctx.Logger().Info(fmt.Sprintf("Remove tracker %s: , Block Height : %d ", getOutTrackerIndex(receiverChain, outTxTssNonce), ctx.BlockHeight())) + // This should set nonce to cctx only if a new revert is created. + k.SetCctxAndNonceToCctxAndInTxHashToCctx(ctx, *cctx) +} + +func (k Keeper) ValidateOutboundMessage(ctx sdk.Context, msg types.MsgVoteOnObservedOutboundTx) (types.CrossChainTx, error) { + // check if CCTX exists and if the nonce matches + cctx, found := k.GetCrossChainTx(ctx, msg.CctxHash) + if !found { + return types.CrossChainTx{}, cosmoserrors.Wrap(sdkerrors.ErrInvalidRequest, fmt.Sprintf("CCTX %s does not exist", msg.CctxHash)) + } + if cctx.GetCurrentOutTxParam().OutboundTxTssNonce != msg.OutTxTssNonce { + return types.CrossChainTx{}, cosmoserrors.Wrap(sdkerrors.ErrInvalidRequest, fmt.Sprintf("OutTxTssNonce %d does not match CCTX OutTxTssNonce %d", msg.OutTxTssNonce, cctx.GetCurrentOutTxParam().OutboundTxTssNonce)) + } + // do not process an outbound vote if TSS is not found + _, found = k.zetaObserverKeeper.GetTSS(ctx) + if !found { + return types.CrossChainTx{}, types.ErrCannotFindTSSKeys + } + if cctx.GetCurrentOutTxParam().ReceiverChainId != msg.OutTxChain { + return types.CrossChainTx{}, cosmoserrors.Wrap(sdkerrors.ErrInvalidRequest, fmt.Sprintf("OutTxChain %d does not match CCTX OutTxChain %d", msg.OutTxChain, cctx.GetCurrentOutTxParam().ReceiverChainId)) + } + return cctx, nil +} 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 b5ad6ab78d..511e703acd 100644 --- a/x/crosschain/keeper/msg_server_vote_outbound_tx_test.go +++ b/x/crosschain/keeper/msg_server_vote_outbound_tx_test.go @@ -2,15 +2,22 @@ package keeper_test import ( "errors" + "fmt" "math/big" "math/rand" "testing" "cosmossdk.io/math" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - testkeeper "github.com/zeta-chain/zetacore/testutil/keeper" + "github.com/zeta-chain/zetacore/pkg/chains" + keepertest "github.com/zeta-chain/zetacore/testutil/keeper" "github.com/zeta-chain/zetacore/testutil/sample" "github.com/zeta-chain/zetacore/x/crosschain/keeper" + "github.com/zeta-chain/zetacore/x/crosschain/types" + fungibletypes "github.com/zeta-chain/zetacore/x/fungible/types" + observertypes "github.com/zeta-chain/zetacore/x/observer/types" ) func TestKeeper_FundGasStabilityPoolFromRemainingFees(t *testing.T) { @@ -85,8 +92,8 @@ func TestKeeper_FundGasStabilityPoolFromRemainingFees(t *testing.T) { for _, tc := range tt { tc := tc t.Run(tc.name, func(t *testing.T) { - k, ctx := testkeeper.CrosschainKeeperAllMocks(t) - fungibleMock := testkeeper.GetCrosschainFungibleMock(t, k) + k, ctx := keepertest.CrosschainKeeperAllMocks(t) + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) // OutboundTxParams outbound := sample.OutboundTxParams(r) @@ -111,3 +118,466 @@ func TestKeeper_FundGasStabilityPoolFromRemainingFees(t *testing.T) { }) } } + +func TestKeeper_VoteOnObservedOutboundTx(t *testing.T) { + t.Run("successfully vote on outbound tx with status pending outbound ,vote-type success", 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(t) + asset := "" + observer := sample.AccAddress() + tss := sample.Tss() + zk.ObserverKeeper.SetObserverSet(ctx, observertypes.ObserverSet{ObserverList: []string{observer}}) + cctx := GetERC20Cctx(t, receiver, *senderChain, asset, amount) + cctx.GetCurrentOutTxParam().TssPubkey = tss.TssPubkey + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + k.SetCrossChainTx(ctx, *cctx) + observerMock.On("GetTSS", ctx).Return(observertypes.TSS{}, true).Once() + + // Successfully mock VoteOnOutboundBallot + keepertest.MockVoteOnOutboundSuccessBallot(observerMock, ctx, cctx, *senderChain, observer) + + // Successfully mock GetOutBound + keepertest.MockGetOutBound(observerMock, ctx) + + // Successfully mock SaveSuccessfulOutbound + keepertest.MockSaveOutBound(observerMock, ctx, cctx, tss) + + msgServer := keeper.NewMsgServerImpl(*k) + _, err := msgServer.VoteOnObservedOutboundTx(ctx, &types.MsgVoteOnObservedOutboundTx{ + CctxHash: cctx.Index, + OutTxTssNonce: cctx.GetCurrentOutTxParam().OutboundTxTssNonce, + OutTxChain: cctx.GetCurrentOutTxParam().ReceiverChainId, + Status: chains.ReceiveStatus_Success, + Creator: observer, + ObservedOutTxHash: sample.Hash().String(), + ValueReceived: cctx.GetCurrentOutTxParam().Amount, + ObservedOutTxBlockHeight: 10, + ObservedOutTxEffectiveGasPrice: math.NewInt(21), + ObservedOutTxGasUsed: 21, + CoinType: cctx.InboundTxParams.CoinType, + }) + require.NoError(t, err) + c, found := k.GetCrossChainTx(ctx, cctx.Index) + require.True(t, found) + require.Equal(t, types.CctxStatus_OutboundMined, c.CctxStatus.Status) + }) + + t.Run("successfully vote on outbound tx, vote-type failed", func(t *testing.T) { + k, ctx, _, zk := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseObserverMock: true, + UseFungibleMock: true, + }) + + // Setup mock data + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + observerMock := keepertest.GetCrosschainObserverMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain(t) + asset := "" + observer := sample.AccAddress() + tss := sample.Tss() + zk.ObserverKeeper.SetObserverSet(ctx, observertypes.ObserverSet{ObserverList: []string{observer}}) + cctx := GetERC20Cctx(t, receiver, *senderChain, asset, amount) + cctx.GetCurrentOutTxParam().TssPubkey = tss.TssPubkey + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + k.SetCrossChainTx(ctx, *cctx) + observerMock.On("GetTSS", ctx).Return(observertypes.TSS{}, true).Once() + + // Successfully mock VoteOnOutboundBallot + keepertest.MockVoteOnOutboundFailedBallot(observerMock, ctx, cctx, *senderChain, observer) + + // Successfully mock GetOutBound + keepertest.MockGetOutBound(observerMock, ctx) + + // Successfully mock ProcessOutbound + keepertest.MockGetRevertGasLimitForERC20(fungibleMock, asset, *senderChain) + keepertest.MockPayGasAndUpdateCCTX(fungibleMock, observerMock, ctx, *k, *senderChain, asset) + _ = keepertest.MockUpdateNonce(observerMock, *senderChain) + + //Successfully mock SaveOutBound + keepertest.MockSaveOutBoundNewRevertCreated(observerMock, ctx, cctx, tss) + oldParamsLen := len(cctx.OutboundTxParams) + msgServer := keeper.NewMsgServerImpl(*k) + _, err := msgServer.VoteOnObservedOutboundTx(ctx, &types.MsgVoteOnObservedOutboundTx{ + CctxHash: cctx.Index, + OutTxTssNonce: cctx.GetCurrentOutTxParam().OutboundTxTssNonce, + OutTxChain: cctx.GetCurrentOutTxParam().ReceiverChainId, + Status: chains.ReceiveStatus_Failed, + Creator: observer, + ObservedOutTxHash: sample.Hash().String(), + ValueReceived: cctx.GetCurrentOutTxParam().Amount, + ObservedOutTxBlockHeight: 10, + ObservedOutTxEffectiveGasPrice: math.NewInt(21), + ObservedOutTxGasUsed: 21, + CoinType: cctx.InboundTxParams.CoinType, + }) + require.NoError(t, err) + c, found := k.GetCrossChainTx(ctx, cctx.Index) + require.True(t, found) + require.Equal(t, types.CctxStatus_PendingRevert, c.CctxStatus.Status) + require.Equal(t, oldParamsLen+1, len(c.OutboundTxParams)) + require.Equal(t, types.TxFinalizationStatus_Executed, c.OutboundTxParams[oldParamsLen-1].TxFinalizationStatus) + require.Equal(t, types.TxFinalizationStatus_NotFinalized, cctx.GetCurrentOutTxParam().TxFinalizationStatus) + }) + + t.Run("unsuccessfully vote on outbound tx, vote-type failed", func(t *testing.T) { + k, ctx, _, zk := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseObserverMock: true, + UseFungibleMock: true, + }) + + // Setup mock data + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + observerMock := keepertest.GetCrosschainObserverMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain(t) + asset := "" + observer := sample.AccAddress() + tss := sample.Tss() + zk.ObserverKeeper.SetObserverSet(ctx, observertypes.ObserverSet{ObserverList: []string{observer}}) + cctx := GetERC20Cctx(t, receiver, *senderChain, asset, amount) + cctx.GetCurrentOutTxParam().TssPubkey = tss.TssPubkey + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + k.SetCrossChainTx(ctx, *cctx) + observerMock.On("GetTSS", ctx).Return(observertypes.TSS{}, true).Once() + + // Successfully mock VoteOnOutboundBallot + keepertest.MockVoteOnOutboundFailedBallot(observerMock, ctx, cctx, *senderChain, observer) + + // Successfully mock GetOutBound + keepertest.MockGetOutBound(observerMock, ctx) + + // Mock Failed ProcessOutbound + keepertest.MockGetRevertGasLimitForERC20(fungibleMock, asset, *senderChain) + keepertest.MockPayGasAndUpdateCCTX(fungibleMock, observerMock, ctx, *k, *senderChain, asset) + observerMock.On("GetChainNonces", mock.Anything, senderChain.ChainName.String()). + Return(observertypes.ChainNonces{}, false) + + //Successfully mock SaveOutBound + keepertest.MockSaveOutBound(observerMock, ctx, cctx, tss) + oldParamsLen := len(cctx.OutboundTxParams) + msgServer := keeper.NewMsgServerImpl(*k) + _, err := msgServer.VoteOnObservedOutboundTx(ctx, &types.MsgVoteOnObservedOutboundTx{ + CctxHash: cctx.Index, + OutTxTssNonce: cctx.GetCurrentOutTxParam().OutboundTxTssNonce, + OutTxChain: cctx.GetCurrentOutTxParam().ReceiverChainId, + Status: chains.ReceiveStatus_Failed, + Creator: observer, + ObservedOutTxHash: sample.Hash().String(), + ValueReceived: cctx.GetCurrentOutTxParam().Amount, + ObservedOutTxBlockHeight: 10, + ObservedOutTxEffectiveGasPrice: math.NewInt(21), + ObservedOutTxGasUsed: 21, + CoinType: cctx.InboundTxParams.CoinType, + }) + require.NoError(t, err) + c, found := k.GetCrossChainTx(ctx, cctx.Index) + require.True(t, found) + require.Equal(t, types.CctxStatus_Aborted, c.CctxStatus.Status) + require.Equal(t, oldParamsLen+1, len(c.OutboundTxParams)) + // The message processing fails during the creation of the revert tx + // So the original outbound tx is executed and the revert tx is not finalized. + // The cctx status is Aborted + require.Equal(t, types.TxFinalizationStatus_NotFinalized, c.GetCurrentOutTxParam().TxFinalizationStatus) + require.Equal(t, types.TxFinalizationStatus_Executed, c.OutboundTxParams[oldParamsLen-1].TxFinalizationStatus) + }) + + t.Run("failure in processing outbound tx", func(t *testing.T) { + k, ctx, _, zk := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseObserverMock: true, + UseFungibleMock: true, + }) + + // Setup mock data + observerMock := keepertest.GetCrosschainObserverMock(t, k) + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain(t) + asset := "" + observer := sample.AccAddress() + tss := sample.Tss() + zk.ObserverKeeper.SetObserverSet(ctx, observertypes.ObserverSet{ObserverList: []string{observer}}) + cctx := GetERC20Cctx(t, receiver, *senderChain, asset, amount) + cctx.GetCurrentOutTxParam().TssPubkey = tss.TssPubkey + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + k.SetCrossChainTx(ctx, *cctx) + + // Successfully mock GetTSS + observerMock.On("GetTSS", ctx).Return(observertypes.TSS{}, true).Once() + + // Successfully mock VoteOnOutboundBallot + keepertest.MockVoteOnOutboundFailedBallot(observerMock, ctx, cctx, *senderChain, observer) + + // Successfully mock GetOutBound + keepertest.MockGetOutBound(observerMock, ctx) + + // Fail ProcessOutbound so that changes are not committed to the state + fungibleMock.On("GetForeignCoinFromAsset", mock.Anything, mock.Anything, mock.Anything).Return(fungibletypes.ForeignCoins{}, false) + + //Successfully mock SaveFailedOutbound + keepertest.MockSaveOutBound(observerMock, ctx, cctx, tss) + + msgServer := keeper.NewMsgServerImpl(*k) + _, err := msgServer.VoteOnObservedOutboundTx(ctx, &types.MsgVoteOnObservedOutboundTx{ + CctxHash: cctx.Index, + OutTxTssNonce: cctx.GetCurrentOutTxParam().OutboundTxTssNonce, + OutTxChain: cctx.GetCurrentOutTxParam().ReceiverChainId, + Status: chains.ReceiveStatus_Failed, + Creator: observer, + ObservedOutTxHash: sample.Hash().String(), + ValueReceived: cctx.GetCurrentOutTxParam().Amount, + ObservedOutTxBlockHeight: 10, + ObservedOutTxEffectiveGasPrice: math.NewInt(21), + ObservedOutTxGasUsed: 21, + CoinType: cctx.InboundTxParams.CoinType, + }) + require.NoError(t, err) + c, found := k.GetCrossChainTx(ctx, cctx.Index) + require.True(t, found) + // Status would be CctxStatus_PendingRevert if process outbound did not fail + require.Equal(t, types.CctxStatus_Aborted, c.CctxStatus.Status) + }) + + t.Run("fail to finalize outbound if not a finalizing vote", func(t *testing.T) { + k, ctx, sk, zk := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{}) + + // Setup mock data + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain(t) + asset := "" + r := rand.New(rand.NewSource(42)) + validator := sample.Validator(t, r) + tss := sample.Tss() + + // set state to successfully vote on outbound tx + accAddress, err := observertypes.GetAccAddressFromOperatorAddress(validator.OperatorAddress) + require.NoError(t, err) + zk.ObserverKeeper.SetObserverSet(ctx, observertypes.ObserverSet{ObserverList: []string{accAddress.String(), sample.AccAddress(), sample.AccAddress()}}) + sk.StakingKeeper.SetValidator(ctx, validator) + cctx := GetERC20Cctx(t, receiver, *senderChain, asset, amount) + cctx.GetCurrentOutTxParam().TssPubkey = tss.TssPubkey + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + k.SetCrossChainTx(ctx, *cctx) + zk.ObserverKeeper.SetTSS(ctx, tss) + + msgServer := keeper.NewMsgServerImpl(*k) + msg := &types.MsgVoteOnObservedOutboundTx{ + CctxHash: cctx.Index, + OutTxTssNonce: cctx.GetCurrentOutTxParam().OutboundTxTssNonce, + OutTxChain: cctx.GetCurrentOutTxParam().ReceiverChainId, + Status: chains.ReceiveStatus_Success, + Creator: accAddress.String(), + ObservedOutTxHash: sample.Hash().String(), + ValueReceived: cctx.GetCurrentOutTxParam().Amount, + ObservedOutTxBlockHeight: 10, + ObservedOutTxEffectiveGasPrice: math.NewInt(21), + ObservedOutTxGasUsed: 21, + CoinType: cctx.InboundTxParams.CoinType, + } + _, err = msgServer.VoteOnObservedOutboundTx(ctx, msg) + require.NoError(t, err) + c, found := k.GetCrossChainTx(ctx, cctx.Index) + require.True(t, found) + require.Equal(t, types.CctxStatus_PendingOutbound, c.CctxStatus.Status) + ballot, found := zk.ObserverKeeper.GetBallot(ctx, msg.Digest()) + require.True(t, found) + require.True(t, ballot.HasVoted(accAddress.String())) + }) + + t.Run("unable to add vote if tss is not present", func(t *testing.T) { + k, ctx, sk, zk := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{}) + + // Setup mock data + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain(t) + asset := "" + r := rand.New(rand.NewSource(42)) + validator := sample.Validator(t, r) + tss := sample.Tss() + + // set state to successfully vote on outbound tx + accAddress, err := observertypes.GetAccAddressFromOperatorAddress(validator.OperatorAddress) + require.NoError(t, err) + zk.ObserverKeeper.SetObserverSet(ctx, observertypes.ObserverSet{ObserverList: []string{accAddress.String()}}) + sk.StakingKeeper.SetValidator(ctx, validator) + cctx := GetERC20Cctx(t, receiver, *senderChain, asset, amount) + cctx.GetCurrentOutTxParam().TssPubkey = tss.TssPubkey + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + k.SetCrossChainTx(ctx, *cctx) + + msgServer := keeper.NewMsgServerImpl(*k) + msg := &types.MsgVoteOnObservedOutboundTx{ + CctxHash: cctx.Index, + OutTxTssNonce: cctx.GetCurrentOutTxParam().OutboundTxTssNonce, + OutTxChain: cctx.GetCurrentOutTxParam().ReceiverChainId, + Status: chains.ReceiveStatus_Success, + Creator: accAddress.String(), + ObservedOutTxHash: sample.Hash().String(), + ValueReceived: cctx.GetCurrentOutTxParam().Amount, + ObservedOutTxBlockHeight: 10, + ObservedOutTxEffectiveGasPrice: math.NewInt(21), + ObservedOutTxGasUsed: 21, + CoinType: cctx.InboundTxParams.CoinType, + } + _, err = msgServer.VoteOnObservedOutboundTx(ctx, msg) + require.ErrorIs(t, err, types.ErrCannotFindTSSKeys) + c, found := k.GetCrossChainTx(ctx, cctx.Index) + require.True(t, found) + require.Equal(t, types.CctxStatus_PendingOutbound, c.CctxStatus.Status) + _, found = zk.ObserverKeeper.GetBallot(ctx, msg.Digest()) + require.False(t, found) + }) +} + +func TestKeeper_SaveFailedOutBound(t *testing.T) { + t.Run("successfully save failed outbound", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + cctx := sample.CrossChainTx(t, "test") + k.SetOutTxTracker(ctx, types.OutTxTracker{ + Index: "", + ChainId: cctx.GetCurrentOutTxParam().ReceiverChainId, + Nonce: cctx.GetCurrentOutTxParam().OutboundTxTssNonce, + HashList: nil, + }) + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + k.SaveFailedOutbound(ctx, cctx, sample.String(), sample.ZetaIndex(t)) + require.Equal(t, cctx.CctxStatus.Status, types.CctxStatus_Aborted) + _, found := k.GetOutTxTracker(ctx, cctx.GetCurrentOutTxParam().ReceiverChainId, cctx.GetCurrentOutTxParam().OutboundTxTssNonce) + require.False(t, found) + }) +} + +func TestKeeper_SaveSuccessfulOutBound(t *testing.T) { + t.Run("successfully save successful outbound", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + cctx := sample.CrossChainTx(t, "test") + k.SetOutTxTracker(ctx, types.OutTxTracker{ + Index: "", + ChainId: cctx.GetCurrentOutTxParam().ReceiverChainId, + Nonce: cctx.GetCurrentOutTxParam().OutboundTxTssNonce, + HashList: nil, + }) + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + k.SaveSuccessfulOutbound(ctx, cctx, sample.String()) + require.Equal(t, cctx.GetCurrentOutTxParam().OutboundTxBallotIndex, sample.String()) + _, found := k.GetOutTxTracker(ctx, cctx.GetCurrentOutTxParam().ReceiverChainId, cctx.GetCurrentOutTxParam().OutboundTxTssNonce) + require.False(t, found) + }) +} + +func TestKeeper_SaveOutbound(t *testing.T) { + t.Run("successfully save outbound", func(t *testing.T) { + k, ctx, _, zk := keepertest.CrosschainKeeper(t) + + // setup state for crosschain and observer modules + cctx := sample.CrossChainTx(t, "test") + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + ballotIndex := sample.String() + k.SetOutTxTracker(ctx, types.OutTxTracker{ + Index: "", + ChainId: cctx.GetCurrentOutTxParam().ReceiverChainId, + Nonce: cctx.GetCurrentOutTxParam().OutboundTxTssNonce, + HashList: nil, + }) + + zk.ObserverKeeper.SetPendingNonces(ctx, observertypes.PendingNonces{ + NonceLow: int64(cctx.GetCurrentOutTxParam().OutboundTxTssNonce) - 1, + NonceHigh: int64(cctx.GetCurrentOutTxParam().OutboundTxTssNonce) + 1, + ChainId: cctx.GetCurrentOutTxParam().ReceiverChainId, + Tss: cctx.GetCurrentOutTxParam().TssPubkey, + }) + zk.ObserverKeeper.SetTSS(ctx, observertypes.TSS{ + TssPubkey: cctx.GetCurrentOutTxParam().TssPubkey, + }) + + // Save outbound and assert all values are successfully saved + k.SaveOutbound(ctx, cctx, ballotIndex) + require.Equal(t, cctx.GetCurrentOutTxParam().OutboundTxBallotIndex, ballotIndex) + _, found := k.GetOutTxTracker(ctx, cctx.GetCurrentOutTxParam().ReceiverChainId, cctx.GetCurrentOutTxParam().OutboundTxTssNonce) + require.False(t, found) + pn, found := zk.ObserverKeeper.GetPendingNonces(ctx, cctx.GetCurrentOutTxParam().TssPubkey, cctx.GetCurrentOutTxParam().ReceiverChainId) + require.True(t, found) + require.Equal(t, pn.NonceLow, int64(cctx.GetCurrentOutTxParam().OutboundTxTssNonce)+1) + require.Equal(t, pn.NonceHigh, int64(cctx.GetCurrentOutTxParam().OutboundTxTssNonce)+1) + _, found = k.GetInTxHashToCctx(ctx, cctx.InboundTxParams.InboundTxObservedHash) + require.True(t, found) + _, found = zk.ObserverKeeper.GetNonceToCctx(ctx, cctx.GetCurrentOutTxParam().TssPubkey, cctx.GetCurrentOutTxParam().ReceiverChainId, int64(cctx.GetCurrentOutTxParam().OutboundTxTssNonce)) + require.True(t, found) + }) +} + +func TestKeeper_ValidateOutboundMessage(t *testing.T) { + t.Run("successfully validate outbound message", func(t *testing.T) { + k, ctx, _, zk := keepertest.CrosschainKeeper(t) + cctx := sample.CrossChainTx(t, "test") + k.SetCrossChainTx(ctx, *cctx) + zk.ObserverKeeper.SetTSS(ctx, sample.Tss()) + _, err := k.ValidateOutboundMessage(ctx, types.MsgVoteOnObservedOutboundTx{ + CctxHash: cctx.Index, + OutTxTssNonce: cctx.GetCurrentOutTxParam().OutboundTxTssNonce, + OutTxChain: cctx.GetCurrentOutTxParam().ReceiverChainId, + }) + require.NoError(t, err) + }) + + t.Run("failed to validate outbound message if cctx not found", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + msg := types.MsgVoteOnObservedOutboundTx{ + CctxHash: sample.String(), + OutTxTssNonce: 1, + } + _, err := k.ValidateOutboundMessage(ctx, msg) + require.ErrorIs(t, err, sdkerrors.ErrInvalidRequest) + require.ErrorContains(t, err, fmt.Sprintf("CCTX %s does not exist", msg.CctxHash)) + }) + + t.Run("failed to validate outbound message if nonce does not match", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + cctx := sample.CrossChainTx(t, "test") + k.SetCrossChainTx(ctx, *cctx) + msg := types.MsgVoteOnObservedOutboundTx{ + CctxHash: cctx.Index, + OutTxTssNonce: 2, + } + _, err := k.ValidateOutboundMessage(ctx, msg) + require.ErrorIs(t, err, sdkerrors.ErrInvalidRequest) + require.ErrorContains(t, err, fmt.Sprintf("OutTxTssNonce %d does not match CCTX OutTxTssNonce %d", msg.OutTxTssNonce, cctx.GetCurrentOutTxParam().OutboundTxTssNonce)) + }) + + t.Run("failed to validate outbound message if tss not found", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + cctx := sample.CrossChainTx(t, "test") + k.SetCrossChainTx(ctx, *cctx) + _, err := k.ValidateOutboundMessage(ctx, types.MsgVoteOnObservedOutboundTx{ + CctxHash: cctx.Index, + OutTxTssNonce: cctx.GetCurrentOutTxParam().OutboundTxTssNonce, + }) + require.ErrorIs(t, err, types.ErrCannotFindTSSKeys) + }) + + t.Run("failed to validate outbound message if chain does not match", func(t *testing.T) { + k, ctx, _, zk := keepertest.CrosschainKeeper(t) + cctx := sample.CrossChainTx(t, "test") + k.SetCrossChainTx(ctx, *cctx) + zk.ObserverKeeper.SetTSS(ctx, sample.Tss()) + _, err := k.ValidateOutboundMessage(ctx, types.MsgVoteOnObservedOutboundTx{ + CctxHash: cctx.Index, + OutTxTssNonce: cctx.GetCurrentOutTxParam().OutboundTxTssNonce, + OutTxChain: 2, + }) + require.ErrorIs(t, err, sdkerrors.ErrInvalidRequest) + require.ErrorContains(t, err, fmt.Sprintf("OutTxChain %d does not match CCTX OutTxChain %d", 2, cctx.GetCurrentOutTxParam().ReceiverChainId)) + }) +} diff --git a/x/crosschain/keeper/msg_server_whitelist_erc20.go b/x/crosschain/keeper/msg_server_whitelist_erc20.go index 5e61ea3e11..0687aedbb1 100644 --- a/x/crosschain/keeper/msg_server_whitelist_erc20.go +++ b/x/crosschain/keeper/msg_server_whitelist_erc20.go @@ -112,6 +112,8 @@ func (k msgServer) WhitelistERC20(goCtx context.Context, msg *types.MsgWhitelist index := hash.Hex() // create a cmd cctx to whitelist the erc20 on the external chain + // TODO : refactor this to use the `NewCCTX` function instead. + //https://github.com/zeta-chain/node/issues/1909 cctx := types.CrossChainTx{ Creator: msg.Creator, Index: index, diff --git a/x/crosschain/keeper/process_inbound.go b/x/crosschain/keeper/process_inbound.go new file mode 100644 index 0000000000..c45d65bf51 --- /dev/null +++ b/x/crosschain/keeper/process_inbound.go @@ -0,0 +1,125 @@ +package keeper + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/x/crosschain/types" +) + +// ProcessInbound processes the inbound CCTX. +// It does a conditional dispatch to ProcessZEVMDeposit or ProcessCrosschainMsgPassing based on the receiver chain. +// The internal functions handle the state changes and error handling. +func (k Keeper) ProcessInbound(ctx sdk.Context, cctx *types.CrossChainTx) { + if chains.IsZetaChain(cctx.GetCurrentOutTxParam().ReceiverChainId) { + k.ProcessZEVMDeposit(ctx, cctx) + } else { + k.ProcessCrosschainMsgPassing(ctx, cctx) + } +} + +/* +ProcessZEVMDeposit processes the EVM deposit CCTX. A deposit is a cctx which has Zetachain as the receiver chain.It trasnsitions state according to the following rules: + - If the deposit is successful, the CCTX status is changed to OutboundMined. + - If the deposit returns an internal error i.e if HandleEVMDeposit() returns an error, but isContractReverted is false, the CCTX status is changed to Aborted. + - If the deposit is reverted, the function tries to create a revert cctx with status PendingRevert. + - If the creation of revert tx also fails it changes the status to Aborted. + +Note : Aborted CCTXs are not refunded in this function. The refund is done using a separate refunding mechanism. +We do not return an error from this function , as all changes need to be persisted to the state. +Instead we use a temporary context to make changes and then commit the context on for the happy path ,i.e cctx is set to OutboundMined. +*/ +func (k Keeper) ProcessZEVMDeposit(ctx sdk.Context, cctx *types.CrossChainTx) { + tmpCtx, commit := ctx.CacheContext() + isContractReverted, err := k.HandleEVMDeposit(tmpCtx, cctx) + + if err != nil && !isContractReverted { // exceptional case; internal error; should abort CCTX + cctx.SetAbort(err.Error()) + return + } else if err != nil && isContractReverted { // contract call reverted; should refund + revertMessage := err.Error() + senderChain := k.zetaObserverKeeper.GetSupportedChainFromChainID(ctx, cctx.InboundTxParams.SenderChainId) + if senderChain == nil { + cctx.SetAbort(fmt.Sprintf("invalid sender chain id %d", cctx.InboundTxParams.SenderChainId)) + return + } + + gasLimit, err := k.GetRevertGasLimit(ctx, *cctx) + if err != nil { + cctx.SetAbort(fmt.Sprintf("revert gas limit error: %s", err.Error())) + return + } + if gasLimit == 0 { + // use same gas limit of outbound as a fallback -- should not happen + gasLimit = cctx.GetCurrentOutTxParam().OutboundTxGasLimit + } + + err = cctx.AddRevertOutbound(gasLimit) + if err != nil { + cctx.SetAbort(fmt.Sprintf("revert outbound error: %s", err.Error())) + return + } + + // we create a new cached context, and we don't commit the previous one with EVM deposit + tmpCtxRevert, commitRevert := ctx.CacheContext() + err = func() error { + err := k.PayGasAndUpdateCctx( + tmpCtxRevert, + senderChain.ChainId, + cctx, + cctx.InboundTxParams.Amount, + false, + ) + if err != nil { + return err + } + // Update nonce using senderchain id as this is a revert tx and would go back to the original sender + return k.UpdateNonce(tmpCtxRevert, senderChain.ChainId, cctx) + }() + if err != nil { + cctx.SetAbort(fmt.Sprintf("deposit revert message: %s err : %s", revertMessage, err.Error())) + return + } + commitRevert() + cctx.SetPendingRevert(revertMessage) + return + } + // successful HandleEVMDeposit; + commit() + cctx.SetOutBoundMined("Remote omnichain contract call completed") + return +} + +/* +ProcessCrosschainMsgPassing processes the CCTX for crosschain message passing. A crosschain message passing is a cctx which has a non-Zetachain as the receiver chain.It trasnsitions state according to the following rules: + - If the crosschain message passing is successful, the CCTX status is changed to PendingOutbound. + - If the crosschain message passing returns an error, the CCTX status is changed to Aborted. + We do not return an error from this function, as all changes need to be persisted to the state. + Instead, we use a temporary context to make changes and then commit the context on for the happy path ,i.e cctx is set to PendingOutbound. +*/ +func (k Keeper) ProcessCrosschainMsgPassing(ctx sdk.Context, cctx *types.CrossChainTx) { + tmpCtx, commit := ctx.CacheContext() + outboundReceiverChainID := cctx.GetCurrentOutTxParam().ReceiverChainId + err := func() error { + err := k.PayGasAndUpdateCctx( + tmpCtx, + outboundReceiverChainID, + cctx, + cctx.InboundTxParams.Amount, + false, + ) + if err != nil { + return err + } + return k.UpdateNonce(tmpCtx, outboundReceiverChainID, cctx) + }() + if err != nil { + // do not commit anything here as the CCTX should be aborted + cctx.SetAbort(err.Error()) + return + } + commit() + cctx.SetPendingOutbound("") + return +} diff --git a/x/crosschain/keeper/process_inbound_test.go b/x/crosschain/keeper/process_inbound_test.go new file mode 100644 index 0000000000..4b53f817de --- /dev/null +++ b/x/crosschain/keeper/process_inbound_test.go @@ -0,0 +1,355 @@ +package keeper_test + +import ( + "fmt" + "math/big" + "testing" + + sdkmath "cosmossdk.io/math" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/coin" + keepertest "github.com/zeta-chain/zetacore/testutil/keeper" + "github.com/zeta-chain/zetacore/testutil/sample" + "github.com/zeta-chain/zetacore/x/crosschain/types" + fungibletypes "github.com/zeta-chain/zetacore/x/fungible/types" + observertypes "github.com/zeta-chain/zetacore/x/observer/types" +) + +func TestKeeper_ProcessZEVMDeposit(t *testing.T) { + t.Run("process zevm deposit successfully", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + }) + + // Setup mock data + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + + // expect DepositCoinZeta to be called + fungibleMock.On("DepositCoinZeta", mock.Anything, receiver, amount). + Return(nil) + + // call ProcessZEVMDeposit + cctx := sample.CrossChainTx(t, "test") + cctx.CctxStatus = &types.Status{Status: types.CctxStatus_PendingInbound} + cctx.GetCurrentOutTxParam().Receiver = receiver.String() + cctx.GetInboundTxParams().Amount = sdkmath.NewUintFromBigInt(amount) + cctx.InboundTxParams.CoinType = coin.CoinType_Zeta + cctx.GetInboundTxParams().SenderChainId = 0 + k.ProcessZEVMDeposit(ctx, cctx) + require.Equal(t, types.CctxStatus_OutboundMined, cctx.CctxStatus.Status) + }) + + t.Run("unable to process zevm deposit HandleEVMDeposit returns err without reverting", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + }) + + // Setup mock data + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + + // mock unsuccessful HandleEVMDeposit which does not revert + fungibleMock.On("DepositCoinZeta", mock.Anything, receiver, amount). + Return(fmt.Errorf("deposit error"), false) + + // call ProcessZEVMDeposit + cctx := sample.CrossChainTx(t, "test") + cctx.CctxStatus = &types.Status{Status: types.CctxStatus_PendingInbound} + cctx.GetCurrentOutTxParam().Receiver = receiver.String() + cctx.GetInboundTxParams().Amount = sdkmath.NewUintFromBigInt(amount) + cctx.InboundTxParams.CoinType = coin.CoinType_Zeta + cctx.GetInboundTxParams().SenderChainId = 0 + k.ProcessZEVMDeposit(ctx, cctx) + require.Equal(t, types.CctxStatus_Aborted, cctx.CctxStatus.Status) + require.Equal(t, "deposit error", cctx.CctxStatus.StatusMessage) + }) + + t.Run("unable to process zevm deposit HandleEVMDeposit reverts fails at GetSupportedChainFromChainID", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + UseObserverMock: true, + }) + + // Setup mock data + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + observerMock := keepertest.GetCrosschainObserverMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain(t) + errDeposit := fmt.Errorf("deposit failed") + + // Setup expected calls + // mock unsuccessful HandleEVMDeposit which reverts , i.e returns err and isContractReverted = true + keepertest.MockRevertForHandleEVMDeposit(fungibleMock, receiver, amount, senderChain.ChainId, errDeposit) + + // mock unsuccessful GetSupportedChainFromChainID + observerMock.On("GetSupportedChainFromChainID", mock.Anything, senderChain.ChainId). + Return(nil) + + // call ProcessZEVMDeposit + cctx := GetERC20Cctx(t, receiver, *senderChain, "", amount) + k.ProcessZEVMDeposit(ctx, cctx) + require.Equal(t, types.CctxStatus_Aborted, cctx.CctxStatus.Status) + require.Equal(t, fmt.Sprintf("invalid sender chain id %d", cctx.InboundTxParams.SenderChainId), cctx.CctxStatus.StatusMessage) + }) + + t.Run("unable to process zevm deposit HandleEVMDeposit revert fails at and GetRevertGasLimit", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + UseObserverMock: true, + }) + + // Setup mock data + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + observerMock := keepertest.GetCrosschainObserverMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain(t) + asset := "" + errDeposit := fmt.Errorf("deposit failed") + + // Setup expected calls + keepertest.MockRevertForHandleEVMDeposit(fungibleMock, receiver, amount, senderChain.ChainId, errDeposit) + + // Mock successful GetSupportedChainFromChainID + keepertest.MockGetSupportedChainFromChainID(observerMock, senderChain) + + // mock unsuccessful GetRevertGasLimit for ERC20 + fungibleMock.On("GetForeignCoinFromAsset", mock.Anything, asset, senderChain.ChainId). + Return(fungibletypes.ForeignCoins{}, false) + + // call ProcessZEVMDeposit + cctx := GetERC20Cctx(t, receiver, *senderChain, asset, amount) + k.ProcessZEVMDeposit(ctx, cctx) + require.Equal(t, types.CctxStatus_Aborted, cctx.CctxStatus.Status) + require.Equal(t, fmt.Sprintf("revert gas limit error: %s", types.ErrForeignCoinNotFound), cctx.CctxStatus.StatusMessage) + }) + + t.Run("unable to process zevm deposit HandleEVMDeposit revert fails at PayGasInERC20AndUpdateCctx", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + UseObserverMock: true, + }) + + // Setup mock data + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain(t) + asset := "" + + // Setup expected calls + errDeposit := fmt.Errorf("deposit failed") + keepertest.MockRevertForHandleEVMDeposit(fungibleMock, receiver, amount, senderChain.ChainId, errDeposit) + + observerMock := keepertest.GetCrosschainObserverMock(t, k) + + // Mock successful GetSupportedChainFromChainID + keepertest.MockGetSupportedChainFromChainID(observerMock, senderChain) + + // mock successful GetRevertGasLimit for ERC20 + keepertest.MockGetRevertGasLimitForERC20(fungibleMock, asset, *senderChain) + + // mock unsuccessful PayGasInERC20AndUpdateCctx + observerMock.On("GetSupportedChainFromChainID", mock.Anything, senderChain.ChainId). + Return(nil).Once() + + // call ProcessZEVMDeposit + cctx := GetERC20Cctx(t, receiver, *senderChain, asset, amount) + k.ProcessZEVMDeposit(ctx, cctx) + require.Equal(t, types.CctxStatus_Aborted, cctx.CctxStatus.Status) + require.Equal(t, fmt.Sprintf("deposit revert message: %s err : %s", errDeposit, observertypes.ErrSupportedChains), cctx.CctxStatus.StatusMessage) + }) + + t.Run("unable to process zevm deposit HandleEVMDeposit reverts fails at UpdateNonce", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + UseObserverMock: true, + }) + + // Setup mock data + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + observerMock := keepertest.GetCrosschainObserverMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain(t) + asset := "" + errDeposit := fmt.Errorf("deposit failed") + + // Setup expected calls + // mock unsuccessful HandleEVMDeposit which reverts , i.e returns err and isContractReverted = true + keepertest.MockRevertForHandleEVMDeposit(fungibleMock, receiver, amount, senderChain.ChainId, errDeposit) + + // Mock successful GetSupportedChainFromChainID + keepertest.MockGetSupportedChainFromChainID(observerMock, senderChain) + + // mock successful GetRevertGasLimit for ERC20 + keepertest.MockGetRevertGasLimitForERC20(fungibleMock, asset, *senderChain) + + // mock successful PayGasAndUpdateCctx + keepertest.MockPayGasAndUpdateCCTX(fungibleMock, observerMock, ctx, *k, *senderChain, asset) + + // Mock unsuccessful UpdateNonce + observerMock.On("GetChainNonces", mock.Anything, senderChain.ChainName.String()). + Return(observertypes.ChainNonces{}, false) + + // call ProcessZEVMDeposit + cctx := GetERC20Cctx(t, receiver, *senderChain, asset, amount) + k.ProcessZEVMDeposit(ctx, cctx) + require.Equal(t, types.CctxStatus_Aborted, cctx.CctxStatus.Status) + require.Contains(t, cctx.CctxStatus.StatusMessage, "cannot find receiver chain nonce") + }) + + t.Run("unable to process zevm deposit HandleEVMDeposit revert successfully", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + UseObserverMock: true, + }) + + // Setup mock data + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + observerMock := keepertest.GetCrosschainObserverMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain(t) + asset := "" + errDeposit := fmt.Errorf("deposit failed") + + // Setup expected calls + // mock unsuccessful HandleEVMDeposit which reverts , i.e returns err and isContractReverted = true + keepertest.MockRevertForHandleEVMDeposit(fungibleMock, receiver, amount, senderChain.ChainId, errDeposit) + + // Mock successful GetSupportedChainFromChainID + keepertest.MockGetSupportedChainFromChainID(observerMock, senderChain) + + // mock successful GetRevertGasLimit for ERC20 + keepertest.MockGetRevertGasLimitForERC20(fungibleMock, asset, *senderChain) + + // mock successful PayGasAndUpdateCctx + keepertest.MockPayGasAndUpdateCCTX(fungibleMock, observerMock, ctx, *k, *senderChain, asset) + // mock successful UpdateNonce + updatedNonce := keepertest.MockUpdateNonce(observerMock, *senderChain) + + // call ProcessZEVMDeposit + cctx := GetERC20Cctx(t, receiver, *senderChain, asset, amount) + k.ProcessZEVMDeposit(ctx, cctx) + require.Equal(t, types.CctxStatus_PendingRevert, cctx.CctxStatus.Status) + require.Equal(t, errDeposit.Error(), cctx.CctxStatus.StatusMessage) + require.Equal(t, updatedNonce, cctx.GetCurrentOutTxParam().OutboundTxTssNonce) + }) + + t.Run("unable to process zevm deposit HandleEVMDeposit revert fails as the cctx has already been reverted", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + UseObserverMock: true, + }) + + // Setup mock data + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + observerMock := keepertest.GetCrosschainObserverMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain(t) + asset := "" + errDeposit := fmt.Errorf("deposit failed") + + // Setup expected calls + // mock unsuccessful HandleEVMDeposit which reverts , i.e returns err and isContractReverted = true + keepertest.MockRevertForHandleEVMDeposit(fungibleMock, receiver, amount, senderChain.ChainId, errDeposit) + + // Mock successful GetSupportedChainFromChainID + keepertest.MockGetSupportedChainFromChainID(observerMock, senderChain) + + // mock successful GetRevertGasLimit for ERC20 + keepertest.MockGetRevertGasLimitForERC20(fungibleMock, asset, *senderChain) + + // call ProcessZEVMDeposit + cctx := GetERC20Cctx(t, receiver, *senderChain, asset, amount) + cctx.OutboundTxParams = append(cctx.OutboundTxParams, cctx.GetCurrentOutTxParam()) + k.ProcessZEVMDeposit(ctx, cctx) + require.Equal(t, types.CctxStatus_Aborted, cctx.CctxStatus.Status) + require.Contains(t, cctx.CctxStatus.StatusMessage, fmt.Sprintf("revert outbound error: %s", "cannot revert a revert tx")) + }) +} + +func TestKeeper_ProcessCrosschainMsgPassing(t *testing.T) { + t.Run("process crosschain msg passing successfully", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + UseObserverMock: true, + }) + + // Setup mock data + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + observerMock := keepertest.GetCrosschainObserverMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + receiverChain := getValidEthChain(t) + + // mock successful PayGasAndUpdateCctx + keepertest.MockPayGasAndUpdateCCTX(fungibleMock, observerMock, ctx, *k, *receiverChain, "") + + // mock successful UpdateNonce + updatedNonce := keepertest.MockUpdateNonce(observerMock, *receiverChain) + + // call ProcessCrosschainMsgPassing + cctx := GetERC20Cctx(t, receiver, *receiverChain, "", amount) + k.ProcessCrosschainMsgPassing(ctx, cctx) + require.Equal(t, types.CctxStatus_PendingOutbound, cctx.CctxStatus.Status) + require.Equal(t, updatedNonce, cctx.GetCurrentOutTxParam().OutboundTxTssNonce) + }) + + t.Run("unable to process crosschain msg passing PayGasAndUpdateCctx fails", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + UseObserverMock: true, + }) + + // Setup mock data + observerMock := keepertest.GetCrosschainObserverMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + receiverChain := getValidEthChain(t) + + // mock unsuccessful PayGasAndUpdateCctx + observerMock.On("GetSupportedChainFromChainID", mock.Anything, receiverChain.ChainId). + Return(nil).Once() + + // call ProcessCrosschainMsgPassing + cctx := GetERC20Cctx(t, receiver, *receiverChain, "", amount) + k.ProcessCrosschainMsgPassing(ctx, cctx) + require.Equal(t, types.CctxStatus_Aborted, cctx.CctxStatus.Status) + require.Equal(t, observertypes.ErrSupportedChains.Error(), cctx.CctxStatus.StatusMessage) + }) + + t.Run("unable to process crosschain msg passing UpdateNonce fails", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + UseObserverMock: true, + }) + + // Setup mock data + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + observerMock := keepertest.GetCrosschainObserverMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + receiverChain := getValidEthChain(t) + + // mock successful PayGasAndUpdateCctx + keepertest.MockPayGasAndUpdateCCTX(fungibleMock, observerMock, ctx, *k, *receiverChain, "") + + // mock unsuccessful UpdateNonce + observerMock.On("GetChainNonces", mock.Anything, receiverChain.ChainName.String()). + Return(observertypes.ChainNonces{}, false) + + // call ProcessCrosschainMsgPassing + cctx := GetERC20Cctx(t, receiver, *receiverChain, "", amount) + k.ProcessCrosschainMsgPassing(ctx, cctx) + require.Equal(t, types.CctxStatus_Aborted, cctx.CctxStatus.Status) + require.Contains(t, cctx.CctxStatus.StatusMessage, "cannot find receiver chain nonce") + }) +} diff --git a/x/crosschain/keeper/process_outbound_test.go b/x/crosschain/keeper/process_outbound_test.go new file mode 100644 index 0000000000..23877e14f7 --- /dev/null +++ b/x/crosschain/keeper/process_outbound_test.go @@ -0,0 +1,289 @@ +package keeper_test + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/pkg/coin" + keepertest "github.com/zeta-chain/zetacore/testutil/keeper" + "github.com/zeta-chain/zetacore/testutil/sample" + "github.com/zeta-chain/zetacore/x/crosschain/types" + fungibletypes "github.com/zeta-chain/zetacore/x/fungible/types" + observertypes "github.com/zeta-chain/zetacore/x/observer/types" +) + +func TestKeeper_ProcessSuccessfulOutbound(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + cctx := sample.CrossChainTx(t, "test") + // transition to reverted if pending revert + cctx.CctxStatus.Status = types.CctxStatus_PendingRevert + k.ProcessSuccessfulOutbound(ctx, cctx, sample.String()) + require.Equal(t, cctx.CctxStatus.Status, types.CctxStatus_Reverted) + // transition to outbound mined if pending outbound + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + k.ProcessSuccessfulOutbound(ctx, cctx, sample.String()) + require.Equal(t, cctx.CctxStatus.Status, types.CctxStatus_OutboundMined) + // do nothing if it's in any other state + k.ProcessSuccessfulOutbound(ctx, cctx, sample.String()) + require.Equal(t, cctx.CctxStatus.Status, types.CctxStatus_OutboundMined) +} + +func TestKeeper_ProcessFailedOutbound(t *testing.T) { + t.Run("successfully process failed outbound set to aborted for type cmd", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + cctx := sample.CrossChainTx(t, "test") + cctx.InboundTxParams.CoinType = coin.CoinType_Cmd + err := k.ProcessFailedOutbound(ctx, cctx, sample.String()) + require.NoError(t, err) + require.Equal(t, cctx.CctxStatus.Status, types.CctxStatus_Aborted) + require.Equal(t, cctx.GetCurrentOutTxParam().TxFinalizationStatus, types.TxFinalizationStatus_Executed) + }) + + t.Run("successfully process failed outbound set to aborted for withdraw tx", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + cctx := sample.CrossChainTx(t, "test") + cctx.InboundTxParams.SenderChainId = chains.ZetaChainMainnet().ChainId + err := k.ProcessFailedOutbound(ctx, cctx, sample.String()) + require.NoError(t, err) + require.Equal(t, cctx.CctxStatus.Status, types.CctxStatus_Aborted) + require.Equal(t, cctx.GetCurrentOutTxParam().TxFinalizationStatus, types.TxFinalizationStatus_Executed) + }) + + t.Run("successfully process failed outbound set to pending revert", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + UseObserverMock: true, + }) + + // Setup mock data + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + observerMock := keepertest.GetCrosschainObserverMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain(t) + asset := "" + + // mock successful GetRevertGasLimit for ERC20 + keepertest.MockGetRevertGasLimitForERC20(fungibleMock, asset, *senderChain) + + // mock successful PayGasAndUpdateCctx + keepertest.MockPayGasAndUpdateCCTX(fungibleMock, observerMock, ctx, *k, *senderChain, asset) + + // mock successful UpdateNonce + _ = keepertest.MockUpdateNonce(observerMock, *senderChain) + + cctx := GetERC20Cctx(t, receiver, *senderChain, asset, amount) + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + err := k.ProcessFailedOutbound(ctx, cctx, sample.String()) + require.NoError(t, err) + require.Equal(t, cctx.CctxStatus.Status, types.CctxStatus_PendingRevert) + require.Equal(t, types.TxFinalizationStatus_NotFinalized, cctx.GetCurrentOutTxParam().TxFinalizationStatus) + require.Equal(t, types.TxFinalizationStatus_Executed, cctx.OutboundTxParams[0].TxFinalizationStatus) + + }) + + t.Run("unable to process revert when update nonce fails", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + UseObserverMock: true, + }) + + // Setup mock data + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + observerMock := keepertest.GetCrosschainObserverMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain(t) + asset := "" + + // mock successful GetRevertGasLimit for ERC20 + keepertest.MockGetRevertGasLimitForERC20(fungibleMock, asset, *senderChain) + + // mock successful PayGasAndUpdateCctx + keepertest.MockPayGasAndUpdateCCTX(fungibleMock, observerMock, ctx, *k, *senderChain, asset) + + // mock failed UpdateNonce + observerMock.On("GetChainNonces", mock.Anything, senderChain.ChainName.String()). + Return(observertypes.ChainNonces{}, false) + + cctx := GetERC20Cctx(t, receiver, *senderChain, asset, amount) + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + err := k.ProcessFailedOutbound(ctx, cctx, sample.String()) + require.ErrorIs(t, err, types.ErrCannotFindReceiverNonce) + require.Equal(t, cctx.CctxStatus.Status, types.CctxStatus_PendingOutbound) + }) + + t.Run("unable to process revert when PayGasAndUpdateCctx fails", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + UseObserverMock: true, + }) + + // Setup mock data + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + observerMock := keepertest.GetCrosschainObserverMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain(t) + asset := "" + + // mock successful GetRevertGasLimit for ERC20 + keepertest.MockGetRevertGasLimitForERC20(fungibleMock, asset, *senderChain) + + // mock successful PayGasAndUpdateCctx + observerMock.On("GetSupportedChainFromChainID", mock.Anything, senderChain.ChainId). + Return(nil).Once() + + cctx := GetERC20Cctx(t, receiver, *senderChain, asset, amount) + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + err := k.ProcessFailedOutbound(ctx, cctx, sample.String()) + require.ErrorIs(t, err, observertypes.ErrSupportedChains) + require.Equal(t, cctx.CctxStatus.Status, types.CctxStatus_PendingOutbound) + }) + + t.Run("unable to process revert when GetRevertGasLimit fails", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + }) + + // Setup mock data + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain(t) + asset := "" + + // mock failed GetRevertGasLimit for ERC20 + fungibleMock.On("GetForeignCoinFromAsset", mock.Anything, asset, senderChain.ChainId). + Return(fungibletypes.ForeignCoins{ + Zrc20ContractAddress: sample.EthAddress().String(), + }, false).Once() + + cctx := GetERC20Cctx(t, receiver, *senderChain, asset, amount) + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + err := k.ProcessFailedOutbound(ctx, cctx, sample.String()) + require.ErrorIs(t, err, types.ErrForeignCoinNotFound) + require.Equal(t, cctx.CctxStatus.Status, types.CctxStatus_PendingOutbound) + }) +} + +func TestKeeper_ProcessOutbound(t *testing.T) { + t.Run("successfully process outbound with ballot finalized to success", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + cctx := GetERC20Cctx(t, sample.EthAddress(), chains.GoerliChain(), "", big.NewInt(42)) + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + err := k.ProcessOutbound(ctx, cctx, observertypes.BallotStatus_BallotFinalized_SuccessObservation, sample.String()) + require.NoError(t, err) + require.Equal(t, cctx.CctxStatus.Status, types.CctxStatus_OutboundMined) + }) + + t.Run("successfully process outbound with ballot finalized to failed and old status is Pending Revert", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + cctx := GetERC20Cctx(t, sample.EthAddress(), chains.GoerliChain(), "", big.NewInt(42)) + cctx.CctxStatus.Status = types.CctxStatus_PendingRevert + err := k.ProcessOutbound(ctx, cctx, observertypes.BallotStatus_BallotFinalized_FailureObservation, sample.String()) + require.NoError(t, err) + require.Equal(t, cctx.CctxStatus.Status, types.CctxStatus_Aborted) + require.Equal(t, cctx.GetCurrentOutTxParam().TxFinalizationStatus, types.TxFinalizationStatus_Executed) + }) + + t.Run("successfully process outbound with ballot finalized to failed and coin-type is CMD", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeper(t) + cctx := GetERC20Cctx(t, sample.EthAddress(), chains.GoerliChain(), "", big.NewInt(42)) + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + cctx.InboundTxParams.CoinType = coin.CoinType_Cmd + err := k.ProcessOutbound(ctx, cctx, observertypes.BallotStatus_BallotFinalized_FailureObservation, sample.String()) + require.NoError(t, err) + require.Equal(t, cctx.CctxStatus.Status, types.CctxStatus_Aborted) + require.Equal(t, cctx.GetCurrentOutTxParam().TxFinalizationStatus, types.TxFinalizationStatus_Executed) + }) + + t.Run("do not process outbound on error, no new outbound created", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + }) + + // Setup mock data + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain(t) + asset := "" + + cctx := GetERC20Cctx(t, receiver, *senderChain, asset, amount) + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + oldOutTxParamsLen := len(cctx.OutboundTxParams) + // mock failed GetRevertGasLimit for ERC20 + fungibleMock.On("GetForeignCoinFromAsset", mock.Anything, asset, senderChain.ChainId). + Return(fungibletypes.ForeignCoins{ + Zrc20ContractAddress: sample.EthAddress().String(), + }, false).Once() + + err := k.ProcessOutbound(ctx, cctx, observertypes.BallotStatus_BallotFinalized_FailureObservation, sample.String()) + require.ErrorIs(t, err, types.ErrForeignCoinNotFound) + require.Equal(t, cctx.CctxStatus.Status, types.CctxStatus_PendingOutbound) + // New outbound not added and the old outbound is not finalized + require.Len(t, cctx.OutboundTxParams, oldOutTxParamsLen) + require.Equal(t, cctx.GetCurrentOutTxParam().TxFinalizationStatus, types.TxFinalizationStatus_NotFinalized) + }) + + t.Run("do not process outbound if the cctx has already been reverted once", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + }) + + // Setup mock data + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain(t) + asset := "" + + cctx := GetERC20Cctx(t, receiver, *senderChain, asset, amount) + cctx.OutboundTxParams = append(cctx.OutboundTxParams, sample.OutboundTxParams(sample.Rand())) + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + // mock successful GetRevertGasLimit for ERC20 + keepertest.MockGetRevertGasLimitForERC20(fungibleMock, asset, *senderChain) + + err := k.ProcessOutbound(ctx, cctx, observertypes.BallotStatus_BallotFinalized_FailureObservation, sample.String()) + require.ErrorContains(t, err, "cannot revert a revert") + }) + + t.Run("successfully revert a outbound and create a new revert tx", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + UseObserverMock: true, + }) + + // Setup mock data + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + observerMock := keepertest.GetCrosschainObserverMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + senderChain := getValidEthChain(t) + asset := "" + + cctx := GetERC20Cctx(t, receiver, *senderChain, asset, amount) + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + oldOutTxParamsLen := len(cctx.OutboundTxParams) + // mock successful GetRevertGasLimit for ERC20 + keepertest.MockGetRevertGasLimitForERC20(fungibleMock, asset, *senderChain) + + // mock successful PayGasAndUpdateCctx + keepertest.MockPayGasAndUpdateCCTX(fungibleMock, observerMock, ctx, *k, *senderChain, asset) + + // mock successful UpdateNonce + _ = keepertest.MockUpdateNonce(observerMock, *senderChain) + + err := k.ProcessOutbound(ctx, cctx, observertypes.BallotStatus_BallotFinalized_FailureObservation, sample.String()) + require.NoError(t, err) + require.Equal(t, cctx.CctxStatus.Status, types.CctxStatus_PendingRevert) + // New outbound added for revert and the old outbound is finalized + require.Len(t, cctx.OutboundTxParams, oldOutTxParamsLen+1) + require.Equal(t, cctx.GetCurrentOutTxParam().TxFinalizationStatus, types.TxFinalizationStatus_NotFinalized) + require.Equal(t, cctx.OutboundTxParams[oldOutTxParamsLen-1].TxFinalizationStatus, types.TxFinalizationStatus_Executed) + }) +} diff --git a/x/crosschain/keeper/processs_outbound.go b/x/crosschain/keeper/processs_outbound.go new file mode 100644 index 0000000000..b10df821cc --- /dev/null +++ b/x/crosschain/keeper/processs_outbound.go @@ -0,0 +1,127 @@ +package keeper + +import ( + cosmoserrors "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/pkg/coin" + "github.com/zeta-chain/zetacore/x/crosschain/types" + observertypes "github.com/zeta-chain/zetacore/x/observer/types" +) + +/* ProcessSuccessfulOutbound processes a successful outbound transaction. It does the following things in one function: + + 1. Change the status of the CCTX from + - PendingRevert to Reverted + - PendingOutbound to OutboundMined + + 2. Set the finalization status of the current outbound tx to executed + + 3. Emit an event for the successful outbound transaction +*/ + +func (k Keeper) ProcessSuccessfulOutbound(ctx sdk.Context, cctx *types.CrossChainTx, valueReceived string) { + oldStatus := cctx.CctxStatus.Status + switch oldStatus { + case types.CctxStatus_PendingRevert: + cctx.SetReverted("Outbound succeeded, revert executed") + case types.CctxStatus_PendingOutbound: + cctx.SetOutBoundMined("Outbound succeeded, mined") + default: + return + } + cctx.GetCurrentOutTxParam().TxFinalizationStatus = types.TxFinalizationStatus_Executed + newStatus := cctx.CctxStatus.Status.String() + EmitOutboundSuccess(ctx, valueReceived, oldStatus.String(), newStatus, cctx.Index) +} + +/* +ProcessFailedOutbound processes a failed outbound transaction. It does the following things in one function: + + 1. For Admin Tx or a withdrawal from Zeta chain, it aborts the CCTX + + 2. For other CCTX + - If the CCTX is in PendingOutbound, it creates a revert tx and sets the finalization status of the current outbound tx to executed + - If the CCTX is in PendingRevert, it sets the Status to Aborted + + 3. Emit an event for the failed outbound transaction + + 4. Set the finalization status of the current outbound tx to executed. If a revert tx is is created, the finalization status is not set, it would get set when the revert is processed via a subsequent transaction +*/ +func (k Keeper) ProcessFailedOutbound(ctx sdk.Context, cctx *types.CrossChainTx, valueReceived string) error { + oldStatus := cctx.CctxStatus.Status + if cctx.InboundTxParams.CoinType == coin.CoinType_Cmd || chains.IsZetaChain(cctx.InboundTxParams.SenderChainId) { + // if the cctx is of coin type cmd or the sender chain is zeta chain, then we do not revert, the cctx is aborted + cctx.GetCurrentOutTxParam().TxFinalizationStatus = types.TxFinalizationStatus_Executed + cctx.CctxStatus.ChangeStatus(types.CctxStatus_Aborted, "") + } else { + switch oldStatus { + case types.CctxStatus_PendingOutbound: + + gasLimit, err := k.GetRevertGasLimit(ctx, *cctx) + if err != nil { + return cosmoserrors.Wrap(err, "GetRevertGasLimit") + } + if gasLimit == 0 { + // use same gas limit of outbound as a fallback -- should not happen + gasLimit = cctx.OutboundTxParams[0].OutboundTxGasLimit + } + + // create new OutboundTxParams for the revert + err = cctx.AddRevertOutbound(gasLimit) + if err != nil { + return cosmoserrors.Wrap(err, "AddRevertOutbound") + } + + err = k.PayGasAndUpdateCctx( + ctx, + cctx.InboundTxParams.SenderChainId, + cctx, + cctx.OutboundTxParams[0].Amount, + false, + ) + if err != nil { + return err + } + err = k.UpdateNonce(ctx, cctx.InboundTxParams.SenderChainId, cctx) + if err != nil { + return err + } + // Not setting the finalization status here, the required changes have been made while creating the revert tx + cctx.SetPendingRevert("Outbound failed, start revert") + case types.CctxStatus_PendingRevert: + cctx.GetCurrentOutTxParam().TxFinalizationStatus = types.TxFinalizationStatus_Executed + cctx.SetAbort("Outbound failed: revert failed; abort TX") + } + } + newStatus := cctx.CctxStatus.Status.String() + EmitOutboundFailure(ctx, valueReceived, oldStatus.String(), newStatus, cctx.Index) + return nil +} + +// ProcessOutbound processes the finalization of an outbound transaction based on the ballot status +// The state is committed only if the individual steps are successful +func (k Keeper) ProcessOutbound(ctx sdk.Context, cctx *types.CrossChainTx, ballotStatus observertypes.BallotStatus, valueReceived string) error { + tmpCtx, commit := ctx.CacheContext() + err := func() error { + switch ballotStatus { + case observertypes.BallotStatus_BallotFinalized_SuccessObservation: + k.ProcessSuccessfulOutbound(tmpCtx, cctx, valueReceived) + case observertypes.BallotStatus_BallotFinalized_FailureObservation: + err := k.ProcessFailedOutbound(tmpCtx, cctx, valueReceived) + if err != nil { + return err + } + } + return nil + }() + if err != nil { + return err + } + err = cctx.Validate() + if err != nil { + return err + } + commit() + return nil +} diff --git a/x/crosschain/keeper/refund_test.go b/x/crosschain/keeper/refund_test.go index 933359008f..b1b893f71e 100644 --- a/x/crosschain/keeper/refund_test.go +++ b/x/crosschain/keeper/refund_test.go @@ -139,7 +139,7 @@ func TestKeeper_RefundAmountOnZetaChainZeta(t *testing.T) { sender, ) require.NoError(t, err) - coin := sdkk.BankKeeper.GetBalance(ctx, sdk.AccAddress(sender.Bytes()), config.BaseDenom) + coin := sdkk.BankKeeper.GetBalance(ctx, sender.Bytes(), config.BaseDenom) fmt.Println(coin.Amount.String()) require.Equal(t, "42", coin.Amount.String()) }) @@ -210,6 +210,7 @@ func TestKeeper_RefundAmountOnZetaChainERC20(t *testing.T) { ) err := k.RefundAmountOnZetaChainERC20(ctx, types.CrossChainTx{ + InboundTxParams: &types.InboundTxParams{ CoinType: coin.CoinType_ERC20, SenderChainId: chainID, @@ -232,6 +233,7 @@ func TestKeeper_RefundAmountOnZetaChainERC20(t *testing.T) { // can refund again err = k.RefundAmountOnZetaChainERC20(ctx, types.CrossChainTx{ + InboundTxParams: &types.InboundTxParams{ CoinType: coin.CoinType_ERC20, SenderChainId: chainID, @@ -251,6 +253,7 @@ func TestKeeper_RefundAmountOnZetaChainERC20(t *testing.T) { k, ctx, _, _ := keepertest.CrosschainKeeper(t) err := k.RefundAmountOnZetaChainERC20(ctx, types.CrossChainTx{ + InboundTxParams: &types.InboundTxParams{ CoinType: coin.CoinType_Zeta, Amount: math.NewUint(42), diff --git a/x/crosschain/migrations/v4/migrate.go b/x/crosschain/migrations/v4/migrate.go index da289e062e..8682042fcb 100644 --- a/x/crosschain/migrations/v4/migrate.go +++ b/x/crosschain/migrations/v4/migrate.go @@ -55,7 +55,7 @@ func SetZetaAccounting( for ; iterator.Valid(); iterator.Next() { var val types.CrossChainTx cdc.MustUnmarshal(iterator.Value(), &val) - if val.CctxStatus.Status == types.CctxStatus_Aborted && val.GetCurrentOutTxParam().CoinType == coin.CoinType_Zeta { + if val.CctxStatus.Status == types.CctxStatus_Aborted && val.InboundTxParams.CoinType == coin.CoinType_Zeta { abortedAmountZeta = abortedAmountZeta.Add(val.GetCurrentOutTxParam().Amount) } } diff --git a/x/crosschain/migrations/v4/migrate_test.go b/x/crosschain/migrations/v4/migrate_test.go index c4c68d93a5..4ba61e399a 100644 --- a/x/crosschain/migrations/v4/migrate_test.go +++ b/x/crosschain/migrations/v4/migrate_test.go @@ -169,6 +169,9 @@ func SetRandomCctx(ctx sdk.Context, k keeper.Keeper) sdkmath.Uint { k.SetCrossChainTx(ctx, types.CrossChainTx{ Index: fmt.Sprintf("%d", i), CctxStatus: &types.Status{Status: types.CctxStatus_Aborted}, + InboundTxParams: &types.InboundTxParams{ + CoinType: coin.CoinType_Zeta, + }, OutboundTxParams: []*types.OutboundTxParams{{ Amount: amount, CoinType: coin.CoinType_Zeta, diff --git a/x/crosschain/migrations/v5/migrate_test.go b/x/crosschain/migrations/v5/migrate_test.go index 3e9e535fda..3bed70bcb1 100644 --- a/x/crosschain/migrations/v5/migrate_test.go +++ b/x/crosschain/migrations/v5/migrate_test.go @@ -25,7 +25,7 @@ func TestMigrateStore(t *testing.T) { v4ZetaAccountingAmount := math.ZeroUint() for _, cctx := range cctxList { k.SetCrossChainTx(ctx, cctx) - if cctx.CctxStatus.Status != crosschaintypes.CctxStatus_Aborted || cctx.GetCurrentOutTxParam().CoinType != coin.CoinType_Zeta { + if cctx.CctxStatus.Status != crosschaintypes.CctxStatus_Aborted || cctx.InboundTxParams.CoinType != coin.CoinType_Zeta { continue } v5ZetaAccountingAmount = v5ZetaAccountingAmount.Add(crosschainkeeper.GetAbortedAmount(cctx)) diff --git a/x/crosschain/module.go b/x/crosschain/module.go index 7c81f220e8..45d23053da 100644 --- a/x/crosschain/module.go +++ b/x/crosschain/module.go @@ -154,6 +154,7 @@ func (am AppModule) RegisterServices(cfg module.Configurator) { if err := cfg.RegisterMigration(types.ModuleName, 4, m.Migrate4to5); err != nil { panic(err) } + } // RegisterInvariants registers the crosschain module's invariants. diff --git a/x/crosschain/types/authz.go b/x/crosschain/types/authz.go new file mode 100644 index 0000000000..7733a34d7a --- /dev/null +++ b/x/crosschain/types/authz.go @@ -0,0 +1,19 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + observertypes "github.com/zeta-chain/zetacore/x/observer/types" +) + +// GetAllAuthzZetaclientTxTypes returns all the authz types for required for zetaclient +func GetAllAuthzZetaclientTxTypes() []string { + return []string{ + sdk.MsgTypeURL(&MsgGasPriceVoter{}), + sdk.MsgTypeURL(&MsgVoteOnObservedInboundTx{}), + sdk.MsgTypeURL(&MsgVoteOnObservedOutboundTx{}), + sdk.MsgTypeURL(&MsgCreateTSSVoter{}), + sdk.MsgTypeURL(&MsgAddToOutTxTracker{}), + sdk.MsgTypeURL(&observertypes.MsgAddBlameVote{}), + sdk.MsgTypeURL(&observertypes.MsgAddBlockHeader{}), + } +} diff --git a/x/crosschain/types/authz_test.go b/x/crosschain/types/authz_test.go new file mode 100644 index 0000000000..9846949636 --- /dev/null +++ b/x/crosschain/types/authz_test.go @@ -0,0 +1,19 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" +) + +func TestGetAllAuthzZetaclientTxTypes(t *testing.T) { + require.Equal(t, []string{"/zetachain.zetacore.crosschain.MsgGasPriceVoter", + "/zetachain.zetacore.crosschain.MsgVoteOnObservedInboundTx", + "/zetachain.zetacore.crosschain.MsgVoteOnObservedOutboundTx", + "/zetachain.zetacore.crosschain.MsgCreateTSSVoter", + "/zetachain.zetacore.crosschain.MsgAddToOutTxTracker", + "/zetachain.zetacore.observer.MsgAddBlameVote", + "/zetachain.zetacore.observer.MsgAddBlockHeader"}, + crosschaintypes.GetAllAuthzZetaclientTxTypes()) +} diff --git a/x/crosschain/types/cctx.go b/x/crosschain/types/cctx.go new file mode 100644 index 0000000000..45138232fc --- /dev/null +++ b/x/crosschain/types/cctx.go @@ -0,0 +1,197 @@ +package types + +import ( + "fmt" + + cosmoserrors "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + observertypes "github.com/zeta-chain/zetacore/x/observer/types" +) + +// GetCurrentOutTxParam returns the current outbound tx params. +// There can only be one active outtx. +// OutboundTxParams[0] is the original outtx, if it reverts, then +// OutboundTxParams[1] is the new outtx. +func (m CrossChainTx) GetCurrentOutTxParam() *OutboundTxParams { + if len(m.OutboundTxParams) == 0 { + return &OutboundTxParams{} + } + return m.OutboundTxParams[len(m.OutboundTxParams)-1] +} + +// IsCurrentOutTxRevert returns true if the current outbound tx is the revert tx. +func (m CrossChainTx) IsCurrentOutTxRevert() bool { + return len(m.OutboundTxParams) >= 2 +} + +// OriginalDestinationChainID returns the original destination of the outbound tx, reverted or not +// If there is no outbound tx, return -1 +func (m CrossChainTx) OriginalDestinationChainID() int64 { + if len(m.OutboundTxParams) == 0 { + return -1 + } + return m.OutboundTxParams[0].ReceiverChainId +} + +// Validate checks if the CCTX is valid. +func (m CrossChainTx) Validate() error { + if m.InboundTxParams == nil { + return fmt.Errorf("inbound tx params cannot be nil") + } + if m.OutboundTxParams == nil { + return fmt.Errorf("outbound tx params cannot be nil") + } + if m.CctxStatus == nil { + return fmt.Errorf("cctx status cannot be nil") + } + if len(m.OutboundTxParams) > 2 { + return fmt.Errorf("outbound tx params cannot be more than 2") + } + if m.Index != "" { + err := ValidateZetaIndex(m.Index) + if err != nil { + return err + } + } + err := m.InboundTxParams.Validate() + if err != nil { + return err + } + for _, outboundTxParam := range m.OutboundTxParams { + err = outboundTxParam.Validate() + if err != nil { + return err + } + } + return nil +} + +/* +AddRevertOutbound does the following things in one function: + + 1. create a new OutboundTxParams for the revert + + 2. append the new OutboundTxParams to the current OutboundTxParams + + 3. update the TxFinalizationStatus of the current OutboundTxParams to Executed. +*/ + +func (m *CrossChainTx) AddRevertOutbound(gasLimit uint64) error { + if m.IsCurrentOutTxRevert() { + return fmt.Errorf("cannot revert a revert tx") + } + revertTxParams := &OutboundTxParams{ + Receiver: m.InboundTxParams.Sender, + ReceiverChainId: m.InboundTxParams.SenderChainId, + Amount: m.InboundTxParams.Amount, + OutboundTxGasLimit: gasLimit, + TssPubkey: m.GetCurrentOutTxParam().TssPubkey, + } + // The original outbound has been finalized, the new outbound is pending + m.GetCurrentOutTxParam().TxFinalizationStatus = TxFinalizationStatus_Executed + m.OutboundTxParams = append(m.OutboundTxParams, revertTxParams) + return nil +} + +// AddOutbound adds a new outbound tx to the CCTX. +func (m *CrossChainTx) AddOutbound(ctx sdk.Context, msg MsgVoteOnObservedOutboundTx, ballotStatus observertypes.BallotStatus) error { + if ballotStatus != observertypes.BallotStatus_BallotFinalized_FailureObservation { + if !msg.ValueReceived.Equal(m.GetCurrentOutTxParam().Amount) { + ctx.Logger().Error(fmt.Sprintf("VoteOnObservedOutboundTx: Mint mismatch: %s value received vs %s cctx amount", + msg.ValueReceived, + m.GetCurrentOutTxParam().Amount)) + return cosmoserrors.Wrap(sdkerrors.ErrInvalidRequest, fmt.Sprintf("ValueReceived %s does not match sent value %s", msg.ValueReceived, m.GetCurrentOutTxParam().Amount)) + } + } + // Update CCTX values + m.GetCurrentOutTxParam().OutboundTxHash = msg.ObservedOutTxHash + m.GetCurrentOutTxParam().OutboundTxGasUsed = msg.ObservedOutTxGasUsed + m.GetCurrentOutTxParam().OutboundTxEffectiveGasPrice = msg.ObservedOutTxEffectiveGasPrice + m.GetCurrentOutTxParam().OutboundTxEffectiveGasLimit = msg.ObservedOutTxEffectiveGasLimit + m.GetCurrentOutTxParam().OutboundTxObservedExternalHeight = msg.ObservedOutTxBlockHeight + m.CctxStatus.LastUpdateTimestamp = ctx.BlockHeader().Time.Unix() + return nil +} + +// SetAbort sets the CCTX status to Aborted with the given error message. +func (m CrossChainTx) SetAbort(message string) { + m.CctxStatus.ChangeStatus(CctxStatus_Aborted, message) +} + +// SetPendingRevert sets the CCTX status to PendingRevert with the given error message. +func (m CrossChainTx) SetPendingRevert(message string) { + m.CctxStatus.ChangeStatus(CctxStatus_PendingRevert, message) +} + +// SetPendingOutbound sets the CCTX status to PendingOutbound with the given error message. +func (m CrossChainTx) SetPendingOutbound(message string) { + m.CctxStatus.ChangeStatus(CctxStatus_PendingOutbound, message) +} + +// SetOutBoundMined sets the CCTX status to OutboundMined with the given error message. +func (m CrossChainTx) SetOutBoundMined(message string) { + m.CctxStatus.ChangeStatus(CctxStatus_OutboundMined, message) +} + +// SetReverted sets the CCTX status to Reverted with the given error message. +func (m CrossChainTx) SetReverted(message string) { + m.CctxStatus.ChangeStatus(CctxStatus_Reverted, message) +} + +// NewCCTX creates a new CCTX.From a MsgVoteOnObservedInboundTx message and a TSS pubkey. +// It also validates the created cctx +func NewCCTX(ctx sdk.Context, msg MsgVoteOnObservedInboundTx, tssPubkey string) (CrossChainTx, error) { + index := msg.Digest() + + if msg.TxOrigin == "" { + msg.TxOrigin = msg.Sender + } + inboundParams := &InboundTxParams{ + Sender: msg.Sender, + SenderChainId: msg.SenderChainId, + TxOrigin: msg.TxOrigin, + Asset: msg.Asset, + Amount: msg.Amount, + InboundTxObservedHash: msg.InTxHash, + InboundTxObservedExternalHeight: msg.InBlockHeight, + InboundTxFinalizedZetaHeight: 0, + InboundTxBallotIndex: index, + CoinType: msg.CoinType, + } + + outBoundParams := &OutboundTxParams{ + Receiver: msg.Receiver, + ReceiverChainId: msg.ReceiverChain, + OutboundTxHash: "", + OutboundTxTssNonce: 0, + OutboundTxGasLimit: msg.GasLimit, + OutboundTxGasPrice: "", + OutboundTxBallotIndex: "", + OutboundTxObservedExternalHeight: 0, + Amount: sdkmath.ZeroUint(), + TssPubkey: tssPubkey, + CoinType: msg.CoinType, + } + status := &Status{ + Status: CctxStatus_PendingInbound, + StatusMessage: "", + LastUpdateTimestamp: ctx.BlockHeader().Time.Unix(), + IsAbortRefunded: false, + } + cctx := CrossChainTx{ + Creator: msg.Creator, + Index: index, + ZetaFees: sdkmath.ZeroUint(), + RelayedMessage: msg.Message, + CctxStatus: status, + InboundTxParams: inboundParams, + OutboundTxParams: []*OutboundTxParams{outBoundParams}, + } + err := cctx.Validate() + if err != nil { + return CrossChainTx{}, err + } + return cctx, nil +} diff --git a/x/crosschain/types/cctx_test.go b/x/crosschain/types/cctx_test.go new file mode 100644 index 0000000000..15df60238e --- /dev/null +++ b/x/crosschain/types/cctx_test.go @@ -0,0 +1,298 @@ +package types_test + +import ( + "math/rand" + "testing" + + sdkmath "cosmossdk.io/math" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/pkg/coin" + keepertest "github.com/zeta-chain/zetacore/testutil/keeper" + "github.com/zeta-chain/zetacore/testutil/sample" + "github.com/zeta-chain/zetacore/x/crosschain/types" + observertypes "github.com/zeta-chain/zetacore/x/observer/types" +) + +func Test_InitializeCCTX(t *testing.T) { + t.Run("should return a cctx with correct values", func(t *testing.T) { + _, ctx, _, _ := keepertest.CrosschainKeeper(t) + senderChain := chains.GoerliChain() + sender := sample.EthAddress() + receiverChain := chains.GoerliChain() + receiver := sample.EthAddress() + creator := sample.AccAddress() + amount := sdkmath.NewUint(42) + message := "test" + intxBlockHeight := uint64(420) + intxHash := sample.Hash() + gasLimit := uint64(100) + asset := "test-asset" + eventIndex := uint64(1) + cointType := coin.CoinType_ERC20 + tss := sample.Tss() + msg := types.MsgVoteOnObservedInboundTx{ + Creator: creator, + Sender: sender.String(), + SenderChainId: senderChain.ChainId, + Receiver: receiver.String(), + ReceiverChain: receiverChain.ChainId, + Amount: amount, + Message: message, + InTxHash: intxHash.String(), + InBlockHeight: intxBlockHeight, + GasLimit: gasLimit, + CoinType: cointType, + TxOrigin: sender.String(), + Asset: asset, + EventIndex: eventIndex, + } + cctx, err := types.NewCCTX(ctx, msg, tss.TssPubkey) + require.NoError(t, err) + require.Equal(t, receiver.String(), cctx.GetCurrentOutTxParam().Receiver) + require.Equal(t, receiverChain.ChainId, cctx.GetCurrentOutTxParam().ReceiverChainId) + require.Equal(t, sender.String(), cctx.GetInboundTxParams().Sender) + require.Equal(t, senderChain.ChainId, cctx.GetInboundTxParams().SenderChainId) + require.Equal(t, amount, cctx.GetInboundTxParams().Amount) + require.Equal(t, message, cctx.RelayedMessage) + require.Equal(t, intxHash.String(), cctx.GetInboundTxParams().InboundTxObservedHash) + require.Equal(t, intxBlockHeight, cctx.GetInboundTxParams().InboundTxObservedExternalHeight) + require.Equal(t, gasLimit, cctx.GetCurrentOutTxParam().OutboundTxGasLimit) + require.Equal(t, asset, cctx.GetInboundTxParams().Asset) + require.Equal(t, cointType, cctx.InboundTxParams.CoinType) + require.Equal(t, uint64(0), cctx.GetCurrentOutTxParam().OutboundTxTssNonce) + require.Equal(t, sdkmath.ZeroUint(), cctx.GetCurrentOutTxParam().Amount) + require.Equal(t, types.CctxStatus_PendingInbound, cctx.CctxStatus.Status) + require.Equal(t, false, cctx.CctxStatus.IsAbortRefunded) + }) + t.Run("should return an error if the cctx is invalid", func(t *testing.T) { + _, ctx, _, _ := keepertest.CrosschainKeeper(t) + senderChain := chains.GoerliChain() + sender := sample.EthAddress() + receiverChain := chains.GoerliChain() + receiver := sample.EthAddress() + creator := sample.AccAddress() + amount := sdkmath.NewUint(42) + message := "test" + intxBlockHeight := uint64(420) + intxHash := sample.Hash() + gasLimit := uint64(100) + asset := "test-asset" + eventIndex := uint64(1) + cointType := coin.CoinType_ERC20 + tss := sample.Tss() + msg := types.MsgVoteOnObservedInboundTx{ + Creator: creator, + Sender: "invalid", + SenderChainId: senderChain.ChainId, + Receiver: receiver.String(), + ReceiverChain: receiverChain.ChainId, + Amount: amount, + Message: message, + InTxHash: intxHash.String(), + InBlockHeight: intxBlockHeight, + GasLimit: gasLimit, + CoinType: cointType, + TxOrigin: sender.String(), + Asset: asset, + EventIndex: eventIndex, + } + _, err := types.NewCCTX(ctx, msg, tss.TssPubkey) + require.ErrorContains(t, err, "invalid address") + }) +} + +func TestCrossChainTx_Validate(t *testing.T) { + cctx := sample.CrossChainTx(t, "foo") + cctx.InboundTxParams = nil + require.ErrorContains(t, cctx.Validate(), "inbound tx params cannot be nil") + cctx = sample.CrossChainTx(t, "foo") + cctx.OutboundTxParams = nil + require.ErrorContains(t, cctx.Validate(), "outbound tx params cannot be nil") + cctx = sample.CrossChainTx(t, "foo") + cctx.CctxStatus = nil + require.ErrorContains(t, cctx.Validate(), "cctx status cannot be nil") + cctx = sample.CrossChainTx(t, "foo") + cctx.OutboundTxParams = make([]*types.OutboundTxParams, 3) + require.ErrorContains(t, cctx.Validate(), "outbound tx params cannot be more than 2") + cctx = sample.CrossChainTx(t, "foo") + cctx.Index = "0" + require.ErrorContains(t, cctx.Validate(), "invalid index length 1") + cctx = sample.CrossChainTx(t, "foo") + cctx.InboundTxParams = sample.InboundTxParamsValidChainID(rand.New(rand.NewSource(42))) + cctx.InboundTxParams.SenderChainId = 1000 + require.ErrorContains(t, cctx.Validate(), "invalid sender chain id 1000") + cctx = sample.CrossChainTx(t, "foo") + cctx.OutboundTxParams = []*types.OutboundTxParams{sample.OutboundTxParamsValidChainID(rand.New(rand.NewSource(42)))} + cctx.InboundTxParams = sample.InboundTxParamsValidChainID(rand.New(rand.NewSource(42))) + cctx.InboundTxParams.InboundTxObservedHash = sample.Hash().String() + cctx.InboundTxParams.InboundTxBallotIndex = sample.ZetaIndex(t) + cctx.OutboundTxParams[0].ReceiverChainId = 1000 + require.ErrorContains(t, cctx.Validate(), "invalid receiver chain id 1000") +} + +func TestCrossChainTx_GetCurrentOutTxParam(t *testing.T) { + r := rand.New(rand.NewSource(42)) + cctx := sample.CrossChainTx(t, "foo") + + cctx.OutboundTxParams = []*types.OutboundTxParams{} + require.Equal(t, &types.OutboundTxParams{}, cctx.GetCurrentOutTxParam()) + + cctx.OutboundTxParams = []*types.OutboundTxParams{sample.OutboundTxParams(r)} + require.Equal(t, cctx.OutboundTxParams[0], cctx.GetCurrentOutTxParam()) + + cctx.OutboundTxParams = []*types.OutboundTxParams{sample.OutboundTxParams(r), sample.OutboundTxParams(r)} + require.Equal(t, cctx.OutboundTxParams[1], cctx.GetCurrentOutTxParam()) +} + +func TestCrossChainTx_IsCurrentOutTxRevert(t *testing.T) { + r := rand.New(rand.NewSource(42)) + cctx := sample.CrossChainTx(t, "foo") + + cctx.OutboundTxParams = []*types.OutboundTxParams{} + require.False(t, cctx.IsCurrentOutTxRevert()) + + cctx.OutboundTxParams = []*types.OutboundTxParams{sample.OutboundTxParams(r)} + require.False(t, cctx.IsCurrentOutTxRevert()) + + cctx.OutboundTxParams = []*types.OutboundTxParams{sample.OutboundTxParams(r), sample.OutboundTxParams(r)} + require.True(t, cctx.IsCurrentOutTxRevert()) +} + +func TestCrossChainTx_OriginalDestinationChainID(t *testing.T) { + r := rand.New(rand.NewSource(42)) + cctx := sample.CrossChainTx(t, "foo") + + cctx.OutboundTxParams = []*types.OutboundTxParams{} + require.Equal(t, int64(-1), cctx.OriginalDestinationChainID()) + + cctx.OutboundTxParams = []*types.OutboundTxParams{sample.OutboundTxParams(r)} + require.Equal(t, cctx.OutboundTxParams[0].ReceiverChainId, cctx.OriginalDestinationChainID()) + + cctx.OutboundTxParams = []*types.OutboundTxParams{sample.OutboundTxParams(r), sample.OutboundTxParams(r)} + require.Equal(t, cctx.OutboundTxParams[0].ReceiverChainId, cctx.OriginalDestinationChainID()) +} + +func TestCrossChainTx_AddOutbound(t *testing.T) { + t.Run("successfully get outbound tx", func(t *testing.T) { + _, ctx, _, _ := keepertest.CrosschainKeeper(t) + cctx := sample.CrossChainTx(t, "test") + hash := sample.Hash().String() + + err := cctx.AddOutbound(ctx, types.MsgVoteOnObservedOutboundTx{ + ValueReceived: cctx.GetCurrentOutTxParam().Amount, + ObservedOutTxHash: hash, + ObservedOutTxBlockHeight: 10, + ObservedOutTxGasUsed: 100, + ObservedOutTxEffectiveGasPrice: sdkmath.NewInt(100), + ObservedOutTxEffectiveGasLimit: 20, + }, observertypes.BallotStatus_BallotFinalized_SuccessObservation) + require.NoError(t, err) + require.Equal(t, cctx.GetCurrentOutTxParam().OutboundTxHash, hash) + require.Equal(t, cctx.GetCurrentOutTxParam().OutboundTxGasUsed, uint64(100)) + require.Equal(t, cctx.GetCurrentOutTxParam().OutboundTxEffectiveGasPrice, sdkmath.NewInt(100)) + require.Equal(t, cctx.GetCurrentOutTxParam().OutboundTxEffectiveGasLimit, uint64(20)) + require.Equal(t, cctx.GetCurrentOutTxParam().OutboundTxObservedExternalHeight, uint64(10)) + require.Equal(t, cctx.CctxStatus.LastUpdateTimestamp, ctx.BlockHeader().Time.Unix()) + }) + + t.Run("successfully get outbound tx for failed ballot without amount check", func(t *testing.T) { + _, ctx, _, _ := keepertest.CrosschainKeeper(t) + cctx := sample.CrossChainTx(t, "test") + hash := sample.Hash().String() + + err := cctx.AddOutbound(ctx, types.MsgVoteOnObservedOutboundTx{ + ObservedOutTxHash: hash, + ObservedOutTxBlockHeight: 10, + ObservedOutTxGasUsed: 100, + ObservedOutTxEffectiveGasPrice: sdkmath.NewInt(100), + ObservedOutTxEffectiveGasLimit: 20, + }, observertypes.BallotStatus_BallotFinalized_FailureObservation) + require.NoError(t, err) + require.Equal(t, cctx.GetCurrentOutTxParam().OutboundTxHash, hash) + require.Equal(t, cctx.GetCurrentOutTxParam().OutboundTxGasUsed, uint64(100)) + require.Equal(t, cctx.GetCurrentOutTxParam().OutboundTxEffectiveGasPrice, sdkmath.NewInt(100)) + require.Equal(t, cctx.GetCurrentOutTxParam().OutboundTxEffectiveGasLimit, uint64(20)) + require.Equal(t, cctx.GetCurrentOutTxParam().OutboundTxObservedExternalHeight, uint64(10)) + require.Equal(t, cctx.CctxStatus.LastUpdateTimestamp, ctx.BlockHeader().Time.Unix()) + }) + + t.Run("failed to get outbound tx if amount does not match value received", func(t *testing.T) { + _, ctx, _, _ := keepertest.CrosschainKeeper(t) + + cctx := sample.CrossChainTx(t, "test") + hash := sample.Hash().String() + + err := cctx.AddOutbound(ctx, types.MsgVoteOnObservedOutboundTx{ + ValueReceived: sdkmath.NewUint(100), + ObservedOutTxHash: hash, + ObservedOutTxBlockHeight: 10, + ObservedOutTxGasUsed: 100, + ObservedOutTxEffectiveGasPrice: sdkmath.NewInt(100), + ObservedOutTxEffectiveGasLimit: 20, + }, observertypes.BallotStatus_BallotFinalized_SuccessObservation) + require.ErrorIs(t, err, sdkerrors.ErrInvalidRequest) + }) +} + +func Test_SetRevertOutboundValues(t *testing.T) { + t.Run("successfully set revert outbound values", func(t *testing.T) { + cctx := sample.CrossChainTx(t, "test") + cctx.OutboundTxParams = cctx.OutboundTxParams[:1] + err := cctx.AddRevertOutbound(100) + require.NoError(t, err) + require.Len(t, cctx.OutboundTxParams, 2) + require.Equal(t, cctx.GetCurrentOutTxParam().Receiver, cctx.InboundTxParams.Sender) + require.Equal(t, cctx.GetCurrentOutTxParam().ReceiverChainId, cctx.InboundTxParams.SenderChainId) + require.Equal(t, cctx.GetCurrentOutTxParam().Amount, cctx.InboundTxParams.Amount) + require.Equal(t, cctx.GetCurrentOutTxParam().OutboundTxGasLimit, uint64(100)) + require.Equal(t, cctx.GetCurrentOutTxParam().TssPubkey, cctx.OutboundTxParams[0].TssPubkey) + require.Equal(t, types.TxFinalizationStatus_Executed, cctx.OutboundTxParams[0].TxFinalizationStatus) + }) + + t.Run("failed to set revert outbound values if revert outbound already exists", func(t *testing.T) { + cctx := sample.CrossChainTx(t, "test") + err := cctx.AddRevertOutbound(100) + require.ErrorContains(t, err, "cannot revert a revert tx") + }) +} + +func TestCrossChainTx_SetAbort(t *testing.T) { + cctx := sample.CrossChainTx(t, "test") + cctx.SetAbort("test") + require.Equal(t, types.CctxStatus_Aborted, cctx.CctxStatus.Status) + require.Equal(t, "test", "test") +} + +func TestCrossChainTx_SetPendingRevert(t *testing.T) { + cctx := sample.CrossChainTx(t, "test") + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + cctx.SetPendingRevert("test") + require.Equal(t, types.CctxStatus_PendingRevert, cctx.CctxStatus.Status) + require.Contains(t, cctx.CctxStatus.StatusMessage, "test") +} + +func TestCrossChainTx_SetPendingOutbound(t *testing.T) { + cctx := sample.CrossChainTx(t, "test") + cctx.CctxStatus.Status = types.CctxStatus_PendingInbound + cctx.SetPendingOutbound("test") + require.Equal(t, types.CctxStatus_PendingOutbound, cctx.CctxStatus.Status) + require.Contains(t, cctx.CctxStatus.StatusMessage, "test") +} + +func TestCrossChainTx_SetOutBoundMined(t *testing.T) { + cctx := sample.CrossChainTx(t, "test") + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + cctx.SetOutBoundMined("test") + require.Equal(t, types.CctxStatus_OutboundMined, cctx.CctxStatus.Status) + require.Contains(t, cctx.CctxStatus.StatusMessage, "test") +} + +func TestCrossChainTx_SetReverted(t *testing.T) { + cctx := sample.CrossChainTx(t, "test") + cctx.CctxStatus.Status = types.CctxStatus_PendingRevert + cctx.SetReverted("test") + require.Equal(t, types.CctxStatus_Reverted, cctx.CctxStatus.Status) + require.Contains(t, cctx.CctxStatus.StatusMessage, "test") +} diff --git a/x/crosschain/types/cctx_utils.go b/x/crosschain/types/cctx_utils.go deleted file mode 100644 index bf3357c709..0000000000 --- a/x/crosschain/types/cctx_utils.go +++ /dev/null @@ -1,57 +0,0 @@ -package types - -import ( - "fmt" - "strconv" - - sdk "github.com/cosmos/cosmos-sdk/types" - observertypes "github.com/zeta-chain/zetacore/x/observer/types" -) - -// GetCurrentOutTxParam returns the current outbound tx params. -// There can only be one active outtx. -// OutboundTxParams[0] is the original outtx, if it reverts, then -// OutboundTxParams[1] is the new outtx. -func (m CrossChainTx) GetCurrentOutTxParam() *OutboundTxParams { - if len(m.OutboundTxParams) == 0 { - return &OutboundTxParams{} - } - return m.OutboundTxParams[len(m.OutboundTxParams)-1] -} - -// IsCurrentOutTxRevert returns true if the current outbound tx is the revert tx. -func (m CrossChainTx) IsCurrentOutTxRevert() bool { - return len(m.OutboundTxParams) == 2 -} - -// OriginalDestinationChainID returns the original destination of the outbound tx, reverted or not -// If there is no outbound tx, return -1 -func (m CrossChainTx) OriginalDestinationChainID() int64 { - if len(m.OutboundTxParams) == 0 { - return -1 - } - return m.OutboundTxParams[0].ReceiverChainId -} - -// GetAllAuthzZetaclientTxTypes returns all the authz types for zetaclient -func GetAllAuthzZetaclientTxTypes() []string { - return []string{ - sdk.MsgTypeURL(&MsgGasPriceVoter{}), - sdk.MsgTypeURL(&MsgVoteOnObservedInboundTx{}), - sdk.MsgTypeURL(&MsgVoteOnObservedOutboundTx{}), - sdk.MsgTypeURL(&MsgCreateTSSVoter{}), - sdk.MsgTypeURL(&MsgAddToOutTxTracker{}), - sdk.MsgTypeURL(&observertypes.MsgAddBlameVote{}), - sdk.MsgTypeURL(&observertypes.MsgAddBlockHeader{}), - } -} - -// GetGasPrice returns the gas price of the outbound tx -func (m OutboundTxParams) GetGasPrice() (uint64, error) { - gasPrice, err := strconv.ParseUint(m.OutboundTxGasPrice, 10, 64) - if err != nil { - return 0, fmt.Errorf("unable to parse cctx gas price %s: %s", m.OutboundTxGasPrice, err.Error()) - } - - return gasPrice, nil -} diff --git a/x/crosschain/types/cctx_utils_test.go b/x/crosschain/types/cctx_utils_test.go deleted file mode 100644 index 381ef47f6c..0000000000 --- a/x/crosschain/types/cctx_utils_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package types_test - -import ( - "math/rand" - "testing" - - "github.com/stretchr/testify/require" - "github.com/zeta-chain/zetacore/testutil/sample" - "github.com/zeta-chain/zetacore/x/crosschain/types" -) - -func TestCrossChainTx_GetCurrentOutTxParam(t *testing.T) { - r := rand.New(rand.NewSource(42)) - cctx := sample.CrossChainTx(t, "foo") - - cctx.OutboundTxParams = []*types.OutboundTxParams{} - require.Equal(t, &types.OutboundTxParams{}, cctx.GetCurrentOutTxParam()) - - cctx.OutboundTxParams = []*types.OutboundTxParams{sample.OutboundTxParams(r)} - require.Equal(t, cctx.OutboundTxParams[0], cctx.GetCurrentOutTxParam()) - - cctx.OutboundTxParams = []*types.OutboundTxParams{sample.OutboundTxParams(r), sample.OutboundTxParams(r)} - require.Equal(t, cctx.OutboundTxParams[1], cctx.GetCurrentOutTxParam()) -} - -func TestCrossChainTx_IsCurrentOutTxRevert(t *testing.T) { - r := rand.New(rand.NewSource(42)) - cctx := sample.CrossChainTx(t, "foo") - - cctx.OutboundTxParams = []*types.OutboundTxParams{} - require.False(t, cctx.IsCurrentOutTxRevert()) - - cctx.OutboundTxParams = []*types.OutboundTxParams{sample.OutboundTxParams(r)} - require.False(t, cctx.IsCurrentOutTxRevert()) - - cctx.OutboundTxParams = []*types.OutboundTxParams{sample.OutboundTxParams(r), sample.OutboundTxParams(r)} - require.True(t, cctx.IsCurrentOutTxRevert()) -} - -func TestCrossChainTx_OriginalDestinationChainID(t *testing.T) { - r := rand.New(rand.NewSource(42)) - cctx := sample.CrossChainTx(t, "foo") - - cctx.OutboundTxParams = []*types.OutboundTxParams{} - require.Equal(t, int64(-1), cctx.OriginalDestinationChainID()) - - cctx.OutboundTxParams = []*types.OutboundTxParams{sample.OutboundTxParams(r)} - require.Equal(t, cctx.OutboundTxParams[0].ReceiverChainId, cctx.OriginalDestinationChainID()) - - cctx.OutboundTxParams = []*types.OutboundTxParams{sample.OutboundTxParams(r), sample.OutboundTxParams(r)} - require.Equal(t, cctx.OutboundTxParams[0].ReceiverChainId, cctx.OriginalDestinationChainID()) -} - -func TestOutboundTxParams_GetGasPrice(t *testing.T) { - // #nosec G404 - random seed is not used for security purposes - r := rand.New(rand.NewSource(42)) - outTxParams := sample.OutboundTxParams(r) - - outTxParams.OutboundTxGasPrice = "42" - gasPrice, err := outTxParams.GetGasPrice() - require.NoError(t, err) - require.EqualValues(t, uint64(42), gasPrice) - - outTxParams.OutboundTxGasPrice = "invalid" - _, err = outTxParams.GetGasPrice() - require.Error(t, err) -} diff --git a/x/crosschain/types/errors.go b/x/crosschain/types/errors.go index 2c5a595eb5..62720aaf66 100644 --- a/x/crosschain/types/errors.go +++ b/x/crosschain/types/errors.go @@ -36,7 +36,7 @@ var ( ErrUnsupportedStatus = errorsmod.Register(ModuleName, 1143, "unsupported status") ErrObservedTxAlreadyFinalized = errorsmod.Register(ModuleName, 1144, "observed tx already finalized") ErrInsufficientFundsTssMigration = errorsmod.Register(ModuleName, 1145, "insufficient funds for TSS migration") - ErrInvalidCCTXIndex = errorsmod.Register(ModuleName, 1146, "invalid cctx index") + ErrInvalidIndexValue = errorsmod.Register(ModuleName, 1146, "invalid index hash") ErrInvalidStatus = errorsmod.Register(ModuleName, 1147, "invalid cctx status") ErrUnableProcessRefund = errorsmod.Register(ModuleName, 1148, "unable to process refund") ErrUnableToFindZetaAccounting = errorsmod.Register(ModuleName, 1149, "unable to find zeta accounting") diff --git a/x/crosschain/types/inbound_params.go b/x/crosschain/types/inbound_params.go new file mode 100644 index 0000000000..1cc3264eae --- /dev/null +++ b/x/crosschain/types/inbound_params.go @@ -0,0 +1,42 @@ +package types + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/zeta-chain/zetacore/pkg/chains" +) + +func (m InboundTxParams) Validate() error { + if m.Sender == "" { + return fmt.Errorf("sender cannot be empty") + } + if chains.GetChainFromChainID(m.SenderChainId) == nil { + return fmt.Errorf("invalid sender chain id %d", m.SenderChainId) + } + err := ValidateAddressForChain(m.Sender, m.SenderChainId) + if err != nil { + return err + } + + if m.TxOrigin != "" { + errTxOrigin := ValidateAddressForChain(m.TxOrigin, m.SenderChainId) + if errTxOrigin != nil { + return errTxOrigin + } + } + if m.Amount.IsNil() { + return fmt.Errorf("amount cannot be nil") + } + err = ValidateHashForChain(m.InboundTxObservedHash, m.SenderChainId) + if err != nil { + return errors.Wrap(err, "invalid inbound tx observed hash") + } + if m.InboundTxBallotIndex != "" { + err = ValidateZetaIndex(m.InboundTxBallotIndex) + if err != nil { + return errors.Wrap(err, "invalid inbound tx ballot index") + } + } + return nil +} diff --git a/x/crosschain/types/inbound_params_test.go b/x/crosschain/types/inbound_params_test.go new file mode 100644 index 0000000000..6b6cdf2293 --- /dev/null +++ b/x/crosschain/types/inbound_params_test.go @@ -0,0 +1,43 @@ +package types_test + +import ( + "math/rand" + "testing" + + sdkmath "cosmossdk.io/math" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/testutil/sample" +) + +func TestInboundTxParams_Validate(t *testing.T) { + r := rand.New(rand.NewSource(42)) + inTxParams := sample.InboundTxParamsValidChainID(r) + inTxParams.Sender = "" + require.ErrorContains(t, inTxParams.Validate(), "sender cannot be empty") + inTxParams = sample.InboundTxParamsValidChainID(r) + inTxParams.SenderChainId = 1000 + require.ErrorContains(t, inTxParams.Validate(), "invalid sender chain id 1000") + inTxParams = sample.InboundTxParamsValidChainID(r) + inTxParams.SenderChainId = chains.GoerliChain().ChainId + inTxParams.Sender = "0x123" + require.ErrorContains(t, inTxParams.Validate(), "invalid address 0x123") + inTxParams = sample.InboundTxParamsValidChainID(r) + inTxParams.SenderChainId = chains.GoerliChain().ChainId + inTxParams.TxOrigin = "0x123" + require.ErrorContains(t, inTxParams.Validate(), "invalid address 0x123") + inTxParams = sample.InboundTxParamsValidChainID(r) + inTxParams.Amount = sdkmath.Uint{} + require.ErrorContains(t, inTxParams.Validate(), "amount cannot be nil") + inTxParams = sample.InboundTxParamsValidChainID(r) + inTxParams.InboundTxObservedHash = "12" + require.ErrorContains(t, inTxParams.Validate(), "hash must be a valid ethereum hash 12") + inTxParams = sample.InboundTxParamsValidChainID(r) + inTxParams.InboundTxObservedHash = sample.Hash().String() + inTxParams.InboundTxBallotIndex = "12" + require.ErrorContains(t, inTxParams.Validate(), "invalid index length 2") + inTxParams = sample.InboundTxParamsValidChainID(r) + inTxParams.InboundTxObservedHash = sample.Hash().String() + inTxParams.InboundTxBallotIndex = sample.ZetaIndex(t) + require.NoError(t, inTxParams.Validate()) +} diff --git a/x/crosschain/types/keys.go b/x/crosschain/types/keys.go index 4adac4d73f..719f54687f 100644 --- a/x/crosschain/types/keys.go +++ b/x/crosschain/types/keys.go @@ -27,6 +27,7 @@ const ( ProtocolFee = 2000000000000000000 //TssMigrationGasMultiplierEVM is multiplied to the median gas price to get the gas price for the tss migration . This is done to avoid the tss migration tx getting stuck in the mempool TssMigrationGasMultiplierEVM = "2.5" + ZetaIndexLength = 66 ) func GetProtocolFee() sdk.Uint { @@ -75,7 +76,6 @@ func (m CrossChainTx) LogIdentifierForCCTX() string { i := len(m.OutboundTxParams) - 1 outTx := m.OutboundTxParams[i] return fmt.Sprintf("%s-%d-%d-%d", m.InboundTxParams.Sender, m.InboundTxParams.SenderChainId, outTx.ReceiverChainId, outTx.OutboundTxTssNonce) - } func FinalizedInboundKey(intxHash string, chainID int64, eventIndex uint64) string { diff --git a/x/crosschain/types/message_abort_stuck_cctx.go b/x/crosschain/types/message_abort_stuck_cctx.go index 47c383276b..86a1f31ec6 100644 --- a/x/crosschain/types/message_abort_stuck_cctx.go +++ b/x/crosschain/types/message_abort_stuck_cctx.go @@ -43,8 +43,8 @@ func (msg *MsgAbortStuckCCTX) ValidateBasic() error { if err != nil { return errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid creator address (%s)", err) } - if len(msg.CctxIndex) != 66 { - return ErrInvalidCCTXIndex + if len(msg.CctxIndex) != ZetaIndexLength { + return ErrInvalidIndexValue } return nil } diff --git a/x/crosschain/types/message_abort_stuck_cctx_test.go b/x/crosschain/types/message_abort_stuck_cctx_test.go index 14b1e82d78..5b0d539c0f 100644 --- a/x/crosschain/types/message_abort_stuck_cctx_test.go +++ b/x/crosschain/types/message_abort_stuck_cctx_test.go @@ -24,7 +24,7 @@ func TestMsgAbortStuckCCTX_ValidateBasic(t *testing.T) { { name: "invalid cctx index", msg: types.NewMsgAbortStuckCCTX(sample.AccAddress(), "cctx_index"), - err: types.ErrInvalidCCTXIndex, + err: types.ErrInvalidIndexValue, }, { name: "valid", diff --git a/x/crosschain/types/message_refund_aborted.go b/x/crosschain/types/message_refund_aborted.go index 698d499b09..d019b8ea31 100644 --- a/x/crosschain/types/message_refund_aborted.go +++ b/x/crosschain/types/message_refund_aborted.go @@ -45,8 +45,8 @@ func (msg *MsgRefundAbortedCCTX) ValidateBasic() error { if err != nil { return errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid creator address (%s)", err) } - if len(msg.CctxIndex) != 66 { - return ErrInvalidCCTXIndex + if len(msg.CctxIndex) != ZetaIndexLength { + return ErrInvalidIndexValue } if msg.RefundAddress != "" && !ethcommon.IsHexAddress(msg.RefundAddress) { return ErrInvalidAddress diff --git a/x/crosschain/types/message_refund_aborted_test.go b/x/crosschain/types/message_refund_aborted_test.go index b02b3dab56..2884b8b7c1 100644 --- a/x/crosschain/types/message_refund_aborted_test.go +++ b/x/crosschain/types/message_refund_aborted_test.go @@ -22,7 +22,7 @@ func TestMsgRefundAbortedCCTX_ValidateBasic(t *testing.T) { }) t.Run("invalid cctx index", func(t *testing.T) { msg := types.NewMsgRefundAbortedCCTX(sample.AccAddress(), "invalid", "") - require.ErrorContains(t, msg.ValidateBasic(), "invalid cctx index") + require.ErrorContains(t, msg.ValidateBasic(), "invalid index hash") }) t.Run("invalid refund address", func(t *testing.T) { cctx := sample.CrossChainTx(t, "test") diff --git a/x/crosschain/types/outbound_params.go b/x/crosschain/types/outbound_params.go new file mode 100644 index 0000000000..10adf6527d --- /dev/null +++ b/x/crosschain/types/outbound_params.go @@ -0,0 +1,47 @@ +package types + +import ( + "fmt" + "strconv" + + "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/zeta-chain/zetacore/pkg/chains" +) + +func (m OutboundTxParams) GetGasPrice() (uint64, error) { + gasPrice, err := strconv.ParseUint(m.OutboundTxGasPrice, 10, 64) + if err != nil { + return 0, fmt.Errorf("unable to parse cctx gas price %s: %s", m.OutboundTxGasPrice, err.Error()) + } + + return gasPrice, nil +} + +func (m OutboundTxParams) Validate() error { + if m.Receiver == "" { + return fmt.Errorf("receiver cannot be empty") + } + if chains.GetChainFromChainID(m.ReceiverChainId) == nil { + return fmt.Errorf("invalid receiver chain id %d", m.ReceiverChainId) + } + err := ValidateAddressForChain(m.Receiver, m.ReceiverChainId) + if err != nil { + return err + } + if m.Amount.IsNil() { + return fmt.Errorf("amount cannot be nil") + } + if m.OutboundTxBallotIndex != "" { + err = ValidateZetaIndex(m.OutboundTxBallotIndex) + if err != nil { + return errors.Wrap(err, "invalid outbound tx ballot index") + } + } + if m.OutboundTxHash != "" { + err = ValidateHashForChain(m.OutboundTxHash, m.ReceiverChainId) + if err != nil { + return errors.Wrap(err, "invalid outbound tx hash") + } + } + return nil +} diff --git a/x/crosschain/types/outbound_params_test.go b/x/crosschain/types/outbound_params_test.go new file mode 100644 index 0000000000..b0075ccde7 --- /dev/null +++ b/x/crosschain/types/outbound_params_test.go @@ -0,0 +1,48 @@ +package types_test + +import ( + "math/rand" + "testing" + + sdkmath "cosmossdk.io/math" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/testutil/sample" +) + +func TestOutboundTxParams_Validate(t *testing.T) { + r := rand.New(rand.NewSource(42)) + outTxParams := sample.OutboundTxParamsValidChainID(r) + outTxParams.Receiver = "" + require.ErrorContains(t, outTxParams.Validate(), "receiver cannot be empty") + outTxParams = sample.OutboundTxParamsValidChainID(r) + outTxParams.ReceiverChainId = 1000 + require.ErrorContains(t, outTxParams.Validate(), "invalid receiver chain id 1000") + outTxParams = sample.OutboundTxParamsValidChainID(r) + outTxParams.Receiver = "0x123" + require.ErrorContains(t, outTxParams.Validate(), "invalid address 0x123") + outTxParams = sample.OutboundTxParamsValidChainID(r) + outTxParams.Amount = sdkmath.Uint{} + require.ErrorContains(t, outTxParams.Validate(), "amount cannot be nil") + outTxParams = sample.OutboundTxParamsValidChainID(r) + outTxParams.OutboundTxBallotIndex = "12" + require.ErrorContains(t, outTxParams.Validate(), "invalid index length 2") + outTxParams = sample.OutboundTxParamsValidChainID(r) + outTxParams.OutboundTxBallotIndex = sample.ZetaIndex(t) + outTxParams.OutboundTxHash = sample.Hash().String() + require.NoError(t, outTxParams.Validate()) +} + +func TestOutboundTxParams_GetGasPrice(t *testing.T) { + // #nosec G404 - random seed is not used for security purposes + r := rand.New(rand.NewSource(42)) + outTxParams := sample.OutboundTxParams(r) + + outTxParams.OutboundTxGasPrice = "42" + gasPrice, err := outTxParams.GetGasPrice() + require.NoError(t, err) + require.EqualValues(t, uint64(42), gasPrice) + + outTxParams.OutboundTxGasPrice = "invalid" + _, err = outTxParams.GetGasPrice() + require.Error(t, err) +} diff --git a/x/crosschain/types/status.go b/x/crosschain/types/status.go index be2b4af62e..729b0b9bcb 100644 --- a/x/crosschain/types/status.go +++ b/x/crosschain/types/status.go @@ -13,7 +13,11 @@ func (m *Status) AbortRefunded(timeStamp int64) { // empty msg does not overwrite old status message func (m *Status) ChangeStatus(newStatus CctxStatus, msg string) { if len(msg) > 0 { - m.StatusMessage = msg + if m.StatusMessage != "" { + m.StatusMessage = fmt.Sprintf("%s : %s", m.StatusMessage, msg) + } else { + m.StatusMessage = msg + } } if !m.ValidateTransition(newStatus) { m.StatusMessage = fmt.Sprintf("Failed to transition : OldStatus %s , NewStatus %s , MSG : %s :", m.Status.String(), newStatus.String(), msg) diff --git a/x/crosschain/types/validate.go b/x/crosschain/types/validate.go new file mode 100644 index 0000000000..713eb8d432 --- /dev/null +++ b/x/crosschain/types/validate.go @@ -0,0 +1,68 @@ +package types + +import ( + "fmt" + "regexp" + + "cosmossdk.io/errors" + "github.com/btcsuite/btcutil" + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/zeta-chain/zetacore/pkg/chains" +) + +// ValidateZetaIndex validates the zeta index +func ValidateZetaIndex(index string) error { + if len(index) != ZetaIndexLength { + return errors.Wrap(ErrInvalidIndexValue, fmt.Sprintf("invalid index length %d", len(index))) + } + return nil +} + +// ValidateHashForChain validates the hash for the chain +func ValidateHashForChain(hash string, chainID int64) error { + if chains.IsEthereumChain(chainID) || chains.IsZetaChain(chainID) { + _, err := hexutil.Decode(hash) + if err != nil { + return fmt.Errorf("hash must be a valid ethereum hash %s", hash) + } + return nil + } + if chains.IsBitcoinChain(chainID) { + r, err := regexp.Compile("^[a-fA-F0-9]{64}$") + if err != nil { + return fmt.Errorf("error compiling regex") + } + if !r.MatchString(hash) { + return fmt.Errorf("hash must be a valid bitcoin hash %s", hash) + } + return nil + } + return fmt.Errorf("invalid chain id %d", chainID) +} + +// ValidateAddressForChain validates the address for the chain +func ValidateAddressForChain(address string, chainID int64) error { + // we do not validate the address for zeta chain as the address field can be btc or eth address + if chains.IsZetaChain(chainID) { + return nil + } + if chains.IsEthereumChain(chainID) { + if !ethcommon.IsHexAddress(address) { + return fmt.Errorf("invalid address %s , chain %d", address, chainID) + } + return nil + } + if chains.IsBitcoinChain(chainID) { + addr, err := chains.DecodeBtcAddress(address, chainID) + if err != nil { + return fmt.Errorf("invalid address %s , chain %d: %s", address, chainID, err) + } + _, ok := addr.(*btcutil.AddressWitnessPubKeyHash) + if !ok { + return fmt.Errorf(" invalid address %s (not P2WPKH address)", address) + } + return nil + } + return fmt.Errorf("invalid chain id %d", chainID) +} diff --git a/x/crosschain/types/validate_test.go b/x/crosschain/types/validate_test.go new file mode 100644 index 0000000000..206f99b39c --- /dev/null +++ b/x/crosschain/types/validate_test.go @@ -0,0 +1,40 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/testutil/sample" + "github.com/zeta-chain/zetacore/x/crosschain/types" +) + +func TestValidateAddressForChain(t *testing.T) { + require.Error(t, types.ValidateAddressForChain("0x123", chains.GoerliChain().ChainId)) + require.Error(t, types.ValidateAddressForChain("", chains.GoerliChain().ChainId)) + require.Error(t, types.ValidateAddressForChain("%%%%", chains.GoerliChain().ChainId)) + require.NoError(t, types.ValidateAddressForChain("0x792c127Fa3AC1D52F904056Baf1D9257391e7D78", chains.GoerliChain().ChainId)) + require.Error(t, types.ValidateAddressForChain("1EYVvXLusCxtVuEwoYvWRyN5EZTXwPVvo3", chains.BtcMainnetChain().ChainId)) + require.Error(t, types.ValidateAddressForChain("bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw", chains.BtcMainnetChain().ChainId)) + require.Error(t, types.ValidateAddressForChain("", chains.BtcRegtestChain().ChainId)) + require.NoError(t, types.ValidateAddressForChain("bc1qysd4sp9q8my59ul9wsf5rvs9p387hf8vfwatzu", chains.BtcMainnetChain().ChainId)) + require.NoError(t, types.ValidateAddressForChain("bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw", chains.BtcRegtestChain().ChainId)) + require.NoError(t, types.ValidateAddressForChain("bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw", chains.ZetaChainMainnet().ChainId)) + require.NoError(t, types.ValidateAddressForChain("0x792c127Fa3AC1D52F904056Baf1D9257391e7D78", chains.ZetaChainMainnet().ChainId)) +} + +func TestValidateZetaIndex(t *testing.T) { + require.NoError(t, types.ValidateZetaIndex("0x84bd5c9922b63c52d8a9ca686e0a57ff978150b71be0583514d01c27aa341910")) + require.NoError(t, types.ValidateZetaIndex(sample.ZetaIndex(t))) + require.Error(t, types.ValidateZetaIndex("0")) + require.Error(t, types.ValidateZetaIndex("0x70e967acFcC17c3941E87562161406d41676FD83")) +} + +func TestValidateHashForChain(t *testing.T) { + require.NoError(t, types.ValidateHashForChain("0x84bd5c9922b63c52d8a9ca686e0a57ff978150b71be0583514d01c27aa341910", chains.GoerliChain().ChainId)) + require.Error(t, types.ValidateHashForChain("", chains.GoerliChain().ChainId)) + require.Error(t, types.ValidateHashForChain("a0fa5a82f106fb192e4c503bfa8d54b2de20a821e09338094ab825cc9b275059", chains.GoerliChain().ChainId)) + require.NoError(t, types.ValidateHashForChain("15b7880f5d236e857a5e8f043ce9d56f5ef01e1c3f2a786baf740fc0bb7a22a3", chains.BtcMainnetChain().ChainId)) + require.NoError(t, types.ValidateHashForChain("a0fa5a82f106fb192e4c503bfa8d54b2de20a821e09338094ab825cc9b275059", chains.BtcTestNetChain().ChainId)) + require.Error(t, types.ValidateHashForChain("0x84bd5c9922b63c52d8a9ca686e0a57ff978150b71be0583514d01c27aa341910", chains.BtcMainnetChain().ChainId)) +} diff --git a/x/emissions/abci_test.go b/x/emissions/abci_test.go index 65153b377a..55289af549 100644 --- a/x/emissions/abci_test.go +++ b/x/emissions/abci_test.go @@ -154,6 +154,7 @@ func TestBeginBlocker(t *testing.T) { func TestDistributeObserverRewards(t *testing.T) { keepertest.SetConfig(false) + k, ctx, _, _ := keepertest.EmissionsKeeper(t) observerSet := sample.ObserverSet(4) tt := []struct { @@ -240,6 +241,12 @@ func TestDistributeObserverRewards(t *testing.T) { } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { + for _, observer := range observerSet.ObserverList { + k.SetWithdrawableEmission(ctx, emissionstypes.WithdrawableEmissions{ + Address: observer, + Amount: sdkmath.NewInt(100), + }) + } // Keeper initialization k, ctx, sk, zk := keepertest.EmissionsKeeper(t) @@ -283,7 +290,6 @@ func TestDistributeObserverRewards(t *testing.T) { Height: 0, BallotsIndexList: ballotIdentifiers, }) - ctx = ctx.WithBlockHeight(100) // Distribute the rewards and check if the rewards are distributed correctly diff --git a/x/emissions/client/tests/cli_test.go b/x/emissions/client/tests/cli_test.go deleted file mode 100644 index 4229cf2a36..0000000000 --- a/x/emissions/client/tests/cli_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package querytests - -import ( - "testing" - - "github.com/stretchr/testify/suite" - "github.com/zeta-chain/zetacore/testutil/network" -) - -func TestCLIQuerySuite(t *testing.T) { - cfg := network.DefaultConfig() - suite.Run(t, NewCLITestSuite(cfg)) -} diff --git a/x/emissions/client/tests/observer_rewards_test.go b/x/emissions/client/tests/observer_rewards_test.go deleted file mode 100644 index c8f11d2449..0000000000 --- a/x/emissions/client/tests/observer_rewards_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package querytests - -import ( - "fmt" - - sdkmath "cosmossdk.io/math" - "github.com/cosmos/cosmos-sdk/client/flags" - clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/bank/client/cli" - "github.com/stretchr/testify/suite" - "github.com/zeta-chain/zetacore/cmd/zetacored/config" - emissionscli "github.com/zeta-chain/zetacore/x/emissions/client/cli" - emissionskeeper "github.com/zeta-chain/zetacore/x/emissions/keeper" - emissionstypes "github.com/zeta-chain/zetacore/x/emissions/types" - observercli "github.com/zeta-chain/zetacore/x/observer/client/cli" - observertypes "github.com/zeta-chain/zetacore/x/observer/types" -) - -func (s *CliTestSuite) TestObserverRewards() { - emissionPool := "800000000000000000000azeta" - val := s.network.Validators[0] - - out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, emissionscli.CmdListPoolAddresses(), []string{"--output", "json"}) - s.Require().NoError(err) - resPools := emissionstypes.QueryListPoolAddressesResponse{} - s.Require().NoError(err) - s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &resPools)) - txArgs := []string{ - fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), - fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(config.BaseDenom, sdk.NewInt(10))).String()), - } - - // Fund the emission pool to start the emission process - sendArgs := []string{val.Address.String(), - resPools.EmissionModuleAddress, emissionPool} - args := append(sendArgs, txArgs...) - out, err = clitestutil.ExecTestCLICmd(val.ClientCtx, cli.NewSendTxCmd(), args) - s.Require().NoError(err) - s.Require().NoError(s.network.WaitForNextBlock()) - - // Collect parameter values and build assertion map for the randomised ballot set created - emissionFactors := emissionstypes.QueryGetEmissionsFactorsResponse{} - out, err = clitestutil.ExecTestCLICmd(val.ClientCtx, emissionscli.CmdGetEmmisonsFactors(), []string{"--output", "json"}) - s.Require().NoError(err) - s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &emissionFactors)) - emissionParams := emissionstypes.QueryParamsResponse{} - out, err = clitestutil.ExecTestCLICmd(val.ClientCtx, emissionscli.CmdQueryParams(), []string{"--output", "json"}) - s.Require().NoError(err) - s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &emissionParams)) - observerParams := observertypes.QueryParamsResponse{} - out, err = clitestutil.ExecTestCLICmd(val.ClientCtx, observercli.CmdQueryParams(), []string{"--output", "json"}) - s.Require().NoError(err) - s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &observerParams)) - _, err = s.network.WaitForHeight(s.ballots[0].BallotCreationHeight + observerParams.Params.BallotMaturityBlocks) - s.Require().NoError(err) - out, err = clitestutil.ExecTestCLICmd(val.ClientCtx, emissionscli.CmdGetEmmisonsFactors(), []string{"--output", "json"}) - resFactorsNewBlocks := emissionstypes.QueryGetEmissionsFactorsResponse{} - s.Require().NoError(err) - s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &resFactorsNewBlocks)) - // Duration factor is calculated in the same block,so we need to query based from the committed state at which the distribution is done - // Would be cleaner to use `--height` flag, but it is not supported by the ExecTestCLICmd function yet - emissionFactors.DurationFactor = resFactorsNewBlocks.DurationFactor - asertValues := CalculateObserverRewards(&s.Suite, s.ballots, emissionParams.Params.ObserverEmissionPercentage, emissionFactors.ReservesFactor, emissionFactors.BondFactor, emissionFactors.DurationFactor) - - // Assert withdrawable rewards for each validator - resAvailable := emissionstypes.QueryShowAvailableEmissionsResponse{} - for i := 0; i < len(s.network.Validators); i++ { - out, err = clitestutil.ExecTestCLICmd(val.ClientCtx, emissionscli.CmdShowAvailableEmissions(), []string{s.network.Validators[i].Address.String(), "--output", "json"}) - s.Require().NoError(err) - s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &resAvailable)) - s.Require().Equal(sdk.NewCoin(config.BaseDenom, asertValues[s.network.Validators[i].Address.String()]).String(), resAvailable.Amount, "Validator %s has incorrect withdrawable rewards", s.network.Validators[i].Address.String()) - } - -} - -func CalculateObserverRewards(s *suite.Suite, ballots []*observertypes.Ballot, observerEmissionPercentage, reservesFactor, bondFactor, durationFactor string) map[string]sdkmath.Int { - calculatedDistributer := map[string]sdkmath.Int{} - //blockRewards := sdk.MustNewDecFromStr(reservesFactor).Mul(sdk.MustNewDecFromStr(bondFactor)).Mul(sdk.MustNewDecFromStr(durationFactor)) - blockRewards, err := emissionskeeper.CalculateFixedValidatorRewards(emissionstypes.AvgBlockTime) - s.Require().NoError(err) - observerRewards := sdk.MustNewDecFromStr(observerEmissionPercentage).Mul(blockRewards).TruncateInt() - rewardsDistributer := map[string]int64{} - totalRewardsUnits := int64(0) - // BuildRewardsDistribution has a separate unit test - for _, ballot := range ballots { - totalRewardsUnits = totalRewardsUnits + ballot.BuildRewardsDistribution(rewardsDistributer) - } - rewardPerUnit := observerRewards.Quo(sdk.NewInt(totalRewardsUnits)) - for address, units := range rewardsDistributer { - if units == 0 { - calculatedDistributer[address] = sdk.ZeroInt() - continue - } - if units < 0 { - calculatedDistributer[address] = sdk.ZeroInt() - continue - } - if units > 0 { - calculatedDistributer[address] = rewardPerUnit.Mul(sdkmath.NewInt(units)) - } - } - return calculatedDistributer -} diff --git a/x/emissions/client/tests/suite.go b/x/emissions/client/tests/suite.go deleted file mode 100644 index c13294ddf7..0000000000 --- a/x/emissions/client/tests/suite.go +++ /dev/null @@ -1,113 +0,0 @@ -package querytests - -import ( - "crypto/rand" - "math/big" - "strconv" - "testing" - - sdk "github.com/cosmos/cosmos-sdk/types" - ethcfg "github.com/evmos/ethermint/cmd/config" - "github.com/stretchr/testify/suite" - "github.com/zeta-chain/zetacore/app" - cmdcfg "github.com/zeta-chain/zetacore/cmd/zetacored/config" - "github.com/zeta-chain/zetacore/testutil/network" - observerTypes "github.com/zeta-chain/zetacore/x/observer/types" -) - -type CliTestSuite struct { - suite.Suite - - cfg network.Config - network *network.Network - ballots []*observerTypes.Ballot -} - -func NewCLITestSuite(cfg network.Config) *CliTestSuite { - return &CliTestSuite{cfg: cfg} -} - -func (s *CliTestSuite) Setconfig() { - config := sdk.GetConfig() - cmdcfg.SetBech32Prefixes(config) - ethcfg.SetBip44CoinType(config) - // Make sure address is compatible with ethereum - config.SetAddressVerifier(app.VerifyAddressFormat) - config.Seal() -} -func (s *CliTestSuite) SetupSuite() { - s.T().Log("setting up integration test suite") - s.Setconfig() - minOBsDel, ok := sdk.NewIntFromString("100000000000000000000") - s.Require().True(ok) - s.cfg.StakingTokens = minOBsDel.Mul(sdk.NewInt(int64(10))) - s.cfg.BondedTokens = minOBsDel - observerList := []string{"zeta13c7p3xrhd6q2rx3h235jpt8pjdwvacyw6twpax", - "zeta1f203dypqg5jh9hqfx0gfkmmnkdfuat3jr45ep2", - "zeta1szrskhdeleyt6wmn0nfxvcvt2l6f4fn06uaga4", - "zeta16h3y7s7030l4chcznwq3n6uz2m9wvmzu5vwt7c", - "zeta1xl2rfsrmx8nxryty3lsjuxwdxs59cn2q65e5ca", - "zeta1ktmprjdvc72jq0mpu8tn8sqx9xwj685qx0q6kt", - "zeta1ygeyr8pqfjvclxay5234gulnjzv2mkz6lph9y4", - "zeta1zegyenj7xg5nck04ykkzndm2qxdzc6v83mklsy", - "zeta1us2qpqdcctk6q7qv2c9d9jvjxlv88jscf68kav", - "zeta1e9fyaulgntkrnqnl0es4nyxghp3petpn2ntu3t", - } - network.SetupZetaGenesisState(s.T(), s.cfg.GenesisState, s.cfg.Codec, observerList, false) - s.ballots = RandomBallotGenerator(s.T(), 20, observerList) - network.AddObserverData(s.T(), 2, s.cfg.GenesisState, s.cfg.Codec, s.ballots) - - net, err := network.New(s.T(), app.NodeDir, s.cfg) - s.Assert().NoError(err) - s.network = net - _, err = s.network.WaitForHeight(1) - s.Require().NoError(err) - -} - -func CreateRandomVoteList(t *testing.T, numberOfVotes int) []observerTypes.VoteType { - voteOptions := []observerTypes.VoteType{observerTypes.VoteType_SuccessObservation, observerTypes.VoteType_FailureObservation, observerTypes.VoteType_NotYetVoted} - minVoterOptions := 0 - maxBoterOptions := len(voteOptions) - 1 - - randomVoteOptions, err := rand.Int(rand.Reader, big.NewInt(int64(maxBoterOptions-minVoterOptions))) - if err != nil { - t.Fatal(err) - } - - voteList := make([]observerTypes.VoteType, numberOfVotes) - for i := 0; i < numberOfVotes; i++ { - voteList[i] = voteOptions[randomVoteOptions.Int64()] - } - return voteList -} -func RandomBallotGenerator(t *testing.T, numberOfBallots int, voterList []string) []*observerTypes.Ballot { - ballots := make([]*observerTypes.Ballot, numberOfBallots) - ballotStatus := []observerTypes.BallotStatus{observerTypes.BallotStatus_BallotFinalized_FailureObservation, observerTypes.BallotStatus_BallotFinalized_SuccessObservation} - minBallotStatus := 0 - maxBallotStatus := len(ballotStatus) - 1 - - randomBallotStatus, err := rand.Int(rand.Reader, big.NewInt(int64(maxBallotStatus-minBallotStatus))) - if err != nil { - t.Fatal(err) - } - - for i := 0; i < numberOfBallots; i++ { - ballots[i] = &observerTypes.Ballot{ - Index: "", - BallotIdentifier: "TestBallot" + strconv.Itoa(i), - VoterList: voterList, - Votes: CreateRandomVoteList(t, len(voterList)), - ObservationType: observerTypes.ObservationType_InBoundTx, - BallotThreshold: sdk.MustNewDecFromStr("0.66"), - BallotStatus: ballotStatus[randomBallotStatus.Int64()], - BallotCreationHeight: 0, - } - } - return ballots -} - -func (s *CliTestSuite) TearDownSuite() { - s.T().Log("tearing down genesis test suite") - s.network.Cleanup() -} diff --git a/x/observer/keeper/msg_server_remove_chain_params_test.go b/x/observer/keeper/msg_server_remove_chain_params_test.go index cdfc56b0c5..21b68f7f8e 100644 --- a/x/observer/keeper/msg_server_remove_chain_params_test.go +++ b/x/observer/keeper/msg_server_remove_chain_params_test.go @@ -39,9 +39,8 @@ func TestMsgServer_RemoveChainParams(t *testing.T) { }, }) - keepertest.MockIsAuthorized(&authorityMock.Mock, admin, authoritytypes.PolicyType_groupOperational, true) - // remove chain params + keepertest.MockIsAuthorized(&authorityMock.Mock, admin, authoritytypes.PolicyType_groupOperational, true) _, err := srv.RemoveChainParams(sdk.WrapSDKContext(ctx), &types.MsgRemoveChainParams{ Creator: admin, ChainId: chain2, @@ -56,7 +55,6 @@ func TestMsgServer_RemoveChainParams(t *testing.T) { require.Equal(t, chain3, chainParamsList.ChainParams[1].ChainId) keepertest.MockIsAuthorized(&authorityMock.Mock, admin, authoritytypes.PolicyType_groupOperational, true) - // remove chain params _, err = srv.RemoveChainParams(sdk.WrapSDKContext(ctx), &types.MsgRemoveChainParams{ Creator: admin, diff --git a/x/observer/keeper/tss_test.go b/x/observer/keeper/tss_test.go index 7574e05726..6408f4f49a 100644 --- a/x/observer/keeper/tss_test.go +++ b/x/observer/keeper/tss_test.go @@ -69,7 +69,7 @@ func TestTSSQuerySingle(t *testing.T) { } func TestTSSQueryHistory(t *testing.T) { - keeper, ctx, _, _ := keepertest.ObserverKeeper(t) + k, ctx, _, _ := keepertest.ObserverKeeper(t) wctx := sdk.WrapSDKContext(ctx) for _, tc := range []struct { desc string @@ -94,16 +94,16 @@ func TestTSSQueryHistory(t *testing.T) { t.Run(tc.desc, func(t *testing.T) { tssList := sample.TssList(tc.tssCount) for _, tss := range tssList { - keeper.SetTSS(ctx, tss) - keeper.SetTSSHistory(ctx, tss) + k.SetTSS(ctx, tss) + k.SetTSSHistory(ctx, tss) } request := &types.QueryTssHistoryRequest{} - response, err := keeper.TssHistory(wctx, request) + response, err := k.TssHistory(wctx, request) if tc.err != nil { require.ErrorIs(t, err, tc.err) } else { require.Equal(t, len(tssList), len(response.TssList)) - prevTss, found := keeper.GetPreviousTSS(ctx) + prevTss, found := k.GetPreviousTSS(ctx) require.Equal(t, tc.foundPrevious, found) if found { require.Equal(t, tssList[len(tssList)-2], prevTss) diff --git a/zetaclient/bitcoin/bitcoin_signer.go b/zetaclient/bitcoin/bitcoin_signer.go index 747649be06..df5502b4ae 100644 --- a/zetaclient/bitcoin/bitcoin_signer.go +++ b/zetaclient/bitcoin/bitcoin_signer.go @@ -8,11 +8,11 @@ import ( "math/rand" "time" - "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/zeta-chain/zetacore/pkg/coin" corecontext "github.com/zeta-chain/zetacore/zetaclient/core_context" ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/pkg/coin" clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" "github.com/zeta-chain/zetacore/zetaclient/compliance" "github.com/zeta-chain/zetacore/zetaclient/interfaces" @@ -286,7 +286,8 @@ func (signer *BTCSigner) TryProcessOutTx( Logger() params := cctx.GetCurrentOutTxParam() - if params.CoinType == coin.CoinType_Zeta || params.CoinType == coin.CoinType_ERC20 { + coinType := cctx.InboundTxParams.CoinType + if coinType == coin.CoinType_Zeta || coinType == coin.CoinType_ERC20 { logger.Error().Msgf("BTC TryProcessOutTx: can only send BTC to a BTC network") return } diff --git a/zetaclient/evm/evm_client.go b/zetaclient/evm/evm_client.go index a70401365e..672c6b65b0 100644 --- a/zetaclient/evm/evm_client.go +++ b/zetaclient/evm/evm_client.go @@ -319,7 +319,7 @@ func (ob *ChainClient) Stop() { // If isConfirmed, it also post to ZetaCore func (ob *ChainClient) IsSendOutTxProcessed(cctx *crosschaintypes.CrossChainTx, logger zerolog.Logger) (bool, bool, error) { sendHash := cctx.Index - cointype := cctx.GetCurrentOutTxParam().CoinType + cointype := cctx.InboundTxParams.CoinType nonce := cctx.GetCurrentOutTxParam().OutboundTxTssNonce // skip if outtx is not confirmed diff --git a/zetaclient/evm/evm_signer.go b/zetaclient/evm/evm_signer.go index a5f6114822..8301a5d267 100644 --- a/zetaclient/evm/evm_signer.go +++ b/zetaclient/evm/evm_signer.go @@ -360,7 +360,7 @@ func (signer *Signer) TryProcessOutTx( logger.Warn().Err(err).Msg(SignerErrorMsg(cctx)) return } - } else if cctx.GetCurrentOutTxParam().CoinType == coin.CoinType_Cmd { // admin command + } else if cctx.InboundTxParams.CoinType == coin.CoinType_Cmd { // admin command to := ethcommon.HexToAddress(cctx.GetCurrentOutTxParam().Receiver) if to == (ethcommon.Address{}) { logger.Error().Msgf("invalid receiver %s", cctx.GetCurrentOutTxParam().Receiver) @@ -383,7 +383,7 @@ func (signer *Signer) TryProcessOutTx( return } } else if IsSenderZetaChain(cctx, zetaBridge, &crossChainflags) { - switch cctx.GetCurrentOutTxParam().CoinType { + switch cctx.InboundTxParams.CoinType { case coin.CoinType_Gas: logger.Info().Msgf("SignWithdrawTx: %d => %s, nonce %d, gasPrice %d", cctx.InboundTxParams.SenderChainId, toChain, cctx.GetCurrentOutTxParam().OutboundTxTssNonce, txData.gasPrice) tx, err = signer.SignWithdrawTx(txData) @@ -399,7 +399,7 @@ func (signer *Signer) TryProcessOutTx( return } } else if cctx.CctxStatus.Status == types.CctxStatus_PendingRevert && cctx.OutboundTxParams[0].ReceiverChainId == zetaBridge.ZetaChain().ChainId { - switch cctx.GetCurrentOutTxParam().CoinType { + switch cctx.InboundTxParams.CoinType { case coin.CoinType_Gas: logger.Info().Msgf("SignWithdrawTx: %d => %s, nonce %d, gasPrice %d", cctx.InboundTxParams.SenderChainId, toChain, cctx.GetCurrentOutTxParam().OutboundTxTssNonce, txData.gasPrice) tx, err = signer.SignWithdrawTx(txData) diff --git a/zetaclient/evm/outbound_transaction_data.go b/zetaclient/evm/outbound_transaction_data.go index 0255c1c525..6734926f0d 100644 --- a/zetaclient/evm/outbound_transaction_data.go +++ b/zetaclient/evm/outbound_transaction_data.go @@ -170,7 +170,7 @@ func NewOutBoundTransactionData( } // Base64 decode message - if cctx.GetCurrentOutTxParam().CoinType != coin.CoinType_Cmd { + if cctx.InboundTxParams.CoinType != coin.CoinType_Cmd { txData.message, err = base64.StdEncoding.DecodeString(cctx.RelayedMessage) if err != nil { logger.Err(err).Msgf("decode CCTX.Message %s error", cctx.RelayedMessage) diff --git a/zetaclient/supplychecker/zeta_supply_checker.go b/zetaclient/supplychecker/zeta_supply_checker.go index 2168c5483e..14b4c056d2 100644 --- a/zetaclient/supplychecker/zeta_supply_checker.go +++ b/zetaclient/supplychecker/zeta_supply_checker.go @@ -247,7 +247,7 @@ func (zs *ZetaSupplyChecker) GetPendingCCTXInTransit(receivingChains []chains.Ch } nonceToCctxMap := make(map[uint64]*types.CrossChainTx) for _, c := range cctx { - if c.GetInboundTxParams().CoinType == coin.CoinType_Zeta { + if c.InboundTxParams.CoinType == coin.CoinType_Zeta { nonceToCctxMap[c.GetCurrentOutTxParam().OutboundTxTssNonce] = c } }