IIP-59: contract-backed rates, native compound, batched log, opt-in migration - #74
Merged
envestcc merged 5 commits intoJul 16, 2026
Conversation
…igration Four amendments to the IIP-59 draft that override the earlier all-native design (pending PR iotexproject#73): 1. Commission rate source. Drop native `SetCommissionRate` action. Read voter-take portions from the existing `DelegateProfile` contract (mainnet 0xfa7f5..0e258 / testnet 0xd19ff..7774) via `getEncodedProfile(delegate)` at `PutPollResult`, invert to commission (`10000 - portion*100`), and support separate `BlockCommissionRate` + `EpochCommissionRate`. Empty profile falls back to legacy full-amount-to-delegate. Reuses operator UX; requires contract-owner governance (multisig / burn / delta caps) as a hard prerequisite. 2. Native compound via existing auto-deposit contract. When a voter has an active bucket registered with the compound contract (mainnet io108ck..q95uat9 / testnet 0xB575D..0B9Cf), the on-chain epoch distribution calls `AddDeposit(bucketId, amount)`; otherwise the share accrues to `unclaimedBalance` for pull-claim. Preserves today's passive-user compound UX. LSD holders get pull-claim parity with today. 3. Batched per-delegate log. Replace per-voter `VOTER_REWARD` receipt events with a single `DelegateDistributed{epoch, delegate, voters[], amounts[], commissionAmount, snapshotHash, blockComponent, epochComponent}` per opted-in delegate per epoch. Cuts receipt log volume from 10k-50k/epoch to ~24-100/epoch and gives off-chain verifiers a single anchor to check the frozen snapshot against. 4. Per-delegate opt-in transition. New `Candidate.VoterRewardOnchainOptIn` bool + `SetVoterRewardOptIn(bool)` action. Default post-fork is opt-out: legacy Hermes continues to distribute the full amount from the delegate's `RewardAddress`. Delegates must send an explicit opt-in tx; effect is delayed one epoch via the poll snapshot. Bidirectional (delegates can opt back out). Hermes off-chain service must filter opted-in delegates from `distributeRewards` at the same snapshot boundary to prevent double-spend. Rationale, backwards compatibility, six new test cases (opt-in delay, compound routing, empty-profile fallback, batched log format, Hermes double-spend prevention, contract-rate snapshot invariance), expanded security considerations (contract-owner governance, semantic inversion guard, opt-in coexistence, compound reentrancy, log payload bound, contract-read determinism), and updated Protocol Changes Required table all included. Co-Authored-By: Chen Chen <envestcc@gmail.com>
Adds §0 Architecture Overview at the top of Specification with a Mermaid flowchart showing the four layers (actors, reused EVM contracts, native protocol state, off-chain Hermes coexistence) and the data flow from setProfile/opt-in through PutPollResult snapshotting to GrantBlockReward pool credit and GrantEpochReward distribution (compound via AutoDeposit or unclaimedBalance credit). Also spells out four design invariants that fall out of the diagram: - rates + opt-in read once per epoch and frozen at PutPollResult - effectiveTip always to producer, only base block reward is split - AutoDeposit read per-voter at drain time, not snapshotted - Hermes MUST filter opted-in delegates at the same boundary Reading guide clarifies solid = writes/transfers, dashed = reads. Co-Authored-By: Chen Chen <envestcc@gmail.com>
Redesigns §0 diagram with four numbered stages (Config → Snapshot → Rewarding → Sinks) in strict left-to-right layout. Key changes: - LR direction instead of TB (fewer node crossings on wide screens). - Removes actor nodes (Delegate/Voter) — described in prose instead. - Drops VoterWeightView node — its output (voterWeights in snapshot) is what matters for the flow. - Removes the intermediate `SnapshotCommissionRates` process node; the read-arrows into PollSnap carry the same information. - Moves AutoDeposit onto the drain arrow (its actual runtime role as a per-voter router) rather than treating it as config source that also gets called back into. - Adds a "sink" color band so the three destination accounts read as a single group. - Uses ①②③④ stage markers so the reading order is unambiguous. - Prose intro rewritten as a single stage-by-stage sentence. Co-Authored-By: Chen Chen <envestcc@gmail.com>
…n a specific node Feedback: subgraph frames were catching arrows at the group boundary, making it ambiguous which inner node the arrow referenced. Fix: remove all subgraphs and split into two focused Mermaid diagrams. - Diagram A — Snapshot pipeline: DelegateProfile + Candidate.OptIn + VoterWeightView → PutPollResult → PollSnapshot. Four arrows, four named nodes each side. - Diagram B — Reward flow: PollSnapshot, GrantBlockReward, PendingPool, GrantEpochReward, AutoDeposit router, three sinks (DelReward / Bucket / Unclaimed), Hermes side-path. Every arrow lands on a named account or the AutoDeposit router — no group boundaries. Prose reworded to point at the two diagrams and cross-ref which § each covers. Co-Authored-By: Chen Chen <envestcc@gmail.com>
…rsion Per feedback, keeping the initial TB-flow overview with the four colored bands (contracts, staking, poll, rewarding) and actor writes shown. The two subsequent rework attempts (LR flatten and two-diagram split) are reverted. Co-Authored-By: Chen Chen <envestcc@gmail.com>
This was referenced Jul 10, 2026
Closed
envestcc
added a commit
to iotexproject/iotex-core
that referenced
this pull request
Jul 13, 2026
…lure to legacy path instead of halting the block (IIP-59) Previously any single delegate's read failure (RPC hiccup, ABI mismatch, out-of-range portion value) at PutPollResult propagated up and failed block production. Determinism-safe (same on-chain state ⇒ same error on every validator ⇒ no fork) but any malformed on-chain profile would deterministically halt the chain at every subsequent epoch boundary. Change: bridge.Snapshot now absorbs per-delegate read errors and returns a Registered=false entry for the affected delegate, keyed the same as successful ones. FreezePollSnapshot writes the degraded snapshot as normal; rewarding routes that delegate via the legacy Hermes path. Errors are logged with delegate + contract address for observability. Only wiring bugs stay hard errors: nil ContractReader, nil delegate address in the slice. Those indicate a caller-side mistake, not a data issue. Trade "deterministic reward-path fallback" for "deterministic block production failure" — the fallback is well-defined (legacy Hermes) and one bad DelegateProfile field never gets to hold the chain hostage. Test churn: - Snapshot_OutOfRangeRejected → Snapshot_OutOfRangeDegradesToUnregistered - Snapshot_LargeBigIntRejected → Snapshot_LargeBigIntDegradesToUnregistered - Snapshot_ReaderErrorPropagates → Snapshot_ReaderErrorDegradesToUnregistered - New: Snapshot_PerDelegateErrorIsolated — one bad delegate does not poison batched successful reads. - FreezePollSnapshot_BridgeErrorPropagates → FreezePollSnapshot_BridgeErrorDegradesToLegacy: snapshot IS written, Registered=false, opt-in still captured from live Candidate. Refs iotexproject/iips#74 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
envestcc
added a commit
to iotexproject/iotex-core
that referenced
this pull request
Jul 13, 2026
Adds a persistent per-delegate opt-in flag on staking.Candidate that gates protocol-native voter reward distribution. Default false — post-fork the legacy path (full block/epoch reward to RewardAddress, off-chain Hermes service continues) still runs unless the delegate explicitly opts in. Scope of this change is intentionally narrow: - stakingpb.Candidate gains field 11 (voterRewardOnchainOptIn bool). - staking.Candidate Go struct gains VoterRewardOnchainOptIn; Clone, Equal, toProto, fromProto all thread it. Default zero-value keeps existing candidates opting out on decode. - TestSerWithVoterRewardOnchainOptIn covers false/true round-trip through proto, Equal's flag sensitivity, and Clone independence. Deferred to follow-up PRs (per amended IIP-59, iotexproject/iips#74): - SetVoterRewardOptIn native action + handler that mutates the flag with a one-epoch delay via the PutPollResult snapshot (bidirectional flip). - BlockCommissionRate / EpochCommissionRate — these are per-epoch snapshot values populated from the DelegateProfile contract at PutPollResult; they live on the poll snapshot (PR 2') rather than the persistent Candidate. Superseded PRs: #4865 / #4866 / #4880 / #4881 (all closed 2026-07-10). Refs iotexproject/iips#74
envestcc
added a commit
to iotexproject/iotex-core
that referenced
this pull request
Jul 13, 2026
… opt-in at PutPollResult (IIP-59)
Introduces the per-candidate poll snapshot IIP-59 needs at each epoch boundary.
Downstream rewarding (PR 3', follow-up) reads this snapshot instead of the
live DelegateProfile contract / live staking.Candidate so a mid-epoch
mutation cannot retroactively re-split rewards that have already begun
accruing.
Wiring:
- stakingpb.CandidatePollSnapshot: block/epoch commission basis points,
registered flag, opt-in flag, per-voter entries (empty in this PR).
stakingpb.VoterWeightEntry per-voter tuple.
- staking._candidatePollSnapshot = 5 tag byte; full key
{tag}||candID.Bytes() under _stakingNameSpace.
- staking.FreezePollSnapshot: writer called from poll/util.setCandidates.
Nil bridge (contract not configured) → skip rate freeze but still
capture opt-in from live Candidate; Registered=false forces legacy
fallback downstream. Any per-delegate error aborts the whole snapshot
write (no partial map).
- staking.PollSnapshotFor: reader; returns ErrStateNotExist pre-fork /
pre-write.
- staking.readLiveOptIn: degrades to false when the poll list names a
candidate that has no staking record, rather than wedging the chain.
- poll.freezeIIP59PollSnapshot: fork + config gate. Guarded by
fCtx.NoVoterRewardDistribution (pre-fork no-op) and
Blockchain.DelegateProfileContractAddress (empty ⇒ nil bridge).
- poll.delegateProfileContractReader: view-call plumbing mirrors
consortium.getContractReaderForGenesisStates verbatim
(address.ZeroAddress caller, evm.SimulateExecution).
- genesis.Blockchain.DelegateProfileContractAddress: per-network config;
default empty.
- protocol.FeatureCtx.NoVoterRewardDistribution: fork gate bound to
!g.IsToBeEnabled(height); zero-value = active post-fork.
Intentionally out of scope for this PR:
- Voter-weight source. Entries is written empty; PR 3' has a degenerate
branch (empty voter list ⇒ full amount as commission). Follow-up PR
fills in the actual weight computation.
- Rewarding consumption of the snapshot (PR 3').
- SetVoterRewardOptIn action to mutate the field this snapshot freezes
(separate PR).
Stacks on iip-59/pr1-candidate-schema-optin (#4911) and
iip-59/pr4.5-delegateprofile-bridge (#4912).
Refs iotexproject/iips#74
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
envestcc
added a commit
to iotexproject/iotex-core
that referenced
this pull request
Jul 13, 2026
Follows PR 4.5's change to have delegateprofile.Bridge.Snapshot degrade per-delegate read failures to Registered=false instead of erroring out. FreezePollSnapshot's docstring now says "on bridge error the snapshot is still written with Registered=false; opt-in remains captured from the live Candidate" so downstream (PR 3') has a single degradation contract to reason about. Test rename: FreezePollSnapshot_BridgeErrorPropagates → FreezePollSnapshot_BridgeErrorDegradesToLegacy — asserts the snapshot IS written and opt-in is preserved on bridge error. Refs iotexproject/iips#74 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
6 tasks
4 tasks
envestcc
added a commit
to iotexproject/iotex-core
that referenced
this pull request
Jul 16, 2026
Introduce a read-only bridge over the existing DelegateProfile contract (mainnet io1lfl4ppn2c3wcft04f0rk0jy9lyn4pcjcm7638u) so that PutPollResult can snapshot per-delegate commission rates from the amended IIP-59 design (iotexproject/iips#74). The bridge invokes getProfileByField twice per delegate (blockRewardPortion and epochRewardPortion), inverts the on-chain voter-take portion into commission basis points, and returns a per-delegate map. Empty bytes for either field flag the delegate as unregistered so the caller falls back to the legacy Hermes path; a partial profile is deliberately treated as unregistered to preserve the "either fully opted-in or fully legacy" invariant. Explicit zero voter-take remains distinguishable (single 0x00 byte) and yields a registered 100% commission split. Values exceeding uint64 or 10000 basis points are rejected rather than silently truncated. The package has no dependency on protocol.StateManager and is exercised by 14 unit tests using an in-process ABI-round-tripping fake reader. Refs iotexproject/iips#74. Follow-up PR (2') will call Snapshot at PutPollResult and freeze the returned rates into the poll snapshot. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
envestcc
added a commit
to iotexproject/iotex-core
that referenced
this pull request
Jul 16, 2026
Adds a persistent per-delegate opt-in flag on staking.Candidate that gates protocol-native voter reward distribution. Default false — post-fork the legacy path (full block/epoch reward to RewardAddress, off-chain Hermes service continues) still runs unless the delegate explicitly opts in. Scope of this change is intentionally narrow: - stakingpb.Candidate gains field 11 (voterRewardOnchainOptIn bool). - staking.Candidate Go struct gains VoterRewardOnchainOptIn; Clone, Equal, toProto, fromProto all thread it. Default zero-value keeps existing candidates opting out on decode. - TestSerWithVoterRewardOnchainOptIn covers false/true round-trip through proto, Equal's flag sensitivity, and Clone independence. Deferred to follow-up PRs (per amended IIP-59, iotexproject/iips#74): - SetVoterRewardOptIn native action + handler that mutates the flag with a one-epoch delay via the PutPollResult snapshot (bidirectional flip). - BlockCommissionRate / EpochCommissionRate — these are per-epoch snapshot values populated from the DelegateProfile contract at PutPollResult; they live on the poll snapshot (PR 2') rather than the persistent Candidate. Superseded PRs: #4865 / #4866 / #4880 / #4881 (all closed 2026-07-10). Refs iotexproject/iips#74
envestcc
added a commit
to iotexproject/iotex-core
that referenced
this pull request
Jul 16, 2026
… opt-in at PutPollResult (IIP-59)
Introduces the per-candidate poll snapshot IIP-59 needs at each epoch boundary.
Downstream rewarding (PR 3', follow-up) reads this snapshot instead of the
live DelegateProfile contract / live staking.Candidate so a mid-epoch
mutation cannot retroactively re-split rewards that have already begun
accruing.
Wiring:
- stakingpb.CandidatePollSnapshot: block/epoch commission basis points,
registered flag, opt-in flag, per-voter entries (empty in this PR).
stakingpb.VoterWeightEntry per-voter tuple.
- staking._candidatePollSnapshot = 5 tag byte; full key
{tag}||candID.Bytes() under _stakingNameSpace.
- staking.FreezePollSnapshot: writer called from poll/util.setCandidates.
Nil bridge (contract not configured) → skip rate freeze but still
capture opt-in from live Candidate; Registered=false forces legacy
fallback downstream. Any per-delegate error aborts the whole snapshot
write (no partial map).
- staking.PollSnapshotFor: reader; returns ErrStateNotExist pre-fork /
pre-write.
- staking.readLiveOptIn: degrades to false when the poll list names a
candidate that has no staking record, rather than wedging the chain.
- poll.freezeIIP59PollSnapshot: fork + config gate. Guarded by
fCtx.NoVoterRewardDistribution (pre-fork no-op) and
Blockchain.DelegateProfileContractAddress (empty ⇒ nil bridge).
- poll.delegateProfileContractReader: view-call plumbing mirrors
consortium.getContractReaderForGenesisStates verbatim
(address.ZeroAddress caller, evm.SimulateExecution).
- genesis.Blockchain.DelegateProfileContractAddress: per-network config;
default empty.
- protocol.FeatureCtx.NoVoterRewardDistribution: fork gate bound to
!g.IsToBeEnabled(height); zero-value = active post-fork.
Intentionally out of scope for this PR:
- Voter-weight source. Entries is written empty; PR 3' has a degenerate
branch (empty voter list ⇒ full amount as commission). Follow-up PR
fills in the actual weight computation.
- Rewarding consumption of the snapshot (PR 3').
- SetVoterRewardOptIn action to mutate the field this snapshot freezes
(separate PR).
Stacks on iip-59/pr1-candidate-schema-optin (#4911) and
iip-59/pr4.5-delegateprofile-bridge (#4912).
Refs iotexproject/iips#74
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
envestcc
added a commit
to iotexproject/iotex-core
that referenced
this pull request
Jul 16, 2026
Follows PR 4.5's change to have delegateprofile.Bridge.Snapshot degrade per-delegate read failures to Registered=false instead of erroring out. FreezePollSnapshot's docstring now says "on bridge error the snapshot is still written with Registered=false; opt-in remains captured from the live Candidate" so downstream (PR 3') has a single degradation contract to reason about. Test rename: FreezePollSnapshot_BridgeErrorPropagates → FreezePollSnapshot_BridgeErrorDegradesToLegacy — asserts the snapshot IS written and opt-in is preserved on bridge error. Refs iotexproject/iips#74 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
envestcc
added a commit
to iotexproject/iotex-core
that referenced
this pull request
Jul 21, 2026
Introduce a read-only bridge over the existing DelegateProfile contract (mainnet io1lfl4ppn2c3wcft04f0rk0jy9lyn4pcjcm7638u) so that PutPollResult can snapshot per-delegate commission rates from the amended IIP-59 design (iotexproject/iips#74). The bridge invokes getProfileByField twice per delegate (blockRewardPortion and epochRewardPortion), inverts the on-chain voter-take portion into commission basis points, and returns a per-delegate map. Empty bytes for either field flag the delegate as unregistered so the caller falls back to the legacy Hermes path; a partial profile is deliberately treated as unregistered to preserve the "either fully opted-in or fully legacy" invariant. Explicit zero voter-take remains distinguishable (single 0x00 byte) and yields a registered 100% commission split. Values exceeding uint64 or 10000 basis points are rejected rather than silently truncated. The package has no dependency on protocol.StateManager and is exercised by 14 unit tests using an in-process ABI-round-tripping fake reader. Refs iotexproject/iips#74. Follow-up PR (2') will call Snapshot at PutPollResult and freeze the returned rates into the poll snapshot. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
envestcc
added a commit
to iotexproject/iotex-core
that referenced
this pull request
Jul 21, 2026
Adds a persistent per-delegate opt-in flag on staking.Candidate that gates protocol-native voter reward distribution. Default false — post-fork the legacy path (full block/epoch reward to RewardAddress, off-chain Hermes service continues) still runs unless the delegate explicitly opts in. Scope of this change is intentionally narrow: - stakingpb.Candidate gains field 11 (voterRewardOnchainOptIn bool). - staking.Candidate Go struct gains VoterRewardOnchainOptIn; Clone, Equal, toProto, fromProto all thread it. Default zero-value keeps existing candidates opting out on decode. - TestSerWithVoterRewardOnchainOptIn covers false/true round-trip through proto, Equal's flag sensitivity, and Clone independence. Deferred to follow-up PRs (per amended IIP-59, iotexproject/iips#74): - SetVoterRewardOptIn native action + handler that mutates the flag with a one-epoch delay via the PutPollResult snapshot (bidirectional flip). - BlockCommissionRate / EpochCommissionRate — these are per-epoch snapshot values populated from the DelegateProfile contract at PutPollResult; they live on the poll snapshot (PR 2') rather than the persistent Candidate. Superseded PRs: #4865 / #4866 / #4880 / #4881 (all closed 2026-07-10). Refs iotexproject/iips#74
envestcc
added a commit
to iotexproject/iotex-core
that referenced
this pull request
Jul 21, 2026
… opt-in at PutPollResult (IIP-59)
Introduces the per-candidate poll snapshot IIP-59 needs at each epoch boundary.
Downstream rewarding (PR 3', follow-up) reads this snapshot instead of the
live DelegateProfile contract / live staking.Candidate so a mid-epoch
mutation cannot retroactively re-split rewards that have already begun
accruing.
Wiring:
- stakingpb.CandidatePollSnapshot: block/epoch commission basis points,
registered flag, opt-in flag, per-voter entries (empty in this PR).
stakingpb.VoterWeightEntry per-voter tuple.
- staking._candidatePollSnapshot = 5 tag byte; full key
{tag}||candID.Bytes() under _stakingNameSpace.
- staking.FreezePollSnapshot: writer called from poll/util.setCandidates.
Nil bridge (contract not configured) → skip rate freeze but still
capture opt-in from live Candidate; Registered=false forces legacy
fallback downstream. Any per-delegate error aborts the whole snapshot
write (no partial map).
- staking.PollSnapshotFor: reader; returns ErrStateNotExist pre-fork /
pre-write.
- staking.readLiveOptIn: degrades to false when the poll list names a
candidate that has no staking record, rather than wedging the chain.
- poll.freezeIIP59PollSnapshot: fork + config gate. Guarded by
fCtx.NoVoterRewardDistribution (pre-fork no-op) and
Blockchain.DelegateProfileContractAddress (empty ⇒ nil bridge).
- poll.delegateProfileContractReader: view-call plumbing mirrors
consortium.getContractReaderForGenesisStates verbatim
(address.ZeroAddress caller, evm.SimulateExecution).
- genesis.Blockchain.DelegateProfileContractAddress: per-network config;
default empty.
- protocol.FeatureCtx.NoVoterRewardDistribution: fork gate bound to
!g.IsToBeEnabled(height); zero-value = active post-fork.
Intentionally out of scope for this PR:
- Voter-weight source. Entries is written empty; PR 3' has a degenerate
branch (empty voter list ⇒ full amount as commission). Follow-up PR
fills in the actual weight computation.
- Rewarding consumption of the snapshot (PR 3').
- SetVoterRewardOptIn action to mutate the field this snapshot freezes
(separate PR).
Stacks on iip-59/pr1-candidate-schema-optin (#4911) and
iip-59/pr4.5-delegateprofile-bridge (#4912).
Refs iotexproject/iips#74
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
envestcc
added a commit
to iotexproject/iotex-core
that referenced
this pull request
Jul 21, 2026
Follows PR 4.5's change to have delegateprofile.Bridge.Snapshot degrade per-delegate read failures to Registered=false instead of erroring out. FreezePollSnapshot's docstring now says "on bridge error the snapshot is still written with Registered=false; opt-in remains captured from the live Candidate" so downstream (PR 3') has a single degradation contract to reason about. Test rename: FreezePollSnapshot_BridgeErrorPropagates → FreezePollSnapshot_BridgeErrorDegradesToLegacy — asserts the snapshot IS written and opt-in is preserved on bridge error. Refs iotexproject/iips#74 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
envestcc
added a commit
to iotexproject/iotex-core
that referenced
this pull request
Jul 21, 2026
Wire the three read-side bridges landed upstream — PR 2' (frozen poll
snapshot), PR 4.6 (autodeposit compound bridge), PR 4.7 (batched
DelegateDistributed log) — into GrantEpochReward. For each opted-in
delegate, IIP-59 §3.2 now: splits the epoch pool by the frozen commission
basis points, allocates the voter pool proportionally by frozen voter
weight in canonical order, routes each per-voter share to compound
(native AddDeposit) or credit (rewarding unclaimedBalance), credits the
delegate's commission to its reward address, and emits exactly one
batched DelegateDistributed log per delegate.
Feature-flag matrix stays honoured:
NoVoterRewardDistribution=true (pre-fork) → return (nil, false, nil);
caller runs legacy grantToAccount unchanged.
VoterRewardOnchainOptIn=false → same fallback (opt-out).
no poll snapshot yet → same fallback (first
epoch after registration).
Registered=false (bridge degraded) → same fallback.
autoDepositBridge nil → split runs; every voter
routes to credit (compound routing inactive).
Per-item consensus fallback per feedback-consensus-fallback-vs-halt:
malformed on-chain data (bridge RPC error, bucket read error, ineligible
bucket) downgrades the affected voter to credit rather than halting the
block. Wiring errors (nil staking protocol, log-encoder failure) still
hard-fail.
Cross-protocol seam: staking.AddDepositForCompound is a package-exported
entry point for the rewarding-side compound path. Skips the
handleDepositToStake action-plumbing checks that PR 3' has already
enforced upstream (positive bucketID from AutoDeposit.bucket(voter);
IsBucketEligibleForCompound confirmed native/active/AutoStake/Owner).
Does NOT emit a staking receipt log — the batched DelegateDistributed
log is the single source of truth per delegate.
New:
- action/protocol/rewarding/voter_reward.go — distributeVoterReward
+ splitCommission (basis-points helper) + resolveAutoDepositReader.
- action/protocol/rewarding/voter_reward_test.go — 14 tests covering
splitCommission edge cases, fork gate, nil-input guards, missing-
snapshot fallback, bridge-nil default, and Option wiring.
- action/protocol/staking/add_deposit_compound.go — cross-protocol seam.
Modified:
- action/protocol/rewarding/reward.go — one call site + one branch
inside GrantEpochReward's per-delegate loop. splitEpochReward now
returns the filtered candidate list so callers see the same index
domain as addrs/amounts.
- action/protocol/rewarding/protocol.go — Option type +
WithAutoDepositBridge / WithAutoDepositReader; NewProtocol accepts
opts ...Option so 11 existing call sites remain unchanged.
- chainservice/builder.go — construct autodeposit.Bridge from
Blockchain.AutoDepositContractAddress at registerRewardingProtocol;
empty string ⇒ nil bridge (compound routing inactive).
Stacks on:
- iip-59/pr4.7-delegatedistributed-log (#4923) — merge base.
- iip-59/pr2-snapshot-writer (#4915) — merged in via d7fe741.
- iip-59/pr4.6-autodeposit-bridge (#4922) — via #4923.
- iip-59/pr4.5-delegateprofile-bridge (#4912) — via #4915.
Deliberately out of scope: block-reward folding + orphan drain
(PR 4'); voter-weight source population (Entries currently empty
per PR 2' skeleton — downstream degenerate branch pays full amount as
commission until the weight-source follow-up lands).
Refs iotexproject/iips#74
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Four amendments to the IIP-59 draft that override the earlier all-native design (superseding parts of pending PR #73). None affect the goal — protocol-native voter reward distribution replacing the centralized Hermes service — but each closes a UX regression or migration gap in the original spec.
1. Commission-rate source: reuse
DelegateProfilecontractSetCommissionRateaction + on-chain rate storage.DelegateProfilecontract atPutPollResultviagetEncodedProfile(delegate); invert to commission (10000 − portion × 100); snapshot bothBlockCommissionRateandEpochCommissionRateseparately.0xfa7f50866ac45d84adf54bc767c885f92750e258, testnet0xd19ffB48a5C18B77c541D32c1B1ac2440c287774.DelegateProfilestores the analyser-format voter-take portion (analyser/common/rewards/hermes.go:60-67). The bridge MUST invert. A regression here would silently swap voter and delegate shares; guarded by unit test against an oracle Hermes distribution over 100 mainnet delegates.RewardAddress), matching today's Hermes behavior for unconfigured delegates.DelegateProfilecontract owner is currently a single EOA (0x68c12a8c5d5f0a1fd13319ba4840301b0c93bd4f). Before the fork, ownership MUST move to a multisig / burn / delta-cap + delay scheme. Documented in §5 Step 1 and Security Considerations.2. Native compound via existing auto-deposit contract
io108ckwzlzpkhva7cnfceajlu7wu6ql5kq95uat9, testnet0xB575DfA55d9Cd04841567Fe8461BFC79D068B9Cf).bucketId != 0 && bucket.Owner == voter && AutoStake && status == active→AddDeposit(bucketId, amount); else accrue tounclaimedBalancefor pull-claim.3. Batched
DelegateDistributedlogReplaces per-voter
VOTER_REWARDreceipt events with a single log per opted-in delegate per epoch:snapshotHashgives off-chain verifiers a single anchor to check the batched arrays against the frozen voter weight snapshot.MaxLogPayloadBytes(200 KB default) with{seq, seqTotal}for delegates that ever exceed the cap.4. Per-delegate opt-in transition
Candidate.VoterRewardOnchainOptIn: bool.SetVoterRewardOptIn(bool)action (protobuf tag60,ioctl stake2 optin [--off]).RewardAddress.distributeRewardsat the same snapshot boundary to prevent double-spend. Documented in Security Considerations.Rollout is now per-delegate rather than a fork-wide flip; delegates who need more time (compound registration, voter comms, sub-tooling) don't block the rest.
What the amendment file changes
VoterRewardOnchainOptIn,BlockCommissionRate,EpochCommissionRateto theCandidatestruct.SetCommissionRatewithSetVoterRewardOptIn.effectiveTipfrom base block reward (§3.1), dual-rate epoch split (§3.2), synthetic*state.Candidatefor orphan drain (§3.3), 3-item PutPollResult snapshotting (§3.4), NEW §3.5DelegateProfilecontract rate source, NEW §3.6AutoDepositcompound, NEW §3.7 per-delegate opt-in gate.DelegateProfileownership handover as Step 1.ioctl stake2 optin [--off].handleSetVoterRewardOptInwith idempotency + pre-fork guard, dual-rate split code,newDelegateDistributedLogemitter, NEW §7.9SnapshotCommissionRatesbridge (evm.SimulateExecution+invertToCommission), NEW §7.10compoundOrCreditbridge with 4-precondition check + graceful degradation.DelegateProfile", "Why Native Compound", "Why a Single Batched Log"; extends Performance Impact.handleSetCommissionRate; addedhandler_voter_reward_optin.go,delegate_profile_bridge.go,auto_deposit_bridge.go,stake2optin.go,SetVoterRewardOptInaction registration.Why now, before more code work
The original design (implemented across draft PRs 1–4 in
iotex-core) broke Hermes UX in four places (push→pull, compound gap, service fee removal, log volume) and had no per-delegate migration ramp. Amending the IIP first — before rewriting or extending those PRs — is cheaper than shipping code that gets rolled back after review.Test plan
Related: supersedes portions of #73.
🤖 Generated with Claude Code