diff --git a/x/slashing/keeper/slash_redelegation_test.go b/x/slashing/keeper/slash_redelegation_test.go index 8b3031c0e2de..184a052802d9 100644 --- a/x/slashing/keeper/slash_redelegation_test.go +++ b/x/slashing/keeper/slash_redelegation_test.go @@ -47,27 +47,38 @@ func TestSlashRedelegation(t *testing.T) { evilValPubKey := secp256k1.GenPrivKey().PubKey() goodValPubKey := secp256k1.GenPrivKey().PubKey() - // both test acc 1 and 2 delegated to evil val, both acc should be slashed when evil val is slashed + // test acc 1, 2 and 3 delegated to evil val, all acc should be slashed when evil val is slashed // test acc 1 use the "undelegation after redelegation" trick (redelegate to good val and then undelegate) to avoid slashing // test acc 2 only undelegate from evil val + // test acc 3 redelegate to good val and stays bonded + // test acc 4 redelegates liquid staked tokens testAcc1 := sdk.AccAddress([]byte("addr1_______________")) testAcc2 := sdk.AccAddress([]byte("addr2_______________")) + testAcc3 := sdk.AccAddress([]byte("addr3_______________")) + testAcc4 := sdk.AccAddress([]byte("addr4_______________")) - // fund acc 1 and acc 2 + // fund accounts testCoins := sdk.NewCoins(sdk.NewCoin(bondDenom, stakingKeeper.TokensFromConsensusPower(ctx, 10))) + validatorFundCoins := sdk.NewCoins(sdk.NewCoin(bondDenom, stakingKeeper.TokensFromConsensusPower(ctx, 20))) banktestutil.FundAccount(ctx, bankKeeper, testAcc1, testCoins) banktestutil.FundAccount(ctx, bankKeeper, testAcc2, testCoins) + banktestutil.FundAccount(ctx, bankKeeper, testAcc3, testCoins) + banktestutil.FundAccount(ctx, bankKeeper, testAcc4, testCoins) balance1Before := bankKeeper.GetBalance(ctx, testAcc1, bondDenom) balance2Before := bankKeeper.GetBalance(ctx, testAcc2, bondDenom) + balance3Before := bankKeeper.GetBalance(ctx, testAcc3, bondDenom) + balance4Before := bankKeeper.GetBalance(ctx, testAcc3, bondDenom) - // assert acc 1 and acc 2 balance + // assert acc balances require.Equal(t, balance1Before.Amount.String(), testCoins[0].Amount.String()) require.Equal(t, balance2Before.Amount.String(), testCoins[0].Amount.String()) + require.Equal(t, balance3Before.Amount.String(), testCoins[0].Amount.String()) + require.Equal(t, balance4Before.Amount.String(), testCoins[0].Amount.String()) // creating evil val evilValAddr := sdk.ValAddress(evilValPubKey.Address()) - banktestutil.FundAccount(ctx, bankKeeper, sdk.AccAddress(evilValAddr), testCoins) + banktestutil.FundAccount(ctx, bankKeeper, sdk.AccAddress(evilValAddr), validatorFundCoins) createValMsg1, _ := stakingtypes.NewMsgCreateValidator( evilValAddr.String(), evilValPubKey, testCoins[0], stakingtypes.Description{Details: "test"}, stakingtypes.NewCommissionRates(math.LegacyNewDecWithPrec(5, 1), math.LegacyNewDecWithPrec(5, 1), math.LegacyNewDec(0))) _, err = stakingMsgServer.CreateValidator(ctx, createValMsg1) @@ -75,14 +86,24 @@ func TestSlashRedelegation(t *testing.T) { // creating good val goodValAddr := sdk.ValAddress(goodValPubKey.Address()) - banktestutil.FundAccount(ctx, bankKeeper, sdk.AccAddress(goodValAddr), testCoins) + goodValAccAddr := sdk.AccAddress(goodValPubKey.Address()) + banktestutil.FundAccount(ctx, bankKeeper, sdk.AccAddress(goodValAddr), validatorFundCoins) createValMsg2, _ := stakingtypes.NewMsgCreateValidator( goodValAddr.String(), goodValPubKey, testCoins[0], stakingtypes.Description{Details: "test"}, stakingtypes.NewCommissionRates(math.LegacyNewDecWithPrec(5, 1), math.LegacyNewDecWithPrec(5, 1), math.LegacyNewDec(0))) _, err = stakingMsgServer.CreateValidator(ctx, createValMsg2) require.NoError(t, err) + // self bond tokens + bondValMsg := stakingtypes.NewMsgValidatorBond(goodValAccAddr.String(), goodValAddr.String()) + _, err = stakingMsgServer.ValidatorBond(ctx, bondValMsg) + require.NoError(t, err) + + goodVal, err := stakingKeeper.GetValidator(ctx, goodValAddr) + require.NoError(t, err) + goodValSelfBondInitial := goodVal.ValidatorBondShares + // next block, commit height 2, move to height 3 - // acc 1 and acc 2 delegate to evil val + // acc 1, 2, 3, 4 and good validator delegate to evil val ctx = ctx.WithBlockHeight(app.LastBlockHeight() + 1).WithHeaderInfo(header.Info{Height: app.LastBlockHeight() + 1}) fmt.Println() ctx, err = simtestutil.NextBlock(app, ctx, time.Duration(1)) @@ -98,9 +119,34 @@ func TestSlashRedelegation(t *testing.T) { _, err = stakingMsgServer.Delegate(ctx, delMsg) require.NoError(t, err) + // Acc 3 delegate + delMsg = stakingtypes.NewMsgDelegate(testAcc3.String(), evilValAddr.String(), testCoins[0]) + _, err = stakingMsgServer.Delegate(ctx, delMsg) + require.NoError(t, err) + + // Acc 4 delegate + delMsg = stakingtypes.NewMsgDelegate(testAcc4.String(), evilValAddr.String(), testCoins[0]) + _, err = stakingMsgServer.Delegate(ctx, delMsg) + require.NoError(t, err) + + // Good validator delegates to evil validator + delMsg = stakingtypes.NewMsgDelegate(goodValAccAddr.String(), evilValAddr.String(), testCoins[0]) + _, err = stakingMsgServer.Delegate(ctx, delMsg) + require.NoError(t, err) + + // Acc 4 liquid stake stokens + tokenizeMsg := stakingtypes.NewMsgTokenizeShares(testAcc4.String(), evilValAddr.String(), testCoins[0], testAcc4.String()) + res, err := stakingMsgServer.TokenizeShares(ctx, tokenizeMsg) + require.NoError(t, err) + + tokenizeSharedRcd, err := stakingKeeper.GetTokenizeShareRecordByDenom(ctx, res.Amount.Denom) + require.NoError(t, err) + moduleAddr := tokenizeSharedRcd.GetModuleAddress() + // next block, commit height 3, move to height 4 // with the new delegations, evil val increases in voting power and commit byzantine behavior at height 4 consensus - // at the same time, acc 1 and acc 2 withdraw delegation from evil val + // at the same time, acc 1 and acc 2 withdraw delegation from evil val, acc 3 redelegate but stays bonded + // good validator redelegates to itself ctx, err = simtestutil.NextBlock(app, ctx, time.Duration(1)) require.NoError(t, err) @@ -114,6 +160,23 @@ func TestSlashRedelegation(t *testing.T) { _, err = stakingMsgServer.BeginRedelegate(ctx, redelMsg) require.NoError(t, err) + // Acc 3 redelegate from evil val to good val + redelMsg = stakingtypes.NewMsgBeginRedelegate(testAcc3.String(), evilValAddr.String(), goodValAddr.String(), testCoins[0]) + _, err = stakingMsgServer.BeginRedelegate(ctx, redelMsg) + require.NoError(t, err) + + // Acc 4 redelegates tokenized shares from evil val to good val + // we set moduleAddr as a delegator because delegator of a tokenize share is lsm module. This is hack for testing, + // in normal scenario this will be ica address that liquid staked some tokens + redelMsg = stakingtypes.NewMsgBeginRedelegate(moduleAddr.String(), evilValAddr.String(), goodValAddr.String(), testCoins[0]) + _, err = stakingMsgServer.BeginRedelegate(ctx, redelMsg) + require.NoError(t, err) + + // Good validator redelegates to itself + redelMsg = stakingtypes.NewMsgBeginRedelegate(goodValAccAddr.String(), evilValAddr.String(), goodValAddr.String(), testCoins[0]) + _, err = stakingMsgServer.BeginRedelegate(ctx, redelMsg) + require.NoError(t, err) + // Acc 1 undelegate from good val undelMsg := stakingtypes.NewMsgUndelegate(testAcc1.String(), goodValAddr.String(), testCoins[0]) _, err = stakingMsgServer.Undelegate(ctx, undelMsg) @@ -124,6 +187,15 @@ func TestSlashRedelegation(t *testing.T) { _, err = stakingMsgServer.Undelegate(ctx, undelMsg) require.NoError(t, err) + // get states before slashing + delegation3BeforeSlashing, err := stakingKeeper.GetDelegatorBonded(ctx, testAcc3) + require.NoError(t, err) + + goodVal, err = stakingKeeper.GetValidator(ctx, goodValAddr) + require.NoError(t, err) + goodValSelfBondAfterRedelegation := goodVal.ValidatorBondShares + goodValLiquidSharesAfterRedelegation := goodVal.LiquidShares + // next block, commit height 4, move to height 5 // Slash evil val for byzantine behavior at height 4 consensus, // at which acc 1 and acc 2 still contributed to evil val voting power @@ -159,7 +231,23 @@ func TestSlashRedelegation(t *testing.T) { // confirm that account 1 and account 2 has been slashed, and the slash amount is correct balance1AfterSlashing := bankKeeper.GetBalance(ctx, testAcc1, bondDenom) balance2AfterSlashing := bankKeeper.GetBalance(ctx, testAcc2, bondDenom) + delegation3AfterSlashing, err := stakingKeeper.GetDelegatorBonded(ctx, testAcc3) + require.NoError(t, err) + // check unbonded amounts require.Equal(t, balance1AfterSlashing.Amount.Mul(math.NewIntFromUint64(10)).String(), balance1Before.Amount.String()) require.Equal(t, balance2AfterSlashing.Amount.Mul(math.NewIntFromUint64(10)).String(), balance2Before.Amount.String()) + + // check bonded amount + require.Equal(t, delegation3AfterSlashing.Mul(math.NewIntFromUint64(10)).String(), delegation3BeforeSlashing.String()) + + // check validator bonded amount + goodVal, err = stakingKeeper.GetValidator(ctx, goodValAddr) + require.NoError(t, err) + redelegatedAmountBeforeSlashing := goodValSelfBondAfterRedelegation.Sub(goodValSelfBondInitial) // unslashed redelegated amount + redelegatedAmountAfterSlashing := goodVal.ValidatorBondShares.Sub(goodValSelfBondInitial) // slashed redelegated amount + require.Equal(t, redelegatedAmountAfterSlashing.Mul(math.LegacyNewDec(10)), redelegatedAmountBeforeSlashing) + + // check liquid shares + require.Equal(t, goodVal.LiquidShares.Mul(math.LegacyNewDec(10)), goodValLiquidSharesAfterRedelegation) } diff --git a/x/staking/keeper/invariants.go b/x/staking/keeper/invariants.go index b23922643ed1..e8e23f0f9aad 100644 --- a/x/staking/keeper/invariants.go +++ b/x/staking/keeper/invariants.go @@ -20,6 +20,8 @@ func RegisterInvariants(ir sdk.InvariantRegistry, k *Keeper) { PositiveDelegationInvariant(k)) ir.RegisterRoute(types.ModuleName, "delegator-shares", DelegatorSharesInvariant(k)) + ir.RegisterRoute(types.ModuleName, "liquid-stake", + LiquidStakeInvariant(k)) } // AllInvariants runs all invariants of the staking module. @@ -40,7 +42,12 @@ func AllInvariants(k *Keeper) sdk.Invariant { return res, stop } - return DelegatorSharesInvariant(k)(ctx) + res, stop = DelegatorSharesInvariant(k)(ctx) + if stop { + return res, stop + } + + return LiquidStakeInvariant(k)(ctx) } } @@ -186,10 +193,13 @@ func DelegatorSharesInvariant(k *Keeper) sdk.Invariant { } validatorsDelegationShares := map[string]math.LegacyDec{} + allValidatorBondShares := math.LegacyNewDec(0) + allValidatorBondDelegations := math.LegacyNewDec(0) // initialize a map: validator -> its delegation shares for _, validator := range validators { validatorsDelegationShares[validator.GetOperator()] = math.LegacyZeroDec() + allValidatorBondShares = allValidatorBondShares.Add(validator.ValidatorBondShares) } // iterate through all the delegations to calculate the total delegation shares for each validator @@ -202,6 +212,9 @@ func DelegatorSharesInvariant(k *Keeper) sdk.Invariant { delegationValidatorAddr := delegation.GetValidatorAddr() validatorDelegationShares := validatorsDelegationShares[delegationValidatorAddr] validatorsDelegationShares[delegationValidatorAddr] = validatorDelegationShares.Add(delegation.Shares) + if delegation.ValidatorBond { + allValidatorBondDelegations = allValidatorBondDelegations.Add(delegation.Shares) + } } // for each validator, check if its total delegation shares calculated from the step above equals to its expected delegation shares @@ -216,6 +229,46 @@ func DelegatorSharesInvariant(k *Keeper) sdk.Invariant { } } + // compare bonded shares + if !allValidatorBondShares.Equal(allValidatorBondDelegations) { + broken = true + msg += fmt.Sprintf("broken delegator shares invariance:\n"+ + "\t sum of validator.ValidatorBondShares: %v\n"+ + "\tsum of validator bonded delegation.Shares: %v\n", allValidatorBondShares, allValidatorBondDelegations) + } + return sdk.FormatInvariant(types.ModuleName, "delegator shares", msg), broken } } + +func LiquidStakeInvariant(k *Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + var ( + msg string + broken bool + ) + + validators, err := k.GetAllValidators(ctx) + if err != nil { + panic(err) + } + + totalLiquidStake := k.GetTotalLiquidStakedTokens(ctx) + + // check if its total liquid staked tokens equals to liquid stake of all validators + calculatedTotalLiquidStake := math.NewInt(0) + + for _, validator := range validators { + calculatedTotalLiquidStake = calculatedTotalLiquidStake.Add(validator.TokensFromShares(validator.LiquidShares).TruncateInt()) + } + + if !totalLiquidStake.Equal(calculatedTotalLiquidStake) { + broken = true + msg += fmt.Sprintf("broken liquid stake invariant:\n"+ + "\tk.GetTotalLiquidStakedTokens: %v\n"+ + "\tsum of validator.LiquidShares: %v\n", totalLiquidStake, calculatedTotalLiquidStake) + } + + return sdk.FormatInvariant(types.ModuleName, "liquid stake", msg), broken + } +} diff --git a/x/staking/keeper/msg_server.go b/x/staking/keeper/msg_server.go index af9a5d9a6ab4..b7253b8d44f4 100644 --- a/x/staking/keeper/msg_server.go +++ b/x/staking/keeper/msg_server.go @@ -369,7 +369,7 @@ func (k msgServer) BeginRedelegate(ctx context.Context, msg *types.MsgBeginRedel } // If this is a validator self-bond, the new liquid delegation cannot fall below the self-bond * bond factor - // The delegation on the new validator will not a validator bond + // The delegation on the new validator will not be a validator bond if srcDelegation.ValidatorBond { if err := k.SafelyDecreaseValidatorBond(ctx, valSrcAddr, srcShares); err != nil { return nil, err diff --git a/x/staking/keeper/slash.go b/x/staking/keeper/slash.go index 38d2c484fc75..e5c5a0cc3aed 100644 --- a/x/staking/keeper/slash.go +++ b/x/staking/keeper/slash.go @@ -402,6 +402,24 @@ func (k Keeper) SlashRedelegation(ctx context.Context, srcValidator types.Valida return math.ZeroInt(), err } + // if the delegator holds a validator bond to destination validator, decrease the destination validator bond shares + if delegation.ValidatorBond { + if err := k.SafelyDecreaseValidatorBond(ctx, valDstAddr, sharesToUnbond); err != nil { + return math.ZeroInt(), err + } + } + + // if this delegation is from a liquid staking provider (identified if the delegator + // is an ICA account), the global and validator liquid totals should be decremented + if k.DelegatorIsLiquidStaker(delegatorAddress) { + if err := k.DecreaseTotalLiquidStakedTokens(ctx, tokensToBurn); err != nil { + return math.ZeroInt(), err + } + if _, err := k.DecreaseValidatorLiquidShares(ctx, valDstAddr, sharesToUnbond); err != nil { + return math.ZeroInt(), err + } + } + // tokens of a redelegation currently live in the destination validator // therefor we must burn tokens from the destination-validator's bonding status switch {