diff --git a/client/x/evmstaking/keeper/validator_test.go b/client/x/evmstaking/keeper/validator_test.go new file mode 100644 index 00000000..a0f77f47 --- /dev/null +++ b/client/x/evmstaking/keeper/validator_test.go @@ -0,0 +1,271 @@ +package keeper_test + +import ( + "math/big" + "testing" + + "cosmossdk.io/math" + + "github.com/cometbft/cometbft/crypto" + sdk "github.com/cosmos/cosmos-sdk/types" + skeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + "github.com/cosmos/cosmos-sdk/x/staking/testutil" + stypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/piplabs/story/client/x/evmstaking/types" + "github.com/piplabs/story/contracts/bindings" + "github.com/piplabs/story/lib/errors" + "github.com/piplabs/story/lib/k1util" + + "go.uber.org/mock/gomock" +) + +func (s *TestSuite) TestProcessCreateValidator() { + require := s.Require() + ctx, keeper, stakingKeeper, accountKeeper, bankKeeper := s.Ctx, s.EVMStakingKeeper, s.StakingKeeper, s.AccountKeeper, s.BankKeeper + + pubKeys, addrs, valAddrs := createAddresses(3) + corruptedPubKey := append([]byte{}, pubKeys[0].Bytes()...) + corruptedPubKey[0] = 0x04 + corruptedPubKey[1] = 0xFF + + tokens10 := stakingKeeper.TokensFromConsensusPower(ctx, 10) + + // checkDelegatorMapAndValidator checks if the delegator map and validator are created + checkDelegatorMapAndValidator := func(c sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, delEvmAddr common.Address, _ math.Int) { + val, err := keeper.DelegatorMap.Get(c, delAddr.String()) + require.NoError(err) + require.Equal(delEvmAddr.String(), val) + // check validator is created + _, err = stakingKeeper.GetValidator(c, valAddr) + require.NoError(err) + } + // checkDelegatorMapAndValTokens checks if the delegator map and validator tokens are added + checkDelegatorMapAndValTokens := func(c sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, delEvmAddr common.Address, previousValTokens math.Int) { + val, err := keeper.DelegatorMap.Get(c, delAddr.String()) + require.NoError(err) + require.Equal(delEvmAddr.String(), val) + // check validator tokens are added + validator, err := stakingKeeper.GetValidator(c, valAddr) + require.NoError(err) + require.True(validator.Tokens.GT(previousValTokens)) + } + + tcs := []struct { + name string + valDelAddr sdk.AccAddress + valAddr sdk.ValAddress + valPubKey crypto.PubKey + valPubKeyBytes []byte + valTokens math.Int + moniker string + preRun func(t *testing.T, c sdk.Context, valDelAddr sdk.AccAddress, valPubKey crypto.PubKey, valTokens math.Int) + postCheck func(c sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, delEvmAddr common.Address, previousTokens math.Int) + expectedError string + }{ + { + name: "fail: nil validator pubkey", + valPubKeyBytes: nil, + expectedError: "validator pubkey to cosmos", + }, + { + name: "fail: invalid validator pubkey", + valPubKeyBytes: pubKeys[0].Bytes()[1:], + expectedError: "validator pubkey to cosmos", + }, + { + name: "fail: corrupted validator pubkey", + valPubKeyBytes: corruptedPubKey, + expectedError: "validator pubkey to evm address", + }, + { + name: "fail: mint coins", + valDelAddr: addrs[0], + valPubKeyBytes: pubKeys[0].Bytes(), + preRun: func(_ *testing.T, _ sdk.Context, valDelAddr sdk.AccAddress, _ crypto.PubKey, _ math.Int) { + accountKeeper.EXPECT().HasAccount(gomock.Any(), valDelAddr).Return(true) + bankKeeper.EXPECT().MintCoins(gomock.Any(), types.ModuleName, gomock.Any()).Return(errors.New("mint coins")) + }, + expectedError: "create stake coin for depositor: mint coins", + }, + { + name: "fail: send coins from module to account", + valDelAddr: addrs[0], + valPubKeyBytes: pubKeys[0].Bytes(), + preRun: func(_ *testing.T, _ sdk.Context, valDelAddr sdk.AccAddress, _ crypto.PubKey, _ math.Int) { + accountKeeper.EXPECT().HasAccount(gomock.Any(), valDelAddr).Return(true) + bankKeeper.EXPECT().MintCoins(gomock.Any(), types.ModuleName, gomock.Any()).Return(nil) + bankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), types.ModuleName, valDelAddr, gomock.Any()).Return(errors.New("send coins")) + }, + expectedError: "create stake coin for depositor: mint coins", + }, + { + name: "pass: new validator & existing delegator", + valDelAddr: addrs[2], + valAddr: valAddrs[2], + valPubKeyBytes: pubKeys[2].Bytes(), + valPubKey: pubKeys[2], + preRun: func(_ *testing.T, _ sdk.Context, valDelAddr sdk.AccAddress, _ crypto.PubKey, _ math.Int) { + accountKeeper.EXPECT().HasAccount(gomock.Any(), valDelAddr).Return(true) + bankKeeper.EXPECT().MintCoins(gomock.Any(), types.ModuleName, gomock.Any()).Return(nil) + bankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), types.ModuleName, valDelAddr, gomock.Any()).Return(nil) + bankKeeper.EXPECT().DelegateCoinsFromAccountToModule(gomock.Any(), valDelAddr, gomock.Any(), gomock.Any()).Return(nil) + }, + postCheck: checkDelegatorMapAndValidator, + }, + { + name: "pass: new validator & existing delegator & default moniker", + valDelAddr: addrs[2], + valAddr: valAddrs[2], + valPubKeyBytes: pubKeys[2].Bytes(), + valPubKey: pubKeys[2], + moniker: "validator", + preRun: func(_ *testing.T, _ sdk.Context, valDelAddr sdk.AccAddress, _ crypto.PubKey, _ math.Int) { + accountKeeper.EXPECT().HasAccount(gomock.Any(), valDelAddr).Return(true) + bankKeeper.EXPECT().MintCoins(gomock.Any(), types.ModuleName, gomock.Any()).Return(nil) + bankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), types.ModuleName, valDelAddr, gomock.Any()).Return(nil) + bankKeeper.EXPECT().DelegateCoinsFromAccountToModule(gomock.Any(), valDelAddr, gomock.Any(), gomock.Any()).Return(nil) + }, + postCheck: checkDelegatorMapAndValidator, + }, + { + name: "pass: new validator & new delegator", + valDelAddr: addrs[1], + valAddr: valAddrs[1], + valPubKeyBytes: pubKeys[1].Bytes(), + valPubKey: pubKeys[1], + preRun: func(_ *testing.T, _ sdk.Context, valDelAddr sdk.AccAddress, _ crypto.PubKey, _ math.Int) { + accountKeeper.EXPECT().HasAccount(gomock.Any(), valDelAddr).Return(false) + accountKeeper.EXPECT().NewAccountWithAddress(gomock.Any(), valDelAddr).Return(nil) + accountKeeper.EXPECT().SetAccount(gomock.Any(), gomock.Any()) + bankKeeper.EXPECT().MintCoins(gomock.Any(), types.ModuleName, gomock.Any()).Return(nil) + bankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), types.ModuleName, valDelAddr, gomock.Any()).Return(nil) + bankKeeper.EXPECT().DelegateCoinsFromAccountToModule(gomock.Any(), valDelAddr, gomock.Any(), gomock.Any()).Return(nil) + }, + postCheck: checkDelegatorMapAndValidator, + }, + { + name: "pass: existing validator & delegator", + valDelAddr: addrs[1], + valAddr: valAddrs[1], + valPubKeyBytes: pubKeys[1].Bytes(), + valPubKey: pubKeys[1], + valTokens: tokens10, + preRun: func(t *testing.T, c sdk.Context, valDelAddr sdk.AccAddress, valPubKey crypto.PubKey, _ math.Int) { + t.Helper() + // create a validator with valTokens + valAddr := sdk.ValAddress(valPubKey.Address().Bytes()) + pubKey, err := k1util.PubKeyToCosmos(valPubKey) + require.NoError(err) + val := testutil.NewValidator(t, valAddr, pubKey) + validator, _ := val.AddTokensFromDel(tokens10) + s.BankKeeper.EXPECT().SendCoinsFromModuleToModule(gomock.Any(), stypes.NotBondedPoolName, stypes.BondedPoolName, gomock.Any()) + _ = skeeper.TestingUpdateValidator(stakingKeeper, c, validator, true) + + accountKeeper.EXPECT().HasAccount(gomock.Any(), valDelAddr).Return(true) + bankKeeper.EXPECT().MintCoins(gomock.Any(), types.ModuleName, gomock.Any()).Return(nil) + bankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), types.ModuleName, valDelAddr, gomock.Any()).Return(nil) + bankKeeper.EXPECT().DelegateCoinsFromAccountToModule(gomock.Any(), valDelAddr, gomock.Any(), gomock.Any()).Return(nil) + }, + postCheck: checkDelegatorMapAndValTokens, + }, + { + name: "pass: existing validator & new delegator", + valDelAddr: addrs[1], + valAddr: valAddrs[1], + valPubKeyBytes: pubKeys[1].Bytes(), + valPubKey: pubKeys[1], + valTokens: tokens10, + preRun: func(t *testing.T, c sdk.Context, valDelAddr sdk.AccAddress, valPubKey crypto.PubKey, valTokens math.Int) { + t.Helper() + // create a validator + valAddr := sdk.ValAddress(valPubKey.Address().Bytes()) + pubKey, err := k1util.PubKeyToCosmos(valPubKey) + require.NoError(err) + val := testutil.NewValidator(t, valAddr, pubKey) + validator, _ := val.AddTokensFromDel(valTokens) + s.BankKeeper.EXPECT().SendCoinsFromModuleToModule(gomock.Any(), stypes.NotBondedPoolName, stypes.BondedPoolName, gomock.Any()) + _ = skeeper.TestingUpdateValidator(stakingKeeper, c, validator, true) + + accountKeeper.EXPECT().HasAccount(gomock.Any(), valDelAddr).Return(false) + accountKeeper.EXPECT().NewAccountWithAddress(gomock.Any(), valDelAddr).Return(nil) + accountKeeper.EXPECT().SetAccount(gomock.Any(), gomock.Any()) + bankKeeper.EXPECT().MintCoins(gomock.Any(), types.ModuleName, gomock.Any()).Return(nil) + bankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), types.ModuleName, valDelAddr, gomock.Any()).Return(nil) + bankKeeper.EXPECT().DelegateCoinsFromAccountToModule(gomock.Any(), valDelAddr, gomock.Any(), gomock.Any()).Return(nil) + }, + postCheck: checkDelegatorMapAndValTokens, + }, + } + + for _, tc := range tcs { + s.Run(tc.name, func() { + cachedCtx, _ := ctx.CacheContext() + // create valPubKey using tc.valPubKeyBytes + if tc.preRun != nil { + tc.preRun(s.T(), cachedCtx, tc.valDelAddr, tc.valPubKey, tc.valTokens) + } + moniker := tc.moniker + if moniker == "" { + moniker = "testing" + } + err := keeper.ProcessCreateValidator(cachedCtx, &bindings.IPTokenStakingCreateValidator{ + ValidatorUncmpPubkey: nil, + ValidatorCmpPubkey: tc.valPubKeyBytes, + Moniker: moniker, + StakeAmount: new(big.Int).SetUint64(100), + CommissionRate: 1000, // 10% + MaxCommissionRate: 5000, // 50% + MaxCommissionChangeRate: 500, // 5% + Raw: gethtypes.Log{}, + }) + if tc.expectedError != "" { + require.Error(err, tc.expectedError) + } else { + require.NoError(err) + delEvmAddr, err := k1util.CosmosPubkeyToEVMAddress(tc.valPubKeyBytes) + require.NoError(err) + tc.postCheck(cachedCtx, tc.valDelAddr, tc.valAddr, delEvmAddr, tc.valTokens) + } + }) + } +} + +func (s *TestSuite) TestParseCreateValidatorLog() { + require := s.Require() + keeper := s.EVMStakingKeeper + + testCases := []struct { + name string + log gethtypes.Log + expectErr bool + }{ + { + name: "Unknown Topic", + log: gethtypes.Log{ + Topics: []common.Hash{common.HexToHash("0x1111111111111111111111111111111111111111111111111111111111111111")}, + }, + expectErr: true, + }, + { + name: "Valid Topic", + log: gethtypes.Log{ + Topics: []common.Hash{types.CreateValidatorEvent.ID}, + }, + expectErr: false, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + _, err := keeper.ParseCreateValidatorLog(tc.log) + if tc.expectErr { + require.Error(err, "should return error for %s", tc.name) + } else { + require.NoError(err, "should not return error for %s", tc.name) + } + }) + } +}