Skip to content

feat(action,staking): IIP-59 SetCommissionRate action + handler (PR 3/5)#4862

Closed
envestcc wants to merge 2 commits into
iotexproject:masterfrom
envestcc:iip-59/pr3-set-commission-rate
Closed

feat(action,staking): IIP-59 SetCommissionRate action + handler (PR 3/5)#4862
envestcc wants to merge 2 commits into
iotexproject:masterfrom
envestcc:iip-59/pr3-set-commission-rate

Conversation

@envestcc

Copy link
Copy Markdown
Member

Summary

PR 3 of the IIP-59 series. Introduces the on-chain entry point delegates use to opt into protocol-native voter reward distribution: SetCommissionRate(uint64 rate). No reward-distribution logic yet — that arrives in PR 4. This PR is independently reviewable: the action is gated at Validate, so until the IIP-59 feature flag activates the protocol rejects every SetCommissionRate at mempool admission and chain behavior is unchanged.

Series

# PR Status
0 iotex-proto: SetCommissionRate + Candidate.commissionRate iotexproject/iotex-proto#174 (draft)
1 iotex-core: fields + feature flag #4859 (draft, prerequisite for this PR)
2 iotex-core: VoterWeightView infrastructure #4860 (draft, independent of this PR)
3 This PR — SetCommissionRate action + handler here, depends on #4859
4 distributeVoterReward + PutPollResult snapshot TODO (depends on this PR + #4860 + PR 2.5)
5 multi-node stress test + e2e harness TODO

What's in this PR

Action (action/set_commission_rate.go):

  • SetCommissionRate{ rate uint64 } with IntrinsicGas (10000) and SanityCheck (rate in [0, MaxCommissionRate=10000]).
  • Full Proto/LoadProto/FillAction wiring for the iotex-proto field 56 oneof slot.
  • EthCompatibleAction implementation + NewSetCommissionRateFromABIBinary decoder so MetaMask/hardhat tooling can submit via the same ETH-tx path as candidateActivate / candidateDeactivate.
  • PackCommissionRateSetEvent helper for the receipt log indexers will subscribe to.

ABI surface (action/native_staking_contract_interface.sol + .json):

event CommissionRateSet(address indexed candidate, uint64 newRate);

function setCommissionRate(uint64 rate) external;

Hand-added to both the source .sol and the parsed .json (the repo doesn't auto-regen).

Envelope (action/envelope.go):

  • Route ActionCore.setCommissionRate (field 56) into the existing unmarshal dispatch.

Handler (action/protocol/staking/handler_set_commission_rate.go):

  • handleSetCommissionRate: owner-check via csm.GetByOwner; write the new rate to staking.Candidate.CommissionRate via Upsert; emit the CommissionRateSet event through receiptLog.AddEvent (the events-path, per the staking CLAUDE.md red-line — never via legacy r.topics / r.data).
  • Non-owner caller returns errCandNotExist (the existing handleError), so the receipt is marked failed but block production continues.
  • No cooldown check: IIP-59 doesn't prescribe one, and our design's ~1.5-epoch reaction window comes for free via the PutPollResult snapshot mechanism (this point was already settled in PR feat(staking,state): IIP-59 commissionRate fields + feature flag (PR 1/5) #4859 review with envestcc).

Validate (action/protocol/staking/validations.go):

  • validateSetCommissionRate: hard-reject when NoVoterRewardDistribution is true (pre-fork mempool admission gate); otherwise delegate to act.SanityCheck for the rate-range check. Returns action.ErrInvalidAct to match the surrounding validateMigrateStake / validateCandidate* convention.

Protocol wiring (action/protocol/staking/protocol.go):

  • Register the new action in both the handle and Validate switches.

Test plan

  • go build ./... passes
  • go test ./action/... -count=1 — 14 new tests added, all pass
    • action/set_commission_rate_test.go (7): SanityCheck boundaries, IntrinsicGas, proto roundtrip, ABI roundtrip, FromABIBinary garbage-rejection, event packing.
    • action/protocol/staking/handler_set_commission_rate_test.go (7): validate pre-fork reject, post-fork accept, rate-over-max reject; handler non-owner errCandNotExist, owner persists, owner re-update; full p.Handle envelope success; pre-fork p.Validate rejection.
  • Full action-package sweep passes (modulo the pre-existing flaky TestProtocol_FetchBucketAndValidate bug: TestProtocol_FetchBucketAndValidate flaky in staking test suite #4813 unrelated to this PR).

Behavior gating

  • Pre-flag (default): NoVoterRewardDistribution = true. Validate rejects every SetCommissionRate with ErrInvalidAct. The action never reaches the handler; chain behavior identical to today.
  • Post-flag: rate writes land on staking.Candidate.CommissionRate. The persisted rate stays inert until PR 4 wires PutPollResult to snapshot it into the per-epoch state.Candidate and GrantEpochReward to consume it.

🤖 Generated with Claude Code

envestcc and others added 2 commits June 25, 2026 11:42
Adds the proto-level field and feature gate that the rest of the IIP-59
protocol-native voter reward distribution PRs will hang behavior off of.
No runtime behavior changes in this PR — the field is populated as zero
on existing chain data, default behavior matches today exactly.

Schema:
- stakingpb.Candidate gains commissionRate=11; Go Candidate struct mirrors
  it. Equal/Clone/toProto/fromProto updated (the original PoC missed Equal
  — flagged in iotexproject#4811 review #2).
- state.Candidate gains CommissionRate, snapshotted per epoch from the
  staking candidate state by PutPollResult (added in PR 4). The latest
  user-set value lives on staking.Candidate; state.Candidate holds the
  per-epoch frozen value consumed by GrantEpochReward.
- iotextypes.Candidate.commissionRate is set/read in candidateToPb /
  pbToCandidate so the new field travels through poll snapshots and over
  the wire.

Feature flag:
- FeatureCtx.NoVoterRewardDistribution, bound to !g.IsToBeEnabled(height)
  per AGENTS.md convention for WIP features (the gate will be swapped for
  a real hardfork height at release time).
- Named so that the bool zero value (false) corresponds to the post-fork
  activated behavior, matching the existing NoCandidateExitQueue /
  NotSlashUnproductiveDelegates convention. A docstring records this rule
  next to the field for future readers.

Why no separate commissionRateLastEpoch field:
- IIP-59 doesn't prescribe a per-rate-change cooldown. Its protection
  against rapid manipulation is epoch-boundary activation, which our
  design already provides for free: a SetCommissionRate at any moment
  only affects rewards in the epoch *after* the next PutPollResult
  snapshot, giving voters ~1.5 epochs of reaction time.
- If the protocol later decides cooldown is needed, adding the field is
  an additive proto change with no migration cost.

Toolchain:
- stakingpb regenerated with protoc-gen-go v1.26.0 to match the version
  recorded in the existing staking.pb.go header.

Local dev:
- go.mod replace pointing at ../iotex-proto so the build resolves the new
  iotex-proto fields prior to the proto PR being tagged. Remove the
  replace once iotex-proto cuts a release containing the
  SetCommissionRate + Candidate.commissionRate additions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces the on-chain entry point delegates use to opt into IIP-59
voter reward distribution. No reward-distribution logic yet — that arrives
in PR 4. This PR is independently reviewable: the action is gated at
Validate, so until the IIP-59 feature flag activates the protocol rejects
every SetCommissionRate at mempool admission and chain behavior is
unchanged.

action/set_commission_rate.go
- SetCommissionRate{ rate uint64 } with IntrinsicGas (10000) and
  SanityCheck (rate must be in [0, 10000]).
- Full Proto / LoadProto / FillAction wiring for the iotex-proto field 56
  oneof slot that ships with the parallel PR
  (iotexproject/iotex-proto#174).
- EthCompatibleAction implementation + NewSetCommissionRateFromABIBinary
  decoder so MetaMask / hardhat tooling can submit the action via the same
  path as candidateActivate / candidateDeactivate.
- PackCommissionRateSetEvent helper that produces the keccak-anchored
  topic-0 + indexed candidate address for the receipt log indexers will
  subscribe to.

action/native_staking_contract_interface.sol +
action/native_staking_contract_abi.json
- Declare `function setCommissionRate(uint64 rate)` and
  `event CommissionRateSet(address indexed candidate, uint64 newRate)`,
  matching the existing native_staking ABI shape. The JSON entries are
  hand-added (the repo doesn't auto-regen from the .sol).

action/envelope.go
- Route `ActionCore.setCommissionRate` (field 56) into the existing
  unmarshal dispatch.

action/protocol/staking/handler_set_commission_rate.go
- handleSetCommissionRate: owner-check via csm.GetByOwner; write the new
  rate to staking.Candidate.CommissionRate via Upsert; emit the
  CommissionRateSet event through receiptLog.AddEvent (the events-path,
  per CLAUDE.md — never via legacy r.topics / r.data).
- Non-owner caller returns errCandNotExist (the existing handleError),
  so the receipt is marked failed but block production continues.
- No cooldown check: IIP-59 doesn't prescribe one, and our design's
  ~1.5-epoch reaction window comes for free via the PutPollResult
  snapshot mechanism (see PR 1 commit message).

action/protocol/staking/validations.go
- validateSetCommissionRate: hard-reject when NoVoterRewardDistribution
  is true (pre-fork mempool admission gate); otherwise delegate to
  act.SanityCheck for the rate-range check. Reuses action.ErrInvalidAct
  to match the surrounding validateMigrateStake / validateCandidate*
  conventions.

action/protocol/staking/protocol.go
- Register the new action in both the handle and Validate switches.

Tests:
- action/set_commission_rate_test.go (7): SanityCheck boundaries, gas,
  proto roundtrip, ABI roundtrip, FromABIBinary garbage-rejection, event
  packing.
- action/protocol/staking/handler_set_commission_rate_test.go (7):
  validate pre-fork reject, post-fork accept, rate-over-max reject;
  handler non-owner errCandNotExist, owner persists, owner re-update;
  full p.Handle envelope success; pre-fork p.Validate rejection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@envestcc

envestcc commented Jul 1, 2026

Copy link
Copy Markdown
Member Author

Closing as part of IIP-59 PR reorganization. The proposal was amended (iotexproject/iips#73) to cover both block reward + epoch reward and to base voter distribution on a per-epoch snapshot. The new PR split is:

The incremental voter weight view (old #4860 + #4863) is dropped in favor of a per-PutPollResult full scan; ~40k buckets sorted+encoded once per epoch fits well inside the mint budget and eliminates the 13-handler-hook consensus-safety surface. Old #4864 (voter reward distribution) is replaced by new PR 3 which reads the snapshot instead of the live view.

Superseded by new PRs — will link once opened.

@envestcc envestcc closed this Jul 1, 2026
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