diff --git a/x/crosschain/keeper/cctx_test.go b/x/crosschain/keeper/cctx_test.go index 3f8c3fe4d4..3a7fb7fe5b 100644 --- a/x/crosschain/keeper/cctx_test.go +++ b/x/crosschain/keeper/cctx_test.go @@ -243,3 +243,13 @@ func TestSendQueryPaginated(t *testing.T) { require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "invalid request")) }) } + +func TestKeeper_RemoveCrossChainTx(t *testing.T) { + keeper, ctx, _, zk := keepertest.CrosschainKeeper(t) + zk.ObserverKeeper.SetTSS(ctx, sample.Tss()) + txs := createNCctx(keeper, ctx, 5) + + keeper.RemoveCrossChainTx(ctx, txs[0].Index) + txs = keeper.GetAllCrossChainTx(ctx) + require.Equal(t, 4, len(txs)) +} diff --git a/x/crosschain/keeper/cctx_utils.go b/x/crosschain/keeper/cctx_utils.go index 0fde2fb2e6..6ae826f9ed 100644 --- a/x/crosschain/keeper/cctx_utils.go +++ b/x/crosschain/keeper/cctx_utils.go @@ -18,7 +18,7 @@ import ( // UpdateNonce sets the CCTX outbound nonce to the next nonce, and updates the nonce of blockchain state. // It also updates the PendingNonces that is used to track the unfulfilled outbound txs. func (k Keeper) UpdateNonce(ctx sdk.Context, receiveChainID int64, cctx *types.CrossChainTx) error { - chain := k.zetaObserverKeeper.GetSupportedChainFromChainID(ctx, receiveChainID) + chain := k.GetObserverKeeper().GetSupportedChainFromChainID(ctx, receiveChainID) if chain == nil { return zetaObserverTypes.ErrSupportedChains } @@ -30,7 +30,7 @@ func (k Keeper) UpdateNonce(ctx sdk.Context, receiveChainID int64, cctx *types.C // SET nonce cctx.GetCurrentOutTxParam().OutboundTxTssNonce = nonce.Nonce - tss, found := k.zetaObserverKeeper.GetTSS(ctx) + tss, found := k.GetObserverKeeper().GetTSS(ctx) if !found { return cosmoserrors.Wrap(types.ErrCannotFindTSSKeys, fmt.Sprintf("Chain(%s) | Identifiers : %s ", chain.ChainName.String(), cctx.LogIdentifierForCCTX())) } diff --git a/x/crosschain/keeper/cctx_utils_test.go b/x/crosschain/keeper/cctx_utils_test.go index 888b3bf445..1c807a97c1 100644 --- a/x/crosschain/keeper/cctx_utils_test.go +++ b/x/crosschain/keeper/cctx_utils_test.go @@ -6,13 +6,16 @@ import ( "testing" sdkmath "cosmossdk.io/math" + "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" crosschainkeeper "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 TestGetRevertGasLimit(t *testing.T) { @@ -216,3 +219,158 @@ func Test_IsPending(t *testing.T) { }) } } + +func TestKeeper_UpdateNonce(t *testing.T) { + t.Run("should error if supported chain is nil", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseObserverMock: true, + }) + + observerMock := keepertest.GetCrosschainObserverMock(t, k) + observerMock.On("GetSupportedChainFromChainID", mock.Anything, mock.Anything).Return(nil) + + err := k.UpdateNonce(ctx, 5, nil) + require.Error(t, err) + }) + + t.Run("should error if chain nonces not found", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseObserverMock: true, + }) + + observerMock := keepertest.GetCrosschainObserverMock(t, k) + observerMock.On("GetSupportedChainFromChainID", mock.Anything, mock.Anything).Return(&chains.Chain{ + ChainName: 5, + ChainId: 5, + }) + observerMock.On("GetChainNonces", mock.Anything, mock.Anything).Return(observertypes.ChainNonces{}, false) + cctx := types.CrossChainTx{ + InboundTxParams: &types.InboundTxParams{ + Amount: sdkmath.ZeroUint(), + }, + OutboundTxParams: []*types.OutboundTxParams{ + {Amount: sdkmath.NewUint(1)}, + }, + } + err := k.UpdateNonce(ctx, 5, &cctx) + require.Error(t, err) + }) + + t.Run("should error if tss not found", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseObserverMock: true, + }) + + observerMock := keepertest.GetCrosschainObserverMock(t, k) + observerMock.On("GetSupportedChainFromChainID", mock.Anything, mock.Anything).Return(&chains.Chain{ + ChainName: 5, + ChainId: 5, + }) + observerMock.On("GetChainNonces", mock.Anything, mock.Anything).Return(observertypes.ChainNonces{ + Nonce: 100, + }, true) + observerMock.On("GetTSS", mock.Anything).Return(observertypes.TSS{}, false) + cctx := types.CrossChainTx{ + InboundTxParams: &types.InboundTxParams{ + Amount: sdkmath.ZeroUint(), + }, + OutboundTxParams: []*types.OutboundTxParams{ + {Amount: sdkmath.NewUint(1)}, + }, + } + err := k.UpdateNonce(ctx, 5, &cctx) + require.Error(t, err) + require.Equal(t, uint64(100), cctx.GetCurrentOutTxParam().OutboundTxTssNonce) + }) + + t.Run("should error if pending nonces not found", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseObserverMock: true, + }) + + observerMock := keepertest.GetCrosschainObserverMock(t, k) + observerMock.On("GetSupportedChainFromChainID", mock.Anything, mock.Anything).Return(&chains.Chain{ + ChainName: 5, + ChainId: 5, + }) + observerMock.On("GetChainNonces", mock.Anything, mock.Anything).Return(observertypes.ChainNonces{ + Nonce: 100, + }, true) + observerMock.On("GetTSS", mock.Anything).Return(observertypes.TSS{}, true) + observerMock.On("GetPendingNonces", mock.Anything, mock.Anything, mock.Anything).Return(observertypes.PendingNonces{}, false) + + cctx := types.CrossChainTx{ + InboundTxParams: &types.InboundTxParams{ + Amount: sdkmath.ZeroUint(), + }, + OutboundTxParams: []*types.OutboundTxParams{ + {Amount: sdkmath.NewUint(1)}, + }, + } + err := k.UpdateNonce(ctx, 5, &cctx) + require.Error(t, err) + }) + + t.Run("should error if nonces not equal", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseObserverMock: true, + }) + + observerMock := keepertest.GetCrosschainObserverMock(t, k) + observerMock.On("GetSupportedChainFromChainID", mock.Anything, mock.Anything).Return(&chains.Chain{ + ChainName: 5, + ChainId: 5, + }) + observerMock.On("GetChainNonces", mock.Anything, mock.Anything).Return(observertypes.ChainNonces{ + Nonce: 100, + }, true) + observerMock.On("GetTSS", mock.Anything).Return(observertypes.TSS{}, true) + observerMock.On("GetPendingNonces", mock.Anything, mock.Anything, mock.Anything).Return(observertypes.PendingNonces{ + NonceHigh: 99, + }, true) + + cctx := types.CrossChainTx{ + InboundTxParams: &types.InboundTxParams{ + Amount: sdkmath.ZeroUint(), + }, + OutboundTxParams: []*types.OutboundTxParams{ + {Amount: sdkmath.NewUint(1)}, + }, + } + err := k.UpdateNonce(ctx, 5, &cctx) + require.Error(t, err) + }) + + t.Run("should update nonces", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseObserverMock: true, + }) + + observerMock := keepertest.GetCrosschainObserverMock(t, k) + observerMock.On("GetSupportedChainFromChainID", mock.Anything, mock.Anything).Return(&chains.Chain{ + ChainName: 5, + ChainId: 5, + }) + observerMock.On("GetChainNonces", mock.Anything, mock.Anything).Return(observertypes.ChainNonces{ + Nonce: 100, + }, true) + observerMock.On("GetTSS", mock.Anything).Return(observertypes.TSS{}, true) + observerMock.On("GetPendingNonces", mock.Anything, mock.Anything, mock.Anything).Return(observertypes.PendingNonces{ + NonceHigh: 100, + }, true) + + observerMock.On("SetChainNonces", mock.Anything, mock.Anything).Once() + observerMock.On("SetPendingNonces", mock.Anything, mock.Anything).Once() + + cctx := types.CrossChainTx{ + InboundTxParams: &types.InboundTxParams{ + Amount: sdkmath.ZeroUint(), + }, + OutboundTxParams: []*types.OutboundTxParams{ + {Amount: sdkmath.NewUint(1)}, + }, + } + err := k.UpdateNonce(ctx, 5, &cctx) + require.NoError(t, err) + }) +} diff --git a/x/crosschain/keeper/evm_deposit_test.go b/x/crosschain/keeper/evm_deposit_test.go index bcb7b684e8..2a74b1ab1b 100644 --- a/x/crosschain/keeper/evm_deposit_test.go +++ b/x/crosschain/keeper/evm_deposit_test.go @@ -117,6 +117,78 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { fungibleMock.AssertExpectations(t) }) + t.Run("can process ERC20 deposit calling fungible method for contract call", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + }) + + senderChain := getValidEthChainID(t) + + fungibleMock := keepertest.GetCrosschainFungibleMock(t, k) + receiver := sample.EthAddress() + amount := big.NewInt(42) + + // expect DepositCoinZeta to be called + // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.CoinType, msg.Asset) + fungibleMock.On( + "ZRC20DepositAndCallContract", + ctx, + mock.Anything, + receiver, + amount, + senderChain, + mock.Anything, + coin.CoinType_ERC20, + mock.Anything, + ).Return(&evmtypes.MsgEthereumTxResponse{ + Logs: []*evmtypes.Log{}, + }, true, nil) + + // call HandleEVMDeposit + cctx := sample.CrossChainTx(t, "foo") + cctx.InboundTxParams.TxOrigin = "" + 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, + cctx, + ) + require.NoError(t, err) + require.False(t, reverted) + fungibleMock.AssertExpectations(t) + }) + + t.Run("should error if invalid sender", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseFungibleMock: true, + }) + + receiver := sample.EthAddress() + amount := big.NewInt(42) + + // call HandleEVMDeposit + cctx := sample.CrossChainTx(t, "foo") + cctx.InboundTxParams.TxOrigin = "" + cctx.GetCurrentOutTxParam().Receiver = receiver.String() + cctx.GetInboundTxParams().Amount = math.NewUintFromBigInt(amount) + cctx.GetInboundTxParams().CoinType = coin.CoinType_ERC20 + cctx.GetInboundTxParams().Sender = "invalid" + cctx.GetInboundTxParams().SenderChainId = 987 + cctx.RelayedMessage = "" + cctx.GetInboundTxParams().Asset = "" + reverted, err := k.HandleEVMDeposit( + ctx, + cctx, + ) + require.Error(t, err) + require.False(t, reverted) + }) + t.Run("should return error with non-reverted if deposit ERC20 fails with tx non-failed", func(t *testing.T) { k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ UseFungibleMock: true, diff --git a/x/crosschain/keeper/msg_server_migrate_tss_funds.go b/x/crosschain/keeper/msg_server_migrate_tss_funds.go index 7fd1228400..b9fddc9dae 100644 --- a/x/crosschain/keeper/msg_server_migrate_tss_funds.go +++ b/x/crosschain/keeper/msg_server_migrate_tss_funds.go @@ -41,6 +41,10 @@ func (k msgServer) MigrateTssFunds(goCtx context.Context, msg *types.MsgMigrateT } tssHistory := k.zetaObserverKeeper.GetAllTSS(ctx) + if len(tssHistory) == 0 { + return nil, errorsmod.Wrap(types.ErrCannotMigrateTssFunds, "empty TSS history") + } + sort.SliceStable(tssHistory, func(i, j int) bool { return tssHistory[i].FinalizedZetaHeight < tssHistory[j].FinalizedZetaHeight }) diff --git a/x/crosschain/keeper/msg_server_migrate_tss_funds_test.go b/x/crosschain/keeper/msg_server_migrate_tss_funds_test.go index d4a6c5206e..f44835d2b7 100644 --- a/x/crosschain/keeper/msg_server_migrate_tss_funds_test.go +++ b/x/crosschain/keeper/msg_server_migrate_tss_funds_test.go @@ -6,6 +6,7 @@ import ( sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/gas" @@ -106,11 +107,192 @@ func TestKeeper_MigrateTSSFundsForChain(t *testing.T) { multipliedValue, err := gas.MultiplyGasPrice(gp, crosschaintypes.TssMigrationGasMultiplierEVM) require.NoError(t, err) require.Equal(t, multipliedValue.String(), cctx.GetCurrentOutTxParam().OutboundTxGasPrice) - }) } func TestMsgServer_MigrateTssFunds(t *testing.T) { + t.Run("should error if not authorized", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseAuthorityMock: true, + }) + + admin := sample.AccAddress() + authorityMock := keepertest.GetCrosschainAuthorityMock(t, k) + keepertest.MockIsAuthorized(&authorityMock.Mock, admin, authoritytypes.PolicyType_groupAdmin, false) + + msgServer := keeper.NewMsgServerImpl(*k) + chain := getValidEthChain(t) + amount := sdkmath.NewUintFromString("10000000000000000000") + _, err := msgServer.MigrateTssFunds(ctx, &crosschaintypes.MsgMigrateTssFunds{ + Creator: admin, + ChainId: chain.ChainId, + Amount: amount, + }) + require.Error(t, err) + }) + + t.Run("should error if inbound enabled", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseAuthorityMock: true, + UseObserverMock: true, + }) + + admin := sample.AccAddress() + authorityMock := keepertest.GetCrosschainAuthorityMock(t, k) + keepertest.MockIsAuthorized(&authorityMock.Mock, admin, authoritytypes.PolicyType_groupAdmin, true) + + observerMock := keepertest.GetCrosschainObserverMock(t, k) + observerMock.On("IsInboundEnabled", mock.Anything).Return(true) + msgServer := keeper.NewMsgServerImpl(*k) + chain := getValidEthChain(t) + amount := sdkmath.NewUintFromString("10000000000000000000") + _, err := msgServer.MigrateTssFunds(ctx, &crosschaintypes.MsgMigrateTssFunds{ + Creator: admin, + ChainId: chain.ChainId, + Amount: amount, + }) + require.Error(t, err) + }) + + t.Run("should error if tss not found", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseAuthorityMock: true, + UseObserverMock: true, + }) + + admin := sample.AccAddress() + authorityMock := keepertest.GetCrosschainAuthorityMock(t, k) + keepertest.MockIsAuthorized(&authorityMock.Mock, admin, authoritytypes.PolicyType_groupAdmin, true) + + observerMock := keepertest.GetCrosschainObserverMock(t, k) + observerMock.On("IsInboundEnabled", mock.Anything).Return(false) + observerMock.On("GetTSS", mock.Anything).Return(observertypes.TSS{}, false) + + msgServer := keeper.NewMsgServerImpl(*k) + chain := getValidEthChain(t) + amount := sdkmath.NewUintFromString("10000000000000000000") + _, err := msgServer.MigrateTssFunds(ctx, &crosschaintypes.MsgMigrateTssFunds{ + Creator: admin, + ChainId: chain.ChainId, + Amount: amount, + }) + require.Error(t, err) + }) + + t.Run("should error if tss history empty", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseAuthorityMock: true, + UseObserverMock: true, + }) + + admin := sample.AccAddress() + authorityMock := keepertest.GetCrosschainAuthorityMock(t, k) + keepertest.MockIsAuthorized(&authorityMock.Mock, admin, authoritytypes.PolicyType_groupAdmin, true) + + observerMock := keepertest.GetCrosschainObserverMock(t, k) + observerMock.On("IsInboundEnabled", mock.Anything).Return(false) + observerMock.On("GetTSS", mock.Anything).Return(sample.Tss(), true) + observerMock.On("GetAllTSS", mock.Anything).Return([]observertypes.TSS{}) + + msgServer := keeper.NewMsgServerImpl(*k) + chain := getValidEthChain(t) + amount := sdkmath.NewUintFromString("10000000000000000000") + _, err := msgServer.MigrateTssFunds(ctx, &crosschaintypes.MsgMigrateTssFunds{ + Creator: admin, + ChainId: chain.ChainId, + Amount: amount, + }) + require.Error(t, err) + }) + + t.Run("should error if no new tss generated", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseAuthorityMock: true, + UseObserverMock: true, + }) + + admin := sample.AccAddress() + authorityMock := keepertest.GetCrosschainAuthorityMock(t, k) + keepertest.MockIsAuthorized(&authorityMock.Mock, admin, authoritytypes.PolicyType_groupAdmin, true) + + observerMock := keepertest.GetCrosschainObserverMock(t, k) + observerMock.On("IsInboundEnabled", mock.Anything).Return(false) + tss := sample.Tss() + observerMock.On("GetTSS", mock.Anything).Return(tss, true) + observerMock.On("GetAllTSS", mock.Anything).Return([]observertypes.TSS{tss}) + + msgServer := keeper.NewMsgServerImpl(*k) + chain := getValidEthChain(t) + amount := sdkmath.NewUintFromString("10000000000000000000") + _, err := msgServer.MigrateTssFunds(ctx, &crosschaintypes.MsgMigrateTssFunds{ + Creator: admin, + ChainId: chain.ChainId, + Amount: amount, + }) + require.Error(t, err) + }) + + t.Run("should error if current tss is the latest", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseAuthorityMock: true, + UseObserverMock: true, + }) + + admin := sample.AccAddress() + authorityMock := keepertest.GetCrosschainAuthorityMock(t, k) + keepertest.MockIsAuthorized(&authorityMock.Mock, admin, authoritytypes.PolicyType_groupAdmin, true) + + observerMock := keepertest.GetCrosschainObserverMock(t, k) + observerMock.On("IsInboundEnabled", mock.Anything).Return(false) + tss1 := sample.Tss() + tss1.FinalizedZetaHeight = 2 + tss2 := sample.Tss() + tss2.FinalizedZetaHeight = 1 + observerMock.On("GetTSS", mock.Anything).Return(tss1, true) + observerMock.On("GetAllTSS", mock.Anything).Return([]observertypes.TSS{tss2}) + + msgServer := keeper.NewMsgServerImpl(*k) + chain := getValidEthChain(t) + amount := sdkmath.NewUintFromString("10000000000000000000") + _, err := msgServer.MigrateTssFunds(ctx, &crosschaintypes.MsgMigrateTssFunds{ + Creator: admin, + ChainId: chain.ChainId, + Amount: amount, + }) + require.Error(t, err) + }) + + t.Run("should error if pending nonces not found", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseAuthorityMock: true, + UseObserverMock: true, + }) + + admin := sample.AccAddress() + authorityMock := keepertest.GetCrosschainAuthorityMock(t, k) + keepertest.MockIsAuthorized(&authorityMock.Mock, admin, authoritytypes.PolicyType_groupAdmin, true) + + observerMock := keepertest.GetCrosschainObserverMock(t, k) + observerMock.On("IsInboundEnabled", mock.Anything).Return(false) + tss1 := sample.Tss() + tss1.FinalizedZetaHeight = 2 + tss2 := sample.Tss() + tss2.FinalizedZetaHeight = 3 + observerMock.On("GetTSS", mock.Anything).Return(tss1, true) + observerMock.On("GetAllTSS", mock.Anything).Return([]observertypes.TSS{tss2}) + observerMock.On("GetPendingNonces", mock.Anything, mock.Anything, mock.Anything).Return(observertypes.PendingNonces{}, false) + + msgServer := keeper.NewMsgServerImpl(*k) + chain := getValidEthChain(t) + amount := sdkmath.NewUintFromString("10000000000000000000") + _, err := msgServer.MigrateTssFunds(ctx, &crosschaintypes.MsgMigrateTssFunds{ + Creator: admin, + ChainId: chain.ChainId, + Amount: amount, + }) + require.Error(t, err) + }) + t.Run("successfully create tss migration cctx", func(t *testing.T) { k, ctx, _, zk := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ UseAuthorityMock: true,