Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore!: lsm redelegation follow-up #22538

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 95 additions & 7 deletions x/slashing/keeper/slash_redelegation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,42 +47,63 @@ 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)
require.NoError(t, err)

// 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))
Expand All @@ -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)

Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
55 changes: 54 additions & 1 deletion x/staking/keeper/invariants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
}
}
2 changes: 1 addition & 1 deletion x/staking/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions x/staking/keeper/slash.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading