-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
fix(x/slashing): Avoid overslashing on redelegation + unbonding in specific situations #20688
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -185,3 +185,208 @@ func fundAccount(t *testing.T, ctx context.Context, bankKeeper bankkeeper.Keeper | |
|
||
require.NoError(t, banktestutil.FundAccount(ctx, bankKeeper, addr, amount)) | ||
} | ||
|
||
func TestOverSlashing(t *testing.T) { | ||
// slash penalty percentage | ||
slashFraction := "0.50" | ||
|
||
// percentage of (undelegation/(undelegation + redelegation)) | ||
undelegationPercentageStr := "0.40" | ||
|
||
// setting up | ||
var ( | ||
stakingKeeper *stakingkeeper.Keeper | ||
bankKeeper bankkeeper.Keeper | ||
slashKeeper slashingkeeper.Keeper | ||
distrKeeper distributionkeeper.Keeper | ||
authKeeper authkeeper.AccountKeeper | ||
) | ||
|
||
app, err := simtestutil.Setup(depinject.Configs( | ||
depinject.Supply(log.NewNopLogger()), | ||
slashing.AppConfig, | ||
), &stakingKeeper, &bankKeeper, &slashKeeper, &distrKeeper, &authKeeper) | ||
require.NoError(t, err) | ||
|
||
// get sdk context, staking msg server and bond denom | ||
ctx := app.BaseApp.NewContext(false) | ||
stakingMsgServer := stakingkeeper.NewMsgServerImpl(stakingKeeper) | ||
bondDenom, err := stakingKeeper.BondDenom(ctx) | ||
require.NoError(t, err) | ||
|
||
// evilVal will be slashed, goodVal won't be slashed | ||
evilValPubKey := secp256k1.GenPrivKey().PubKey() | ||
goodValPubKey := secp256k1.GenPrivKey().PubKey() | ||
|
||
/* | ||
all test accs will delegate to evil val, which evil validator will eventually be slashed | ||
|
||
- test acc 1: redelegate -> undelegate full amount | ||
- test acc 2: simple undelegation. intended scenario. | ||
- test acc 3: redelegate -> undelegate some amount | ||
|
||
*/ | ||
|
||
testAcc1 := sdk.AccAddress([]byte("addr1new____________")) | ||
testAcc2 := sdk.AccAddress([]byte("addr2new____________")) | ||
testAcc3 := sdk.AccAddress([]byte("addr3new____________")) | ||
|
||
fmt.Println("testAcc1 address:", testAcc1.String()) | ||
fmt.Println("testAcc2 address:", testAcc2.String()) | ||
fmt.Println("testAcc3 address:", testAcc3.String()) | ||
facundomedica marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// fund all accounts | ||
testCoins := sdk.NewCoins(sdk.NewCoin(bondDenom, stakingKeeper.TokensFromConsensusPower(ctx, 1))) | ||
facundomedica marked this conversation as resolved.
Show resolved
Hide resolved
|
||
fundAccount(t, ctx, bankKeeper, authKeeper, testAcc1, testCoins) | ||
fundAccount(t, ctx, bankKeeper, authKeeper, testAcc2, testCoins) | ||
fundAccount(t, ctx, bankKeeper, authKeeper, testAcc3, testCoins) | ||
facundomedica marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
balance1Before := bankKeeper.GetBalance(ctx, testAcc1, bondDenom) | ||
balance2Before := bankKeeper.GetBalance(ctx, testAcc2, bondDenom) | ||
balance3Before := bankKeeper.GetBalance(ctx, testAcc3, bondDenom) | ||
|
||
fmt.Println("testAcc1 balance amount:", balance1Before.Amount) | ||
fmt.Println("testAcc2 balance amount:", balance2Before.Amount) | ||
fmt.Println("testAcc3 balance amount:", balance3Before.Amount) | ||
|
||
// assert acc 1, 2 and 3 balance | ||
require.Equal(t, testCoins[0].Amount.String(), balance1Before.Amount.String()) | ||
require.Equal(t, testCoins[0].Amount.String(), balance2Before.Amount.String()) | ||
require.Equal(t, testCoins[0].Amount.String(), balance3Before.Amount.String()) | ||
|
||
fmt.Println("initial balance for accounts: ", testCoins[0]) | ||
fmt.Println("slash percentage: ", slashFraction) | ||
slashPercentage := math.LegacyMustNewDecFromStr(slashFraction) | ||
|
||
penalty := testCoins[0].Amount.ToLegacyDec().Mul(slashPercentage).TruncateInt() | ||
postSlash := testCoins[0].Amount.Sub(penalty) | ||
fmt.Println("expected balance after slash: ", sdk.NewCoin(bondDenom, postSlash)) | ||
facundomedica marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// create evil val | ||
evilValAddr := sdk.ValAddress(evilValPubKey.Address()) | ||
fundAccount(t, ctx, bankKeeper, authKeeper, sdk.AccAddress(evilValAddr), testCoins) | ||
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)), math.OneInt()) | ||
_, err = stakingMsgServer.CreateValidator(ctx, createValMsg1) | ||
require.NoError(t, err) | ||
|
||
// create good val 1 | ||
goodValAddr := sdk.ValAddress(goodValPubKey.Address()) | ||
fundAccount(t, ctx, bankKeeper, authKeeper, sdk.AccAddress(goodValAddr), testCoins) | ||
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)), math.OneInt()) | ||
_, err = stakingMsgServer.CreateValidator(ctx, createValMsg2) | ||
require.NoError(t, err) | ||
|
||
// next block | ||
ctx = ctx.WithBlockHeight(app.LastBlockHeight() + 1).WithHeaderInfo(header.Info{Height: app.LastBlockHeight() + 1}) | ||
ctx, err = simtestutil.NextBlock(app, ctx, time.Duration(1)) | ||
require.NoError(t, err) | ||
|
||
// delegate all accs to evil val | ||
delMsg := stakingtypes.NewMsgDelegate(testAcc1.String(), evilValAddr.String(), testCoins[0]) | ||
_, err = stakingMsgServer.Delegate(ctx, delMsg) | ||
require.NoError(t, err) | ||
|
||
delMsg = stakingtypes.NewMsgDelegate(testAcc2.String(), evilValAddr.String(), testCoins[0]) | ||
_, err = stakingMsgServer.Delegate(ctx, delMsg) | ||
require.NoError(t, err) | ||
|
||
delMsg = stakingtypes.NewMsgDelegate(testAcc3.String(), evilValAddr.String(), testCoins[0]) | ||
_, err = stakingMsgServer.Delegate(ctx, delMsg) | ||
require.NoError(t, err) | ||
|
||
// next block | ||
ctx, err = simtestutil.NextBlock(app, ctx, time.Duration(1)) | ||
require.NoError(t, err) | ||
|
||
// evilValAddr done something bad | ||
misbehaveHeight := ctx.BlockHeader().Height | ||
fmt.Println("evilValAddr misbehaved in height: ", misbehaveHeight) | ||
evilVal, err := stakingKeeper.GetValidator(ctx, evilValAddr) | ||
require.NoError(t, err) | ||
|
||
evilValConsAddr, err := evilVal.GetConsAddr() | ||
require.NoError(t, err) | ||
|
||
evilPower := stakingKeeper.TokensToConsensusPower(ctx, evilVal.Tokens) | ||
|
||
// next block | ||
ctx, err = simtestutil.NextBlock(app, ctx, time.Duration(1)) | ||
require.NoError(t, err) | ||
|
||
// acc 1: redelegate to goodval1 and undelegate FULL amount | ||
redelMsg := stakingtypes.NewMsgBeginRedelegate(testAcc1.String(), evilValAddr.String(), goodValAddr.String(), testCoins[0]) | ||
_, err = stakingMsgServer.BeginRedelegate(ctx, redelMsg) | ||
require.NoError(t, err) | ||
undelMsg := stakingtypes.NewMsgUndelegate(testAcc1.String(), goodValAddr.String(), testCoins[0]) | ||
_, err = stakingMsgServer.Undelegate(ctx, undelMsg) | ||
require.NoError(t, err) | ||
|
||
// acc 2: undelegate full amount | ||
undelMsg = stakingtypes.NewMsgUndelegate(testAcc2.String(), evilValAddr.String(), testCoins[0]) | ||
_, err = stakingMsgServer.Undelegate(ctx, undelMsg) | ||
require.NoError(t, err) | ||
|
||
// acc 3: redelegate to goodval1 and undelegate some amount | ||
redelMsg = stakingtypes.NewMsgBeginRedelegate(testAcc3.String(), evilValAddr.String(), goodValAddr.String(), testCoins[0]) | ||
_, err = stakingMsgServer.BeginRedelegate(ctx, redelMsg) | ||
require.NoError(t, err) | ||
|
||
undelegationPercentage := math.LegacyMustNewDecFromStr(undelegationPercentageStr) | ||
undelegationAmountDec := math.LegacyNewDecFromInt(testCoins[0].Amount).Mul(undelegationPercentage) | ||
amountToUndelegate := undelegationAmountDec.TruncateInt() | ||
|
||
// next block | ||
ctx, err = simtestutil.NextBlock(app, ctx, time.Duration(1)) | ||
require.NoError(t, err) | ||
|
||
portionofTestCoins := sdk.NewCoins(sdk.NewCoin(bondDenom, amountToUndelegate)) | ||
fmt.Println("Undelegation amount:", portionofTestCoins[0].Amount) | ||
|
||
undelMsg = stakingtypes.NewMsgUndelegate(testAcc3.String(), goodValAddr.String(), portionofTestCoins[0]) | ||
_, err = stakingMsgServer.Undelegate(ctx, undelMsg) | ||
require.NoError(t, err) | ||
|
||
// next block | ||
ctx, err = simtestutil.NextBlock(app, ctx, time.Duration(1)) | ||
require.NoError(t, err) | ||
|
||
// slash the evil val | ||
err = slashKeeper.Slash(ctx, evilValConsAddr, math.LegacyMustNewDecFromStr(slashFraction), evilPower, misbehaveHeight) | ||
facundomedica marked this conversation as resolved.
Show resolved
Hide resolved
|
||
require.NoError(t, err) | ||
|
||
// assert invariants | ||
_, stop := stakingkeeper.AllInvariants(stakingKeeper)(ctx) | ||
require.False(t, stop) | ||
_, stop = bankkeeper.AllInvariants(bankKeeper)(ctx) | ||
require.False(t, stop) | ||
_, stop = distributionkeeper.AllInvariants(distrKeeper)(ctx) | ||
require.False(t, stop) | ||
|
||
// fastforward few block (and time) to complete redelegations and unbondings | ||
ctx, err = simtestutil.NextBlock(app, ctx, time.Duration(1000000000000000000)) | ||
require.NoError(t, err) | ||
ctx, err = simtestutil.NextBlock(app, ctx, time.Duration(1000000000000000000)) | ||
require.NoError(t, err) | ||
ctx, err = simtestutil.NextBlock(app, ctx, time.Duration(1000000000000000000)) | ||
require.NoError(t, err) | ||
facundomedica marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// we check all accounts should be slashed with the equal amount, and they should end up with same balance including staked amount | ||
stakedAcc1, err := stakingKeeper.GetDelegatorBonded(ctx, testAcc1) | ||
require.NoError(t, err) | ||
stakedAcc2, err := stakingKeeper.GetDelegatorBonded(ctx, testAcc2) | ||
require.NoError(t, err) | ||
stakedAcc3, err := stakingKeeper.GetDelegatorBonded(ctx, testAcc3) | ||
require.NoError(t, err) | ||
|
||
balance1AfterSlashing := bankKeeper.GetBalance(ctx, testAcc1, bondDenom).Add(sdk.NewCoin(bondDenom, stakedAcc1)) | ||
balance2AfterSlashing := bankKeeper.GetBalance(ctx, testAcc2, bondDenom).Add(sdk.NewCoin(bondDenom, stakedAcc2)) | ||
balance3AfterSlashing := bankKeeper.GetBalance(ctx, testAcc3, bondDenom).Add(sdk.NewCoin(bondDenom, stakedAcc3)) | ||
fmt.Println("testAcc1 balance amount:", balance1AfterSlashing.Amount, "staked amount:", stakedAcc1) | ||
fmt.Println("testAcc2 balance amount:", balance2AfterSlashing.Amount, "staked amount:", stakedAcc2) | ||
fmt.Println("testAcc3 balance amount:", balance3AfterSlashing.Amount, "staked amount:", stakedAcc3) | ||
|
||
require.Equal(t, balance1AfterSlashing.Amount, balance2AfterSlashing.Amount) | ||
require.Equal(t, balance2AfterSlashing.Amount, balance3AfterSlashing.Amount) | ||
facundomedica marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good test case. For bonus points:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll take a look but I think I won't be able to make it a table test as afaict we need more than one delegation for this to happen |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -365,8 +365,20 @@ func (k Keeper) SlashRedelegation(ctx context.Context, srcValidator types.Valida | |
|
||
// Slash the moved delegation | ||
// Unbond from target validator | ||
sharesToUnbond := slashFactor.Mul(entry.SharesDst) | ||
if sharesToUnbond.IsZero() || slashAmount.IsZero() { | ||
if slashAmount.IsZero() { | ||
continue | ||
} | ||
|
||
dstVal, err := k.GetValidator(ctx, valDstAddr) | ||
if err != nil { | ||
return math.ZeroInt(), err | ||
} | ||
sharesToUnbond, err := dstVal.SharesFromTokensTruncated(slashAmount) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I understood this correct, then you calculate the shares for the remaining slash amount. The slash amount may be (partially) filled from slashing unbonding delegations first. This looks correct to me and fixes the issue. Good work 👍 |
||
if err != nil { | ||
return math.ZeroInt(), err | ||
} | ||
|
||
if sharesToUnbond.IsZero() { | ||
continue | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: prefer an odd value so that you which half was actually used. Seeing some rounding can be interesting, too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed it to 45 👌