-
Notifications
You must be signed in to change notification settings - Fork 124
fix: remove burn from slashing #298
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
base: main
Are you sure you want to change the base?
Changes from all commits
2bf4a8d
a900922
6dd7d04
d5ee1c2
86e8691
eb25b26
8d5bf69
4361e73
2b7ec6a
0ac84c7
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 |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| package keepers | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| "cosmossdk.io/math" | ||
|
|
||
| sdk "github.com/cosmos/cosmos-sdk/types" | ||
| authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" | ||
| stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" | ||
|
|
||
| tokenfactorytypes "github.com/kiichain/kiichain/v7/x/tokenfactory/types" | ||
| ) | ||
|
|
||
| // slashStakingKeeper is the staking-keeper subset needed by SlashHooks | ||
| type slashStakingKeeper interface { | ||
| GetValidator(ctx context.Context, addr sdk.ValAddress) (stakingtypes.Validator, error) | ||
| BondDenom(ctx context.Context) (string, error) | ||
| } | ||
|
|
||
| // slashBankKeeper is the bank-keeper subset needed by SlashHooks | ||
| type slashBankKeeper interface { | ||
| MintCoins(ctx context.Context, moduleName string, amounts sdk.Coins) error | ||
| } | ||
|
|
||
| // slashDistrKeeper is the distribution-keeper subset needed by SlashHooks | ||
| type slashDistrKeeper interface { | ||
| FundCommunityPool(ctx context.Context, amount sdk.Coins, sender sdk.AccAddress) error | ||
| } | ||
|
|
||
| // SlashHooks implements stakingtypes.StakingHooks | ||
| type SlashHooks struct { | ||
| stakingKeeper slashStakingKeeper | ||
| bankKeeper slashBankKeeper | ||
| distrKeeper slashDistrKeeper | ||
| } | ||
|
|
||
| // NewSlashHooks returns a SlashHooks | ||
| func NewSlashHooks(sk slashStakingKeeper, bk slashBankKeeper, dk slashDistrKeeper) SlashHooks { | ||
| return SlashHooks{stakingKeeper: sk, bankKeeper: bk, distrKeeper: dk} | ||
| } | ||
|
|
||
| var _ stakingtypes.StakingHooks = SlashHooks{} | ||
|
|
||
| // BeforeValidatorSlashed mints the equivalent of the slash amount into the | ||
| // community pool before the tokens are burned | ||
| func (h SlashHooks) BeforeValidatorSlashed(ctx context.Context, valAddr sdk.ValAddress, fraction math.LegacyDec) error { | ||
| validator, err := h.stakingKeeper.GetValidator(ctx, valAddr) | ||
| if err != nil || !validator.Tokens.IsPositive() { | ||
| return err | ||
| } | ||
|
|
||
| // The effectiveFraction passed by slashToPool is tokensToBurn / validator.Tokens, | ||
| // so multiplying back recovers tokensToBurn | ||
| mintAmount := math.LegacyNewDecFromInt(validator.Tokens).Mul(fraction).TruncateInt() | ||
| if mintAmount.IsZero() { | ||
| return nil | ||
| } | ||
|
|
||
| bondDenom, err := h.stakingKeeper.BondDenom(ctx) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| coins := sdk.NewCoins(sdk.NewCoin(bondDenom, mintAmount)) | ||
|
|
||
| // Mint into the tokenfactory module account (needs mint permission) | ||
| // and immediately forward to the distribution community pool | ||
| if err := h.bankKeeper.MintCoins(ctx, tokenfactorytypes.ModuleName, coins); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| tokenfactoryAddr := authtypes.NewModuleAddress(tokenfactorytypes.ModuleName) | ||
| return h.distrKeeper.FundCommunityPool(ctx, coins, tokenfactoryAddr) | ||
| } | ||
|
Thaleszh marked this conversation as resolved.
|
||
|
|
||
| // The remaining hooks are not in use; only BeforeValidatorSlashed is used | ||
|
|
||
| func (h SlashHooks) AfterValidatorCreated(_ context.Context, _ sdk.ValAddress) error { | ||
| return nil | ||
| } | ||
|
|
||
| func (h SlashHooks) BeforeValidatorModified(_ context.Context, _ sdk.ValAddress) error { | ||
| return nil | ||
| } | ||
|
|
||
| func (h SlashHooks) AfterValidatorRemoved(_ context.Context, _ sdk.ConsAddress, _ sdk.ValAddress) error { | ||
| return nil | ||
| } | ||
|
|
||
| func (h SlashHooks) AfterValidatorBonded(_ context.Context, _ sdk.ConsAddress, _ sdk.ValAddress) error { | ||
| return nil | ||
| } | ||
|
|
||
| func (h SlashHooks) AfterValidatorBeginUnbonding(_ context.Context, _ sdk.ConsAddress, _ sdk.ValAddress) error { | ||
| return nil | ||
| } | ||
|
|
||
| func (h SlashHooks) BeforeDelegationCreated(_ context.Context, _ sdk.AccAddress, _ sdk.ValAddress) error { | ||
| return nil | ||
| } | ||
|
|
||
| func (h SlashHooks) BeforeDelegationSharesModified(_ context.Context, _ sdk.AccAddress, _ sdk.ValAddress) error { | ||
| return nil | ||
| } | ||
|
|
||
| func (h SlashHooks) BeforeDelegationRemoved(_ context.Context, _ sdk.AccAddress, _ sdk.ValAddress) error { | ||
| return nil | ||
| } | ||
|
|
||
| func (h SlashHooks) AfterDelegationModified(_ context.Context, _ sdk.AccAddress, _ sdk.ValAddress) error { | ||
| return nil | ||
| } | ||
|
|
||
| func (h SlashHooks) AfterUnbondingInitiated(_ context.Context, _ uint64) error { | ||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| package keepers_test | ||
|
|
||
| import ( | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
|
|
||
| tmtypes "github.com/cometbft/cometbft/proto/tendermint/types" | ||
|
|
||
| "cosmossdk.io/math" | ||
|
|
||
| "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" | ||
| sdk "github.com/cosmos/cosmos-sdk/types" | ||
| slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" | ||
| stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" | ||
| stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" | ||
|
|
||
| kiichain "github.com/kiichain/kiichain/v7/app" | ||
| helpers "github.com/kiichain/kiichain/v7/app/helpers" | ||
| ) | ||
|
|
||
| // createBondedValidator funds a new validator address, creates the validator, | ||
| // runs EndBlocker to move it to the bonded set, and registers signing info. | ||
| // Returns the val address and consensus address | ||
| func createBondedValidator(t *testing.T, app *kiichain.KiichainApp, ctx sdk.Context, stakedAmount math.Int) (sdk.ValAddress, sdk.ConsAddress) { | ||
| t.Helper() | ||
|
|
||
| privKey := secp256k1.GenPrivKey() | ||
| pubKey := privKey.PubKey() | ||
| valAddr := sdk.ValAddress(pubKey.Address()) | ||
| consAddr := sdk.ConsAddress(pubKey.Address()) | ||
|
|
||
| bondDenom, err := app.StakingKeeper.BondDenom(ctx) | ||
| require.NoError(t, err) | ||
|
|
||
| selfBond := sdk.NewCoins(sdk.NewCoin(bondDenom, stakedAmount)) | ||
| require.NoError(t, app.BankKeeper.MintCoins(ctx, "tokenfactory", selfBond)) | ||
| require.NoError(t, app.BankKeeper.SendCoinsFromModuleToAccount(ctx, "tokenfactory", sdk.AccAddress(valAddr), selfBond)) | ||
|
|
||
| msgServer := stakingkeeper.NewMsgServerImpl(app.StakingKeeper) | ||
| createMsg, err := stakingtypes.NewMsgCreateValidator( | ||
| valAddr.String(), | ||
| pubKey, | ||
| sdk.NewCoin(bondDenom, stakedAmount), | ||
| stakingtypes.Description{Moniker: "test-validator"}, | ||
| stakingtypes.NewCommissionRates( | ||
| math.LegacyNewDecWithPrec(5, 2), | ||
| math.LegacyNewDecWithPrec(20, 2), | ||
| math.LegacyNewDecWithPrec(1, 2), | ||
| ), | ||
| math.OneInt(), | ||
| ) | ||
| require.NoError(t, err) | ||
| _, err = msgServer.CreateValidator(ctx, createMsg) | ||
| require.NoError(t, err) | ||
|
|
||
| _, err = app.StakingKeeper.EndBlocker(ctx) | ||
| require.NoError(t, err) | ||
|
|
||
| signingInfo := slashingtypes.NewValidatorSigningInfo( | ||
| consAddr, ctx.BlockHeight(), 0, time.Unix(0, 0), false, 0, | ||
| ) | ||
| require.NoError(t, app.SlashingKeeper.SetValidatorSigningInfo(ctx, consAddr, signingInfo)) | ||
|
|
||
| return valAddr, consAddr | ||
| } | ||
|
|
||
| // TestSlashHooks_SupplyUnchangedAfterSlash verifies that when | ||
| // the staking keeper slashes a validator, the total token supply is | ||
| // unchanged | ||
| func TestSlashHooks_SupplyUnchangedAfterSlash(t *testing.T) { | ||
| app := helpers.Setup(t) | ||
| ctx := app.NewUncachedContext(true, tmtypes.Header{ | ||
| Height: 1, | ||
| ChainID: "testing", | ||
| Time: time.Now().UTC(), | ||
| }) | ||
|
|
||
| bondDenom, err := app.StakingKeeper.BondDenom(ctx) | ||
| require.NoError(t, err) | ||
|
|
||
| stakedAmount := sdk.TokensFromConsensusPower(100, sdk.DefaultPowerReduction) | ||
| valAddr, consAddr := createBondedValidator(t, app, ctx, stakedAmount) | ||
|
|
||
| supplyBefore := app.BankKeeper.GetSupply(ctx, bondDenom).Amount | ||
|
|
||
| validator, err := app.StakingKeeper.GetValidator(ctx, valAddr) | ||
| require.NoError(t, err) | ||
| powerReduction := app.StakingKeeper.PowerReduction(ctx) | ||
| consensusPower := validator.GetConsensusPower(powerReduction) | ||
|
|
||
| slashFactor := math.LegacyNewDecWithPrec(5, 2) // 5% | ||
| slashed, err := app.StakingKeeper.Slash(ctx, consAddr, 0, consensusPower, slashFactor) | ||
| require.NoError(t, err) | ||
| require.True(t, slashed.IsPositive(), "expected a non-zero slash amount") | ||
|
|
||
| // Supply must be unchanged: the burn is offset by the hook's mint | ||
| require.Equal(t, supplyBefore, app.BankKeeper.GetSupply(ctx, bondDenom).Amount, | ||
| "total supply must be unchanged after slash") | ||
|
|
||
| // Community pool must have received exactly the slashed amount | ||
| feePool, err := app.DistrKeeper.FeePool.Get(ctx) | ||
| require.NoError(t, err) | ||
| communityPool := feePool.CommunityPool.AmountOf(bondDenom) | ||
| require.Equal(t, math.LegacyNewDecFromInt(slashed), communityPool, | ||
| "community pool must receive exactly the slash amount") | ||
| } | ||
|
|
||
| // TestSlashHooks_ZeroSlashFactor verifies no tokens move and supply is | ||
| // unchanged when the slash factor is zero | ||
| func TestSlashHooks_ZeroSlashFactor(t *testing.T) { | ||
| app := helpers.Setup(t) | ||
| ctx := app.NewUncachedContext(true, tmtypes.Header{ | ||
| Height: 1, | ||
| ChainID: "testing", | ||
| Time: time.Now().UTC(), | ||
| }) | ||
|
|
||
| bondDenom, err := app.StakingKeeper.BondDenom(ctx) | ||
| require.NoError(t, err) | ||
|
|
||
| stakedAmount := sdk.TokensFromConsensusPower(100, sdk.DefaultPowerReduction) | ||
| _, consAddr := createBondedValidator(t, app, ctx, stakedAmount) | ||
|
|
||
| supplyBefore := app.BankKeeper.GetSupply(ctx, bondDenom).Amount | ||
|
|
||
| validator, err := app.StakingKeeper.GetValidatorByConsAddr(ctx, consAddr) | ||
| require.NoError(t, err) | ||
| powerReduction := app.StakingKeeper.PowerReduction(ctx) | ||
| consensusPower := validator.GetConsensusPower(powerReduction) | ||
|
|
||
| slashed, err := app.StakingKeeper.Slash(ctx, consAddr, 0, consensusPower, math.LegacyZeroDec()) | ||
| require.NoError(t, err) | ||
| require.True(t, slashed.IsZero(), "expected zero slash for zero factor") | ||
|
|
||
| require.Equal(t, supplyBefore, app.BankKeeper.GetSupply(ctx, bondDenom).Amount, | ||
| "supply must be unchanged when slash factor is zero") | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ import ( | |
| "cosmossdk.io/math" | ||
|
|
||
| sdk "github.com/cosmos/cosmos-sdk/types" | ||
| authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" | ||
| stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" | ||
|
|
||
| "github.com/kiichain/kiichain/v7/x/oracle/types" | ||
|
|
@@ -15,9 +16,6 @@ import ( | |
| // SlashAndResetCounters calculate if the validator must be slashed if success votes / total votes | ||
| // is lower than MinValidPerWindow param. Then reset the vote penalty info | ||
| func (k Keeper) SlashAndResetCounters(ctx sdk.Context) error { | ||
| height := ctx.BlockHeight() | ||
| distributionHeight := height - sdk.ValidatorUpdateDelay - 1 | ||
|
|
||
| // Get the module params | ||
| params, err := k.Params.Get(ctx) | ||
| if err != nil { | ||
|
|
@@ -69,8 +67,8 @@ func (k Keeper) SlashAndResetCounters(ctx sdk.Context) error { | |
| // Calculate consensus power | ||
| consensusPower := validator.GetConsensusPower(powerReduction) | ||
|
|
||
| // Slash the validator | ||
| _, err = k.StakingKeeper.Slash(ctx, consAddr, distributionHeight, consensusPower, slashFraction) | ||
| // Redirect the slash amount to the community pool instead of burning | ||
| _, err = k.slashToPool(ctx, consAddr, consensusPower, slashFraction) | ||
| if err != nil { | ||
| k.Logger(ctx).Error("failed to slash validator", "operator", operator.String(), "error", err) | ||
| return true, err | ||
|
|
@@ -103,3 +101,54 @@ func (k Keeper) SlashAndResetCounters(ctx sdk.Context) error { | |
| }) | ||
| return err | ||
| } | ||
|
|
||
| // slashToPool removes tokens from a validator and sends them to the community pool | ||
| // instead of burning, preserving total supply. | ||
| func (k Keeper) slashToPool(ctx sdk.Context, consAddr sdk.ConsAddress, consensusPower int64, slashFactor math.LegacyDec) (math.Int, error) { | ||
| if slashFactor.IsZero() { | ||
| return math.ZeroInt(), nil | ||
| } | ||
|
|
||
| // Calculate slash amount: same math as the staking module | ||
| amount := k.StakingKeeper.TokensFromConsensusPower(ctx, consensusPower) | ||
| slashAmount := math.LegacyNewDecFromInt(amount).Mul(slashFactor).TruncateInt() | ||
|
|
||
| // Get the concrete validator (needed for RemoveValidatorTokens) | ||
| validator, err := k.StakingKeeper.GetValidatorByConsAddr(ctx, consAddr) | ||
| if err != nil { | ||
| return math.ZeroInt(), err | ||
| } | ||
|
|
||
| // Clamp slash amount to the validator's available tokens | ||
| tokensToBurn := math.MinInt(slashAmount, validator.Tokens) | ||
| if tokensToBurn.IsZero() { | ||
| return math.ZeroInt(), nil | ||
| } | ||
|
|
||
| // Remove tokens from the validator's bookkeeping without burning | ||
| _, err = k.StakingKeeper.RemoveValidatorTokens(ctx, validator, tokensToBurn) | ||
| if err != nil { | ||
| return math.ZeroInt(), err | ||
| } | ||
|
|
||
| // Create the coin (value but with akii) | ||
| bondDenom, err := k.StakingKeeper.BondDenom(ctx) | ||
| if err != nil { | ||
| return math.ZeroInt(), err | ||
| } | ||
| coins := sdk.NewCoins(sdk.NewCoin(bondDenom, tokensToBurn)) | ||
| // Send the slashed tokens from the bonded pool to the community pool | ||
| bondedPoolAddr := authtypes.NewModuleAddress(stakingtypes.BondedPoolName) | ||
| if err := k.distrKeeper.FundCommunityPool(ctx, coins, bondedPoolAddr); err != nil { | ||
| return math.ZeroInt(), err | ||
| } | ||
|
|
||
| k.Logger(ctx).Info( | ||
| "oracle slash redirected to community pool", | ||
| "validator", consAddr.String(), | ||
| "slash_factor", slashFactor.String(), | ||
| "amount", tokensToBurn.String(), | ||
| ) | ||
|
|
||
| return tokensToBurn, nil | ||
|
Comment on lines
+107
to
+153
Contributor
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. 🧩 Analysis chain🏁 Script executed: # First, find the oracle keeper slash file
find . -name "slash.go" -path "*/oracle/keeper/*" 2>/dev/nullRepository: KiiChain/kiichain Length of output: 87 🏁 Script executed: # Check the file size before reading
wc -l x/oracle/keeper/slash.go 2>/dev/null || find . -name "slash.go" -path "*/oracle*" -exec wc -l {} \;Repository: KiiChain/kiichain Length of output: 89 🏁 Script executed: # Search for where slashToPool or similar functions are used
rg "slashToPool|Slash" --type go -l | head -20Repository: KiiChain/kiichain Length of output: 573 🏁 Script executed: # Look for BeforeValidatorSlashed usage
rg "BeforeValidatorSlashed" --type go -A 2 -B 2Repository: KiiChain/kiichain Length of output: 43 🏁 Script executed: # Search for RemoveValidatorTokens usage to understand the pattern
rg "RemoveValidatorTokens" --type go -lRepository: KiiChain/kiichain Length of output: 120 🏁 Script executed: # Read the complete slash.go file
cat -n x/oracle/keeper/slash.goRepository: KiiChain/kiichain Length of output: 6558 🏁 Script executed: # Search for where slash functions are called to understand the flow
rg "slash\(" --type go x/oracle -A 3 -B 3Repository: KiiChain/kiichain Length of output: 43 🏁 Script executed: # Check the staking keeper interface to understand what Slash does
rg "interface.*StakingKeeper|Slash.*func" --type go -A 5 | head -100Repository: KiiChain/kiichain Length of output: 2081 🏁 Script executed: # Look at the expected_keepers.go to see what methods are expected
cat x/oracle/types/expected_keepers.goRepository: KiiChain/kiichain Length of output: 3866 🏁 Script executed: # Search for any usage of the actual Slash method from staking keeper
rg "\.Slash\(" --type go -B 3 -A 3Repository: KiiChain/kiichain Length of output: 43 🏁 Script executed: # Look for BeforeValidatorSlashed hook registration
rg "BeforeValidatorSlashed|Hooks" --type go x/oracle -A 2 -B 2Repository: KiiChain/kiichain Length of output: 429 🏁 Script executed: # Check if there are any tests for slashToPool to understand the expected behavior
cat -n x/oracle/keeper/slash_test.go 2>/dev/null | head -100Repository: KiiChain/kiichain Length of output: 4254 🏁 Script executed: # Look for staking keeper imports to understand what methods are available
rg "staking" --type go x/oracle/keeper -i -lRepository: KiiChain/kiichain Length of output: 298 🏁 Script executed: # Search for any references to unbonding or redelegation handling
rg "unbonding|Unbonding|redelegation|Redelegation" --type go x/oracleRepository: KiiChain/kiichain Length of output: 43 🏁 Script executed: # Look at the full slash_test.go to understand what's being tested
cat -n x/oracle/keeper/slash_test.goRepository: KiiChain/kiichain Length of output: 25621 🏁 Script executed: # Search for any slash event emission or distribution hooks in oracle keeper
rg "slash|event|Event" --type go x/oracle/keeper/slash.go -iRepository: KiiChain/kiichain Length of output: 1716 🏁 Script executed: # Check if there's any direct usage of staking.Keeper.Slash elsewhere in the codebase
rg "staking\..*Slash|keeper\.Slash" --type go -iRepository: KiiChain/kiichain Length of output: 1337 🏁 Script executed: # Look at the Cosmos SDK documentation by checking what staking.Keeper.Slash method signature should be
# Check if there are any imports or references that show what Slash should do
rg "import.*staking" --type go x/oracleRepository: KiiChain/kiichain Length of output: 43 🏁 Script executed: # Check if there's any infraction height tracking or unbonding handling
rg "unbonding|InfractionHeight|Fraction" --type go x/oracle/keeperRepository: KiiChain/kiichain Length of output: 1572 🏁 Script executed: # Look for distribution slash event emission elsewhere
rg "EventTypeSlash|slash.*event|Event.*Slash" --type go -iRepository: KiiChain/kiichain Length of output: 320 🏁 Script executed: # Check the app keepers configuration to see how distribution hooks are set up
cat -n app/keepers/keepers.go | head -300Repository: KiiChain/kiichain Length of output: 13984 🏁 Script executed: # Check if there are any other slash implementations or references in the staking keeper interface
rg "Slash\(" --type go -B 2 -A 5 | head -80Repository: KiiChain/kiichain Length of output: 825 🏁 Script executed: # Look for any hook registration related to oracle or validation
rg "SetHooks|Hooks\(\)|BeforeValidator" --type go app/keepers/keepers.go -iRepository: KiiChain/kiichain Length of output: 255 🏁 Script executed: # Search for any distribution slash event handling
rg "ValidatorSlashEvent|SlashEvent" --type goRepository: KiiChain/kiichain Length of output: 126 🏁 Script executed: # Search for any actual usage of StakingKeeper.Slash method in the codebase
rg "StakingKeeper\.Slash|\.Slash\(" --type go -B 3 -A 5Repository: KiiChain/kiichain Length of output: 791 🏁 Script executed: # Check the Cosmos SDK version to understand what Slash method does
rg "cosmos-sdk" go.mod | head -5Repository: KiiChain/kiichain Length of output: 239 🏁 Script executed: # Look at the test helper to see the context of distribution hooks
cat -n x/oracle/keeper/test_utils.go | head -100Repository: KiiChain/kiichain Length of output: 4397 🏁 Script executed: # Look for the Cosmos SDK Slash method signature in go.mod dependencies
grep -A 5 "cosmos-sdk" go.modRepository: KiiChain/kiichain Length of output: 876 🏁 Script executed: # Check if there's any BeforeValidatorSlashed hook in the distribution keeper
rg "BeforeValidatorSlashed" --type go -B 3 -A 3Repository: KiiChain/kiichain Length of output: 43 🏁 Script executed: # Look for any unbonding delegation concepts in oracle
rg "unbond|Unbond" --type go x/oracleRepository: KiiChain/kiichain Length of output: 219 🏁 Script executed: # Examine the test that explicitly mentions previous behavior used StakingKeeper.Slash
sed -n '242,250p' x/oracle/keeper/slash_test.goRepository: KiiChain/kiichain Length of output: 549 🌐 Web query:
💡 Result: The Cosmos SDK Staking Keeper's Slash method is used by the Slashing module to penalize validators for infractions like downtime or double-signing. It is called from the slashing keeper's Slash or SlashWithInfractionReason methods. Signature (from pkg.go.dev docs and source references): func (k Keeper) Slash(ctx context.Context, consAddr sdk.ConsAddress, fraction sdkmath.LegacyDec, power int64, ...) error More precisely, the staking Slash method takes parameters including: - validator consensus address - slash fraction (e.g., SlashFractionDowntime or SlashFractionDoubleSign) - validator power at time of infraction - infraction height (int64 block height where infraction occurred) The infraction height is critical for handling unbonding delegations. When slashing: 1. Slash bonded tokens from the validator's pool (BondedPool or NotBondedPool). 2. Iterate over all unbonding delegations (UBDs) from the validator. For each UBD entry: - If the unbonding completion time is after the infraction height + unbonding period (i.e., UBD started after infraction), slash proportionally from InitialBalance. - Mature/completed UBDs (IsMature && !OnHold) are skipped. 3. Similarly slash ongoing redelegations (pseudo-unbonding) that started after the infraction. 4. Burn slashed tokens, reducing total supply. This ensures only stake "at risk" at infraction time is slashed, using historical state via infraction height to filter UBDs/redelegs. Examples: - Liveness slash: distributionHeight = ctx.BlockHeight - sdk.ValidatorUpdateDelay - 1 - Evidence-based (double-sign): exact infraction height from evidence. Source: Official Cosmos SDK GitHub (x/staking/keeper/slash.go), docs.cosmos.network (staking/slashing modules), pkg.go.dev/cosmossdk.io/x/staking, interchain-security ADR on equivocation slashing logic. Citations:
Use the full staking.Keeper.Slash semantics, only swapping the burn destination to preserve supply. The helper at lines 107–153 sidesteps the SDK's Slash path in favor of a simplified token removal and community pool redirection. While supply preservation is valuable, this loses critical semantic guarantees:
Instead of reimplementing this logic, call 🤖 Prompt for AI Agents
Contributor
Author
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. Check point 1 of this, it might be wroth
Contributor
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. 🧩 Analysis chainLet me check the original implementation to understand what was previously slashed (bonded tokens only vs. UBDs/redelegations) and how significant point 1 is. 🏁 Script executed: Length of output: 5860 🏁 Script executed: Length of output: 5872
What the original code did: distributionHeight := height - sdk.ValidatorUpdateDelay - 1
k.StakingKeeper.Slash(ctx, consAddr, distributionHeight, consensusPower, slashFraction)The SDK's What the new code does: The exploit path:
Practical severity:
Suggested fix approach: The cleanest fix without fully reimplementing // After RemoveValidatorTokens, also slash UBDs started after distributionHeight
distributionHeight := sdk.Context.BlockHeight() - sdk.ValidatorUpdateDelay - 1
// Iterate unbonding delegations from this validator
unbondingDels, err := k.StakingKeeper.GetUnbondingDelegationsFromValidator(ctx, valAddr)
for _, ubd := range unbondingDels {
for i, entry := range ubd.Entries {
if entry.IsMature(ctx.BlockTime()) {
continue // skip already matured entries
}
if entry.CreationHeight < distributionHeight {
continue // only slash entries started after infraction height
}
slashAmountUBD := slashFactor.MulInt(entry.InitialBalance).TruncateInt()
slashAmountUBD = math.MinInt(slashAmountUBD, entry.Balance)
ubd.Entries[i].Balance = entry.Balance.Sub(slashAmountUBD)
totalSlashed = totalSlashed.Add(slashAmountUBD)
// ... accumulate coins and send to community pool
}
}Bottom line: If your oracle slash fraction is low and gaming it requires deliberate and observable action by validators, you could defer this. But if you want full parity with original semantics (which protected against this escape hatch), it's worth adding UBD slashing with the funds redirected to the community pool instead of being burned. Would you like me to open a follow-up issue to track this? |
||
| } | ||
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.
🧩 Analysis chain
🌐 Web query:
In cosmos-sdk v0.53.6, what exact value doesx/staking/keeper/slash.gopass as thefractionargument toStakingHooks.BeforeValidatorSlashed? Please cite the implementation and clarify whether it is the raw slash factor or an effective post-clamp fraction.💡 Result:
In cosmos-sdk v0.53.6, the x/staking/keeper/slash.go file passes the raw slash factor (the unclamped fraction parameter received by the Slash function) as the fraction argument to StakingHooks.BeforeValidatorSlashed. This occurs before any clamping of the slash amount to ensure the minimum balance is maintained. The hook receives the intended slash fraction prior to applying clamps in the staking keeper's Slash logic.
Citations:
🏁 Script executed:
Repository: KiiChain/kiichain
Length of output: 1885
🏁 Script executed:
Repository: KiiChain/kiichain
Length of output: 1885
🏁 Script executed:
Repository: KiiChain/kiichain
Length of output: 1885
Reconstruct the exact slash amount instead of deriving it from the current validator tokens.
The
fractionparameter is the raw slash factor (before clamping). Multiplying it againstvalidator.Tokensand converting through decimals introduces rounding loss that can under-mint, and delayed evidence execution where validator tokens differ from the infraction-power basis will further drift from the actual amount slashed. Pass or compute the exacttokensToBurnvalue from the slash execution path instead.🤖 Prompt for AI Agents