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

fix: add validation for potential slashing evasion during re-delegation #1306

Merged
merged 7 commits into from
Mar 27, 2024
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* (types) [\#1299](https://github.com/Finschia/finschia-sdk/pull/1299) add missing nil checks
* (x/staking) [\#1301](https://github.com/Finschia/finschia-sdk/pull/1301) Use bytes instead of string comparison in delete validator queue (backport cosmos/cosmos-sdk#12303)
* (x/gov) [\#1304](https://github.com/Finschia/finschia-sdk/pull/1304) fetch a failed proposal tally from proposal.FinalTallyResult in the gprc query
* (x/staking) [\#1306](https://github.com/Finschia/finschia-sdk/pull/1306) add validation for potential slashing evasion during re-delegation

### Removed

Expand Down
16 changes: 16 additions & 0 deletions simapp/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"net/http"
"os"
"path/filepath"
"time"

"github.com/gorilla/mux"
"github.com/spf13/cast"
Expand Down Expand Up @@ -597,6 +598,21 @@
return MakeTestEncodingConfig().TxConfig
}

// NextBlock starts a new block.
func (app *SimApp) NextBlock(ctx sdk.Context, jumpTime time.Duration) sdk.Context {
app.EndBlock(abci.RequestEndBlock{Height: ctx.BlockHeight()})

Check warning on line 603 in simapp/app.go

View check run for this annotation

Codecov / codecov/patch

simapp/app.go#L602-L603

Added lines #L602 - L603 were not covered by tests

app.Commit()

Check warning on line 605 in simapp/app.go

View check run for this annotation

Codecov / codecov/patch

simapp/app.go#L605

Added line #L605 was not covered by tests

newBlockTime := ctx.BlockTime().Add(jumpTime)
nextHeight := ctx.BlockHeight() + 1
newHeader := tmproto.Header{Height: nextHeight, Time: newBlockTime}

Check warning on line 609 in simapp/app.go

View check run for this annotation

Codecov / codecov/patch

simapp/app.go#L607-L609

Added lines #L607 - L609 were not covered by tests

app.BeginBlock(abci.RequestBeginBlock{Header: newHeader})

Check warning on line 611 in simapp/app.go

View check run for this annotation

Codecov / codecov/patch

simapp/app.go#L611

Added line #L611 was not covered by tests

return app.NewUncachedContext(false, newHeader)

Check warning on line 613 in simapp/app.go

View check run for this annotation

Codecov / codecov/patch

simapp/app.go#L613

Added line #L613 was not covered by tests
}

// AppCodec returns SimApp's app codec.
//
// NOTE: This is solely to be used for testing purposes as it may be desirable
Expand Down
151 changes: 151 additions & 0 deletions x/slashing/keeper/slash_redelegation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package keeper_test

import (
"testing"
"time"

"github.com/stretchr/testify/require"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"

"github.com/Finschia/finschia-sdk/crypto/keys/secp256k1"
"github.com/Finschia/finschia-sdk/simapp"
sdk "github.com/Finschia/finschia-sdk/types"
bankkeeper "github.com/Finschia/finschia-sdk/x/bank/keeper"
banktestutil "github.com/Finschia/finschia-sdk/x/bank/testutil"
distributionkeeper "github.com/Finschia/finschia-sdk/x/distribution/keeper"
stakingkeeper "github.com/Finschia/finschia-sdk/x/staking/keeper"
stakingtypes "github.com/Finschia/finschia-sdk/x/staking/types"
)

func TestSlashRedelegation(t *testing.T) {
// setting up
app := simapp.Setup(false)

stakingKeeper := app.StakingKeeper
bankKeeper := app.BankKeeper
slashKeeper := app.SlashingKeeper
distrKeeper := app.DistrKeeper

// get sdk context, staking msg server and bond denom
ctx := app.BaseApp.NewContext(false, tmproto.Header{Height: app.LastBlockHeight() + 1})
stakingMsgServer := stakingkeeper.NewMsgServerImpl(stakingKeeper)
bondDenom := stakingKeeper.BondDenom(ctx)

// evilVal will be slashed, goodVal won't be slashed
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 use the "undelegation after redelegation" trick (redelegate to good val and then undelegate) to avoid slashing
// test acc 2 only undelegate from evil val
testAcc1 := sdk.AccAddress([]byte("addr1_______________"))
testAcc2 := sdk.AccAddress([]byte("addr2_______________"))

// fund acc 1 and acc 2
testCoins := sdk.NewCoins(sdk.NewCoin(bondDenom, stakingKeeper.TokensFromConsensusPower(ctx, 10)))
err := banktestutil.FundAccount(bankKeeper, ctx, testAcc1, testCoins)
require.NoError(t, err)
err = banktestutil.FundAccount(bankKeeper, ctx, testAcc2, testCoins)
require.NoError(t, err)

balance1Before := bankKeeper.GetBalance(ctx, testAcc1, bondDenom)
balance2Before := bankKeeper.GetBalance(ctx, testAcc2, bondDenom)

// assert acc 1 and acc 2 balance
require.Equal(t, balance1Before.Amount.String(), testCoins[0].Amount.String())
require.Equal(t, balance2Before.Amount.String(), testCoins[0].Amount.String())

// creating evil val
evilValAddr := sdk.ValAddress(evilValPubKey.Address())
err = banktestutil.FundAccount(bankKeeper, ctx, sdk.AccAddress(evilValAddr), testCoins)
require.NoError(t, err)
createValMsg1, _ := stakingtypes.NewMsgCreateValidator(
evilValAddr, evilValPubKey, testCoins[0], stakingtypes.Description{Details: "test"}, stakingtypes.NewCommissionRates(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0)), sdk.OneInt())
_, err = stakingMsgServer.CreateValidator(sdk.WrapSDKContext(ctx), createValMsg1)
require.NoError(t, err)

// creating good val
goodValAddr := sdk.ValAddress(goodValPubKey.Address())
err = banktestutil.FundAccount(bankKeeper, ctx, sdk.AccAddress(goodValAddr), testCoins)
require.NoError(t, err)
createValMsg2, _ := stakingtypes.NewMsgCreateValidator(
goodValAddr, goodValPubKey, testCoins[0], stakingtypes.Description{Details: "test"}, stakingtypes.NewCommissionRates(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0)), sdk.OneInt())
_, err = stakingMsgServer.CreateValidator(sdk.WrapSDKContext(ctx), createValMsg2)
require.NoError(t, err)

// next block, commit height 1, move to height 2
// acc 1 and acc 2 delegate to evil val
ctx = app.NextBlock(ctx, time.Duration(1))
require.NoError(t, err)

// Acc 2 delegate
delMsg := stakingtypes.NewMsgDelegate(testAcc2, evilValAddr, testCoins[0])
_, err = stakingMsgServer.Delegate(sdk.WrapSDKContext(ctx), delMsg)
require.NoError(t, err)

// Acc 1 delegate
delMsg = stakingtypes.NewMsgDelegate(testAcc1, evilValAddr, testCoins[0])
_, err = stakingMsgServer.Delegate(sdk.WrapSDKContext(ctx), delMsg)
require.NoError(t, err)

// next block, commit height 2, move to height 3
// with the new delegations, evil val increases in voting power and commit byzantine behavior at height 3 consensus
// at the same time, acc 1 and acc 2 withdraw delegation from evil val
ctx = app.NextBlock(ctx, time.Duration(1))
require.NoError(t, err)

evilVal, found := stakingKeeper.GetValidator(ctx, evilValAddr)
require.True(t, found)

evilPower := stakingKeeper.TokensToConsensusPower(ctx, evilVal.Tokens)

// Acc 1 redelegate from evil val to good val
redelMsg := stakingtypes.NewMsgBeginRedelegate(testAcc1, evilValAddr, goodValAddr, testCoins[0])
_, err = stakingMsgServer.BeginRedelegate(sdk.WrapSDKContext(ctx), redelMsg)
require.NoError(t, err)

// Acc 1 undelegate from good val
undelMsg := stakingtypes.NewMsgUndelegate(testAcc1, goodValAddr, testCoins[0])
_, err = stakingMsgServer.Undelegate(sdk.WrapSDKContext(ctx), undelMsg)
require.NoError(t, err)

// Acc 2 undelegate from evil val
undelMsg = stakingtypes.NewMsgUndelegate(testAcc2, evilValAddr, testCoins[0])
_, err = stakingMsgServer.Undelegate(sdk.WrapSDKContext(ctx), undelMsg)
require.NoError(t, err)

// next block, commit height 3, move to height 4
// Slash evil val for byzantine behavior at height 3 consensus,
// at which acc 1 and acc 2 still contributed to evil val voting power
// even tho they undelegate at block 3, the valset update is applied after committed block 3 when height 3 consensus already passes
ctx = app.NextBlock(ctx, time.Duration(1))
require.NoError(t, err)

// slash evil val with slash factor = 0.9, leaving only 10% of stake after slashing
evilVal, _ = stakingKeeper.GetValidator(ctx, evilValAddr)
evilValConsAddr, err := evilVal.GetConsAddr()
require.NoError(t, err)

slashKeeper.Slash(ctx, evilValConsAddr, sdk.MustNewDecFromStr("0.9"), evilPower, 3)

// assert invariant to make sure we conduct slashing correctly
_, 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)

// one eternity later
ctx = app.NextBlock(ctx, time.Duration(1000000000000000000))
ctx = app.NextBlock(ctx, time.Duration(1))

// 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)

require.Equal(t, balance1AfterSlashing.Amount.Mul(sdk.NewIntFromUint64(10)).String(), balance1Before.Amount.String())
require.Equal(t, balance2AfterSlashing.Amount.Mul(sdk.NewIntFromUint64(10)).String(), balance2Before.Amount.String())
}
39 changes: 38 additions & 1 deletion x/staking/keeper/slash.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,46 @@
slashAmount := slashAmountDec.TruncateInt()
totalSlashAmount = totalSlashAmount.Add(slashAmount)

validatorDstAddress, err := sdk.ValAddressFromBech32(redelegation.ValidatorDstAddress)
if err != nil {
panic(err)

Check warning on line 253 in x/staking/keeper/slash.go

View check run for this annotation

Codecov / codecov/patch

x/staking/keeper/slash.go#L253

Added line #L253 was not covered by tests
}
// Handle undelegation after redelegation
// Prioritize slashing unbondingDelegation than delegation
unbondingDelegation, found := k.GetUnbondingDelegation(ctx, sdk.MustAccAddressFromBech32(redelegation.DelegatorAddress), validatorDstAddress)
if found {
for i, entry := range unbondingDelegation.Entries {

Check warning on line 259 in x/staking/keeper/slash.go

View check run for this annotation

Codecov / codecov/patch

x/staking/keeper/slash.go#L259

Added line #L259 was not covered by tests
// slash with the amount of `slashAmount` if possible, else slash all unbonding token
unbondingSlashAmount := sdk.MinInt(slashAmount, entry.Balance)

Check warning on line 261 in x/staking/keeper/slash.go

View check run for this annotation

Codecov / codecov/patch

x/staking/keeper/slash.go#L261

Added line #L261 was not covered by tests

switch {

Check warning on line 263 in x/staking/keeper/slash.go

View check run for this annotation

Codecov / codecov/patch

x/staking/keeper/slash.go#L263

Added line #L263 was not covered by tests
// There's no token to slash
case unbondingSlashAmount.IsZero():
continue

Check warning on line 266 in x/staking/keeper/slash.go

View check run for this annotation

Codecov / codecov/patch

x/staking/keeper/slash.go#L265-L266

Added lines #L265 - L266 were not covered by tests
// If unbonding started before this height, stake didn't contribute to infraction
case entry.CreationHeight < infractionHeight:
continue

Check warning on line 269 in x/staking/keeper/slash.go

View check run for this annotation

Codecov / codecov/patch

x/staking/keeper/slash.go#L268-L269

Added lines #L268 - L269 were not covered by tests
// Unbonding delegation no longer eligible for slashing, skip it
case entry.IsMature(now):
continue

Check warning on line 272 in x/staking/keeper/slash.go

View check run for this annotation

Codecov / codecov/patch

x/staking/keeper/slash.go#L271-L272

Added lines #L271 - L272 were not covered by tests
// Slash the unbonding delegation
default:

Check warning on line 274 in x/staking/keeper/slash.go

View check run for this annotation

Codecov / codecov/patch

x/staking/keeper/slash.go#L274

Added line #L274 was not covered by tests
// update remaining slashAmount
slashAmount = slashAmount.Sub(unbondingSlashAmount)

Check warning on line 276 in x/staking/keeper/slash.go

View check run for this annotation

Codecov / codecov/patch

x/staking/keeper/slash.go#L276

Added line #L276 was not covered by tests

notBondedBurnedAmount = notBondedBurnedAmount.Add(unbondingSlashAmount)
entry.Balance = entry.Balance.Sub(unbondingSlashAmount)
unbondingDelegation.Entries[i] = entry
k.SetUnbondingDelegation(ctx, unbondingDelegation)

Check warning on line 281 in x/staking/keeper/slash.go

View check run for this annotation

Codecov / codecov/patch

x/staking/keeper/slash.go#L278-L281

Added lines #L278 - L281 were not covered by tests
}
}
}

// Slash the moved delegation

// Unbond from target validator
sharesToUnbond := slashFactor.Mul(entry.SharesDst)
if sharesToUnbond.IsZero() {
if sharesToUnbond.IsZero() || slashAmount.IsZero() {
continue
}

Expand Down
Loading