Skip to content

Commit 9bd1e03

Browse files
authored
Merge pull request #202 from yihuang/release/v0.47.x
Merge pull request from GHSA-86h5-xcpx-cfqc
2 parents 0f7197f + 7d76e2f commit 9bd1e03

File tree

5 files changed

+221
-7
lines changed

5 files changed

+221
-7
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ Ref: https://keepachangelog.com/en/1.0.0/
3737

3838
## [Unreleased]
3939

40+
* (x/staking) Fix a possible bypass of delagator slashing: [GHSA-86h5-xcpx-cfqc](https://github.com/cosmos/cosmos-sdk/security/advisories/GHSA-86h5-xcpx-cfqc)
41+
4042
## [v0.47.9](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.47.9) - 2024-02-19
4143

4244
### Bug Fixes

RELEASE_NOTES.md

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1-
# Cosmos SDK v0.47.9 Release Notes
1+
# Cosmos SDK v0.47.10 Release Notes
22

33
💬 [**Release Discussion**](https://github.com/orgs/cosmos/discussions/6)
44

55
## 🚀 Highlights
66

7-
This patch release includes a fix in baseapp in `DefaultProposalHandler` and fixes [GHSA-4j93-fm92-rp4m](https://github.com/cosmos/cosmos-sdk/security/advisories/GHSA-4j93-fm92-rp4m).
7+
This early monthly patch release fixes [GHSA-86h5-xcpx-cfqc](https://github.com/cosmos/cosmos-sdk/security/advisories/GHSA-86h5-xcpx-cfqc).
88

99
We recommended to upgrade to this patch release as soon as possible.
10-
When upgrading from <= v0.47.8, please ensure that 2/3 of the validator power upgrade to v0.47.9.
10+
When upgrading from <= v0.47.9, please ensure that 2/3 of the validator power upgrade to v0.47.10.
1111

12-
Curious? Check out the [changelog](https://github.com/cosmos/cosmos-sdk/blob/v0.47.9/CHANGELOG.md) for an exhaustive list of changes or [compare changes](https://github.com/cosmos/cosmos-sdk/compare/v0.47.8...v0.47.9) from last release.
12+
Curious? Check out the [changelog](https://github.com/cosmos/cosmos-sdk/blob/v0.47.10/CHANGELOG.md) for an exhaustive list of changes or [compare changes](https://github.com/cosmos/cosmos-sdk/compare/v0.47.9...v0.47.10) from last release.
1313

1414
Refer to the [upgrading guide](https://github.com/cosmos/cosmos-sdk/blob/release/v0.50.x/UPGRADING.md) when migrating from `v0.47.x` to `v0.50.x`.
1515

1616
## Maintenance Policy
1717

18-
v0.50 has been released which means the v0.47.x line is now supported for bug fixes only, as per our release policy.
19-
Start integrating with [Cosmos SDK Eden (v0.50)](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.50.4) and enjoy and the new features and performance improvements.
18+
v0.50 has been released which means the v0.47.x line is now supported for bug fixes only, as per our release policy. Earlier versions are not maintained.
19+
20+
Start integrating with [Cosmos SDK Eden (v0.50)](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.50.5) and enjoy and the new features and performance improvements.

testutil/sims/app_helpers.go

+15
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,21 @@ func SetupAtGenesis(appConfig depinject.Config, extraOutputs ...interface{}) (*r
106106
return SetupWithConfiguration(appConfig, cfg, extraOutputs...)
107107
}
108108

109+
// NextBlock starts a new block.
110+
func NextBlock(app *runtime.App, ctx sdk.Context, jumpTime time.Duration) sdk.Context {
111+
app.EndBlock(abci.RequestEndBlock{Height: ctx.BlockHeight()})
112+
113+
app.Commit()
114+
115+
newBlockTime := ctx.BlockTime().Add(jumpTime)
116+
nextHeight := ctx.BlockHeight() + 1
117+
newHeader := tmproto.Header{Height: nextHeight, Time: newBlockTime}
118+
119+
app.BeginBlock(abci.RequestBeginBlock{Header: newHeader})
120+
121+
return app.NewUncachedContext(false, newHeader)
122+
}
123+
109124
// SetupWithConfiguration initializes a new runtime.App. A Nop logger is set in runtime.App.
110125
// appConfig defines the application configuration (f.e. app_config.go).
111126
// extraOutputs defines the extra outputs to be assigned by the dependency injector (depinject).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package keeper_test
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"cosmossdk.io/depinject"
8+
"cosmossdk.io/log"
9+
"cosmossdk.io/math"
10+
tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
11+
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
12+
banktestutil "github.com/cosmos/cosmos-sdk/x/bank/testutil"
13+
slashingkeeper "github.com/cosmos/cosmos-sdk/x/slashing/keeper"
14+
"github.com/cosmos/cosmos-sdk/x/slashing/testutil"
15+
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
16+
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
17+
18+
distributionkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper"
19+
20+
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
21+
sdk "github.com/cosmos/cosmos-sdk/types"
22+
"github.com/stretchr/testify/require"
23+
24+
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
25+
)
26+
27+
func TestSlashRedelegation(t *testing.T) {
28+
// setting up
29+
var stakingKeeper *stakingkeeper.Keeper
30+
var bankKeeper bankkeeper.Keeper
31+
var slashKeeper slashingkeeper.Keeper
32+
var distrKeeper distributionkeeper.Keeper
33+
34+
app, err := simtestutil.Setup(depinject.Configs(
35+
depinject.Supply(log.NewNopLogger()),
36+
testutil.AppConfig,
37+
), &stakingKeeper, &bankKeeper, &slashKeeper, &distrKeeper)
38+
require.NoError(t, err)
39+
40+
// get sdk context, staking msg server and bond denom
41+
ctx := app.BaseApp.NewContext(false, tmproto.Header{Height: app.LastBlockHeight() + 1})
42+
stakingMsgServer := stakingkeeper.NewMsgServerImpl(stakingKeeper)
43+
bondDenom := stakingKeeper.BondDenom(ctx)
44+
require.NoError(t, err)
45+
46+
// evilVal will be slashed, goodVal won't be slashed
47+
evilValPubKey := secp256k1.GenPrivKey().PubKey()
48+
goodValPubKey := secp256k1.GenPrivKey().PubKey()
49+
50+
// both test acc 1 and 2 delegated to evil val, both acc should be slashed when evil val is slashed
51+
// test acc 1 use the "undelegation after redelegation" trick (redelegate to good val and then undelegate) to avoid slashing
52+
// test acc 2 only undelegate from evil val
53+
testAcc1 := sdk.AccAddress([]byte("addr1_______________"))
54+
testAcc2 := sdk.AccAddress([]byte("addr2_______________"))
55+
56+
// fund acc 1 and acc 2
57+
testCoins := sdk.NewCoins(sdk.NewCoin(bondDenom, stakingKeeper.TokensFromConsensusPower(ctx, 10)))
58+
banktestutil.FundAccount(bankKeeper, ctx, testAcc1, testCoins)
59+
banktestutil.FundAccount(bankKeeper, ctx, testAcc2, testCoins)
60+
61+
balance1Before := bankKeeper.GetBalance(ctx, testAcc1, bondDenom)
62+
balance2Before := bankKeeper.GetBalance(ctx, testAcc2, bondDenom)
63+
64+
// assert acc 1 and acc 2 balance
65+
require.Equal(t, balance1Before.Amount.String(), testCoins[0].Amount.String())
66+
require.Equal(t, balance2Before.Amount.String(), testCoins[0].Amount.String())
67+
68+
// creating evil val
69+
evilValAddr := sdk.ValAddress(evilValPubKey.Address())
70+
banktestutil.FundAccount(bankKeeper, ctx, sdk.AccAddress(evilValAddr), testCoins)
71+
createValMsg1, _ := stakingtypes.NewMsgCreateValidator(
72+
evilValAddr, evilValPubKey, testCoins[0], stakingtypes.Description{Details: "test"}, stakingtypes.NewCommissionRates(math.LegacyNewDecWithPrec(5, 1), math.LegacyNewDecWithPrec(5, 1), math.LegacyNewDec(0)), math.OneInt())
73+
_, err = stakingMsgServer.CreateValidator(ctx, createValMsg1)
74+
require.NoError(t, err)
75+
76+
// creating good val
77+
goodValAddr := sdk.ValAddress(goodValPubKey.Address())
78+
banktestutil.FundAccount(bankKeeper, ctx, sdk.AccAddress(goodValAddr), testCoins)
79+
createValMsg2, _ := stakingtypes.NewMsgCreateValidator(
80+
goodValAddr, goodValPubKey, testCoins[0], stakingtypes.Description{Details: "test"}, stakingtypes.NewCommissionRates(math.LegacyNewDecWithPrec(5, 1), math.LegacyNewDecWithPrec(5, 1), math.LegacyNewDec(0)), math.OneInt())
81+
_, err = stakingMsgServer.CreateValidator(ctx, createValMsg2)
82+
require.NoError(t, err)
83+
84+
// next block, commit height 2, move to height 3
85+
// acc 1 and acc 2 delegate to evil val
86+
ctx = simtestutil.NextBlock(app, ctx, time.Duration(1))
87+
require.NoError(t, err)
88+
89+
// Acc 2 delegate
90+
delMsg := stakingtypes.NewMsgDelegate(testAcc2, evilValAddr, testCoins[0])
91+
_, err = stakingMsgServer.Delegate(ctx, delMsg)
92+
require.NoError(t, err)
93+
94+
// Acc 1 delegate
95+
delMsg = stakingtypes.NewMsgDelegate(testAcc1, evilValAddr, testCoins[0])
96+
_, err = stakingMsgServer.Delegate(ctx, delMsg)
97+
require.NoError(t, err)
98+
99+
// next block, commit height 3, move to height 4
100+
// with the new delegations, evil val increases in voting power and commit byzantine behaviour at height 4 consensus
101+
// at the same time, acc 1 and acc 2 withdraw delegation from evil val
102+
ctx = simtestutil.NextBlock(app, ctx, time.Duration(1))
103+
require.NoError(t, err)
104+
105+
evilVal, found := stakingKeeper.GetValidator(ctx, evilValAddr)
106+
require.True(t, found)
107+
108+
evilPower := stakingKeeper.TokensToConsensusPower(ctx, evilVal.Tokens)
109+
110+
// Acc 1 redelegate from evil val to good val
111+
redelMsg := stakingtypes.NewMsgBeginRedelegate(testAcc1, evilValAddr, goodValAddr, testCoins[0])
112+
_, err = stakingMsgServer.BeginRedelegate(ctx, redelMsg)
113+
require.NoError(t, err)
114+
115+
// Acc 1 undelegate from good val
116+
undelMsg := stakingtypes.NewMsgUndelegate(testAcc1, goodValAddr, testCoins[0])
117+
_, err = stakingMsgServer.Undelegate(ctx, undelMsg)
118+
require.NoError(t, err)
119+
120+
// Acc 2 undelegate from evil val
121+
undelMsg = stakingtypes.NewMsgUndelegate(testAcc2, evilValAddr, testCoins[0])
122+
_, err = stakingMsgServer.Undelegate(ctx, undelMsg)
123+
require.NoError(t, err)
124+
125+
// next block, commit height 4, move to height 5
126+
// Slash evil val for byzantine behaviour at height 4 consensus,
127+
// at which acc 1 and acc 2 still contributed to evil val voting power
128+
// even tho they undelegate at block 4, the valset update is applied after commited block 4 when height 4 consensus already passes
129+
ctx = simtestutil.NextBlock(app, ctx, time.Duration(1))
130+
require.NoError(t, err)
131+
132+
// slash evil val with slash factor = 0.9, leaving only 10% of stake after slashing
133+
evilVal, _ = stakingKeeper.GetValidator(ctx, evilValAddr)
134+
evilValConsAddr, err := evilVal.GetConsAddr()
135+
require.NoError(t, err)
136+
137+
slashKeeper.Slash(ctx, evilValConsAddr, math.LegacyMustNewDecFromStr("0.9"), evilPower, 4)
138+
139+
// assert invariant to make sure we conduct slashing correctly
140+
_, stop := stakingkeeper.AllInvariants(stakingKeeper)(ctx)
141+
require.False(t, stop)
142+
143+
_, stop = bankkeeper.AllInvariants(bankKeeper)(ctx)
144+
require.False(t, stop)
145+
146+
_, stop = distributionkeeper.AllInvariants(distrKeeper)(ctx)
147+
require.False(t, stop)
148+
149+
// one eternity later
150+
ctx = simtestutil.NextBlock(app, ctx, time.Duration(1000000000000000000))
151+
ctx = simtestutil.NextBlock(app, ctx, time.Duration(1))
152+
153+
// confirm that account 1 and account 2 has been slashed, and the slash amount is correct
154+
balance1AfterSlashing := bankKeeper.GetBalance(ctx, testAcc1, bondDenom)
155+
balance2AfterSlashing := bankKeeper.GetBalance(ctx, testAcc2, bondDenom)
156+
157+
require.Equal(t, balance1AfterSlashing.Amount.Mul(math.NewIntFromUint64(10)).String(), balance1Before.Amount.String())
158+
require.Equal(t, balance2AfterSlashing.Amount.Mul(math.NewIntFromUint64(10)).String(), balance2Before.Amount.String())
159+
}

x/staking/keeper/slash.go

+38-1
Original file line numberDiff line numberDiff line change
@@ -260,9 +260,46 @@ func (k Keeper) SlashRedelegation(ctx sdk.Context, srcValidator types.Validator,
260260
slashAmount := slashAmountDec.TruncateInt()
261261
totalSlashAmount = totalSlashAmount.Add(slashAmount)
262262

263+
validatorDstAddress, err := sdk.ValAddressFromBech32(redelegation.ValidatorDstAddress)
264+
if err != nil {
265+
panic(err)
266+
}
267+
// Handle undelegation after redelegation
268+
// Prioritize slashing unbondingDelegation than delegation
269+
unbondingDelegation, found := k.GetUnbondingDelegation(ctx, sdk.MustAccAddressFromBech32(redelegation.DelegatorAddress), validatorDstAddress)
270+
if found {
271+
for i, entry := range unbondingDelegation.Entries {
272+
// slash with the amount of `slashAmount` if possible, else slash all unbonding token
273+
unbondingSlashAmount := math.MinInt(slashAmount, entry.Balance)
274+
275+
switch {
276+
// There's no token to slash
277+
case unbondingSlashAmount.IsZero():
278+
continue
279+
// If unbonding started before this height, stake didn't contribute to infraction
280+
case entry.CreationHeight < infractionHeight:
281+
continue
282+
// Unbonding delegation no longer eligible for slashing, skip it
283+
case entry.IsMature(now) && !entry.OnHold():
284+
continue
285+
// Slash the unbonding delegation
286+
default:
287+
// update remaining slashAmount
288+
slashAmount = slashAmount.Sub(unbondingSlashAmount)
289+
290+
notBondedBurnedAmount = notBondedBurnedAmount.Add(unbondingSlashAmount)
291+
entry.Balance = entry.Balance.Sub(unbondingSlashAmount)
292+
unbondingDelegation.Entries[i] = entry
293+
k.SetUnbondingDelegation(ctx, unbondingDelegation)
294+
}
295+
}
296+
}
297+
298+
// Slash the moved delegation
299+
263300
// Unbond from target validator
264301
sharesToUnbond := slashFactor.Mul(entry.SharesDst)
265-
if sharesToUnbond.IsZero() {
302+
if sharesToUnbond.IsZero() || slashAmount.IsZero() {
266303
continue
267304
}
268305

0 commit comments

Comments
 (0)