Skip to content

fix: poa unbond all validator delegations on removal#141

Draft
kpitapeersyst wants to merge 1 commit into
mainfrom
fix/poa-remove-validator-state
Draft

fix: poa unbond all validator delegations on removal#141
kpitapeersyst wants to merge 1 commit into
mainfrom
fix/poa-remove-validator-state

Conversation

@kpitapeersyst
Copy link
Copy Markdown
Contributor

@kpitapeersyst kpitapeersyst commented May 5, 2026

fix: poa unbond all validator delegations on removal

Motivation 💡

  • Removing a POA validator only unbonded the validator self-delegation. If other delegators had shares on the validator, their delegation records were not cleaned up during validator removal.

Changes 🛠

  • Fetch validator delegations before mutating validator staking state.
  • Validate and store delegator addresses before token removal/burn.
  • Unbond every delegation using its original shares so staking removes delegation records through normal hooks/indexes.
  • Add GetValidatorDelegations to the expected staking keeper interface and generated mock.
  • Expand ExecuteRemoveValidator unit tests for self-delegation, foreign delegators, invalid delegator addresses, and unbond failures.

Summary by CodeRabbit

  • Bug Fixes

    • Validator removal now correctly unbonds stakes for all delegators associated with a validator, not just the validator's own stake.
  • Tests

    • Updated and added tests covering delegation-driven unbonding, invalid delegator addresses, error handling, and multi-delegator scenarios.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

📝 Walkthrough

Walkthrough

The POA keeper's ExecuteRemoveValidator now fetches all delegations for a validator, decodes delegator Bech32 addresses, and unbonds each delegation individually. A StakingKeeper interface method GetValidatorDelegations was added and mocks/tests updated to exercise delegation-driven unbonding.

Changes

Delegation-Based Unbonding Refactor

Layer / File(s) Summary
Interface Contract
x/poa/types/expected_keepers.go
Adds StakingKeeper.GetValidatorDelegations(ctx context.Context, valAddr sdk.ValAddress) ([]stakingtypes.Delegation, error).
Mock Support
x/poa/testutil/expected_keepers_mock.go
Adds GoMock method/recorder for GetValidatorDelegations on MockStakingKeeper.
Core Implementation
x/poa/keeper/keeper.go
ExecuteRemoveValidator calls k.sk.GetValidatorDelegations, converts each Delegation.DelegatorAddress to sdk.AccAddress (wrapping decode errors), and loops to call k.sk.Unbond(ctx, delegatorAddr, valAddr, del.Shares) for each delegation instead of unbonding only self-delegation.
Test Coverage
x/poa/keeper/keeper_test.go
Adds mustAccAddress helper, wires GetValidatorDelegations into default mock setup, and adds/updates tests covering: GetValidatorDelegations error, invalid delegator Bech32, zero delegations, self-only delegation, and self+foreign delegations; updates expectations for RemoveValidatorTokens and Unbond calls.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hop and fetch each delegator's name,
Decode their addresses, call out each claim,
One by one their shares are gently freed,
A keeper's careful work — precise and keyed. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: unbonding all validator delegations (not just self-delegation) when removing a POA validator.
Description check ✅ Passed The description provides clear motivation, detailed changes, and addresses all required template sections with specific implementation details.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/poa-remove-validator-state

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.1)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@kpitapeersyst kpitapeersyst force-pushed the fix/poa-remove-validator-state branch from 6143a41 to 7f71d01 Compare May 5, 2026 07:04
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
x/poa/keeper/keeper.go (1)

257-264: 💤 Low value

Optional: consolidate the parallel delegations / delegatorAddrs slices.

Index-aligned parallel slices are easy to break in future refactors. A small local struct (or storing the pair) makes the relationship explicit without changing behavior.

♻️ Proposed refactor
-	delegatorAddrs := make([]sdk.AccAddress, 0, len(delegations))
-	for _, del := range delegations {
-		delAddr, err := sdk.AccAddressFromBech32(del.DelegatorAddress)
-		if err != nil {
-			return errors.Wrapf(err, "invalid delegator address %q for validator %q", del.DelegatorAddress, validatorAddress)
-		}
-		delegatorAddrs = append(delegatorAddrs, delAddr)
-	}
+	type pendingUnbond struct {
+		delAddr sdk.AccAddress
+		shares  math.LegacyDec
+		bech32  string
+	}
+	pending := make([]pendingUnbond, 0, len(delegations))
+	for _, del := range delegations {
+		delAddr, err := sdk.AccAddressFromBech32(del.DelegatorAddress)
+		if err != nil {
+			return errors.Wrapf(err, "invalid delegator address %q for validator %q", del.DelegatorAddress, validatorAddress)
+		}
+		pending = append(pending, pendingUnbond{delAddr: delAddr, shares: del.Shares, bech32: del.DelegatorAddress})
+	}
@@
-	for i, del := range delegations {
-		if _, err := k.sk.Unbond(ctx, delegatorAddrs[i], valAddress, del.Shares); err != nil {
-			return errors.Wrapf(err, "failed to unbond delegation from %q to validator %q", del.DelegatorAddress, validatorAddress)
+	for _, p := range pending {
+		if _, err := k.sk.Unbond(ctx, p.delAddr, valAddress, p.shares); err != nil {
+			return errors.Wrapf(err, "failed to unbond delegation from %q to validator %q", p.bech32, validatorAddress)
		}
	}

Also applies to: 288-295

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@x/poa/keeper/keeper.go` around lines 257 - 264, The code builds index-aligned
parallel slices delegations and delegatorAddrs which is error-prone; refactor to
a single local struct slice (e.g., type delegatorPair struct { delegation
stakingtypes.Delegation; addr sdk.AccAddress }) and populate that in the loop
that currently calls sdk.AccAddressFromBech32 for each del and appends to
delegatorAddrs (refer to variables delegations, delegatorAddrs,
sdk.AccAddressFromBech32 and validatorAddress); update subsequent usage sites
(the block around lines 288-295 mentioned in the comment) to iterate the new
slice of pairs instead of relying on two parallel slices, preserving the
existing error handling and return behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@x/poa/keeper/keeper.go`:
- Around line 257-264: The code builds index-aligned parallel slices delegations
and delegatorAddrs which is error-prone; refactor to a single local struct slice
(e.g., type delegatorPair struct { delegation stakingtypes.Delegation; addr
sdk.AccAddress }) and populate that in the loop that currently calls
sdk.AccAddressFromBech32 for each del and appends to delegatorAddrs (refer to
variables delegations, delegatorAddrs, sdk.AccAddressFromBech32 and
validatorAddress); update subsequent usage sites (the block around lines 288-295
mentioned in the comment) to iterate the new slice of pairs instead of relying
on two parallel slices, preserving the existing error handling and return
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 54f19a55-80f7-4dd6-92e9-f9d36fd0cac3

📥 Commits

Reviewing files that changed from the base of the PR and between 6143a41 and 7f71d01.

📒 Files selected for processing (4)
  • x/poa/keeper/keeper.go
  • x/poa/keeper/keeper_test.go
  • x/poa/testutil/expected_keepers_mock.go
  • x/poa/types/expected_keepers.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • x/poa/testutil/expected_keepers_mock.go

@kpitapeersyst kpitapeersyst marked this pull request as draft May 19, 2026 07:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant