Reward Entitlement Identity and Batch Canonicalization
- Name: Reward entitlement identity and batch canonicalization
- Category: rewards reference / batch-claim checklist / epoch-set normalization / reward-payout idempotency note
- Summary: Healthy reward systems do not let
earned reward,who certifies that reward,which account or position earned it,which epoch or tranche it belongs to, andwhether it has already been consumeddrift into separate stories. A claim helper may expose convenient batch arrays, multiple staking modules, epoch lists, proof bundles, or tranche selectors, but those are only views over one security object: a canonical reward entitlement that should be authentic, position-bound, distinct, and consumable exactly once. When the batch surface accepts duplicate epoch IDs, trusts caller-supplied certifier contracts, lets the caller rebind certifier output onto an unrelated beneficiary, or aggregates payout before the consumed-state object is definitively advanced, the helper has already become a mint or drain surface. Once a helper is deciding which selectors are real, which certifiers are legitimate, which beneficiary they are allowed to pay, and whether the same right is still live, it is already live reward-entitlement authority rather than harmless UX sugar. - Why this note exists:
- Reward incidents are often narrated as
duplicate claim,missing validation,bad batch accounting, orreward debt bug, but many of them first break the same positive-design boundary: the protocol no longer has one canonical answer towhat exact reward right is being spent right now? - Batch claim helpers are especially dangerous because they are written as convenience code while quietly becoming the place where reward identity, uniqueness, provenance, and payout accumulation all get decided.
- The analytical win is to ask
what exact reward entitlement object should this batch be consuming?before debating whether the immediate bug was=versus+=, duplicate array entries, fake staking contracts, or stale post-update accounting.
- Reward incidents are often narrated as
The five joints a healthy reward claim system should keep fused
-
Canonical reward entitlement identity
- The system should have one exact object that defines a spendable reward right:
(program, account, epoch/index/tranche, asset)or an equivalently precise claim record. - Public APIs may expose arrays of epochs, claim indices, staking modules, or Merkle leaves, but those are only selectors for that underlying entitlement.
- If one path keys rewards by
epoch, another bycontract, and another by ambient accrued amount, the protocol is already relying on folklore about whether those views describe the same right.
- The system should have one exact object that defines a spendable reward right:
-
Canonical reward certifier provenance
- The protocol must know who is allowed to certify that the entitlement exists and how large it is.
- The safest shape is that the batch helper derives claim sources from canonical storage, immutable registry state, or already-authenticated proofs, rather than from caller-supplied contract arrays.
- A certifier that implements the expected interface is still not enough. The protocol also needs to know what beneficiary / position scope that certifier is allowed to speak for, so
this contract can report some rewarddoes not quietly becomethis contract may mint for any account the caller names. - This is the joint that broke in Convergence Finance, where the user could choose the contract that defined reward reality.
-
Distinct-set canonicalization before payout aggregation
- The batch surface should either derive the unique live entitlement set internally or canonicalize caller-supplied selectors before it starts summing value.
Users won't repeat the same epochis not a security property.- This is the joint that broke in Level Finance, where one real reward epoch could be replayed inside a single batch.
-
Consumed-state advance before sibling iterations still see the old right
- Once one reward entitlement has been priced into payout, later loop iterations or sibling branches should not still be able to observe it as fresh.
- The safest design is a shared internal consume primitive that computes the live amount, advances the canonical consumed state, and only then contributes to the batch accumulator.
- This is the joint that breaks in duplicate-claim families like duplicate-claim idempotency failure and in stale reward-debt families when post-update user state still settles from an old accumulator view.
-
Repeated exercise should collapse to zero additional reward
- A healthy reward claim system is idempotent at the entitlement level: repeating the same logical claim should revert, return zero, or observe already-consumed state.
- That property should hold across single-claim and batch-claim cousins, not just inside one happy-path function.
- If
claim()is safe butclaimMultiple()can still pay, the protocol does not have one reward-consumption boundary; it has two stories about the same money.
Three reward-claim authority facts reviewers should not collapse
-
This source is allowed to describe reward reality at all
- Before discussing duplicate epochs or replayable tranches, reviewers should ask whether this contract, registry branch, Merkle root, staking module, or proof family is even allowed to certify reward existence and size.
- Convergence Finance is the clean reminder that an interface-compatible reward source is still not automatically an authorized reward certifier.
-
This selector resolves to one unique live entitlement
- A caller-supplied epoch, leaf, tranche id, or module selector is not yet money-good just because it parses and points somewhere real.
- The protocol still has to prove that the selector maps to one distinct
(program, account, epoch/index/tranche, asset)right rather than several copies of the same right hiding inside a batch.
-
This helper invocation may consume and pay that entitlement now
- Even after source authenticity and selector uniqueness are settled, the helper still has to make one more transition: this exact call path may advance the consumed-state object and authorize payout once.
- That is why batch helpers should be reviewed like live custody code. The function that canonicalizes selectors and flips consumed progress is already deciding when reward becomes money.
What sound design looks like
-
One reward right, many selectors, one consume boundary
- Users may see epochs, indices, proofs, staking modules, or tranche ids, but those should all resolve to one canonical entitlement object before any payout is counted.
- The batch helper should not become the place where economic identity is improvised from raw user input.
-
A shared internal consume step used by single and batch claim paths
- The safest shape is
authenticate entitlement -> compute live reward -> advance consumed state -> add to accumulator -> pay. - If
claim()andclaimMultiple()open-code different storage updates, drift is already likely.
- The safest shape is
-
Caller convenience should come after canonicalization, not before
- Accepting arrays from the caller is fine only if duplicates, ordering games, and counterfeit sources have already been neutralized.
- If the protocol cannot cheaply canonicalize user input, it is often safer to derive claimable items from storage or to force one-at-a-time claims through a shared consume primitive.
-
Batch helpers should be treated as live entitlement-authority surfaces
- The helper that decides
these sources are authentic,these selectors are distinct, andthis consumed-state object may now advanceis already part of the custody boundary. - Review it with the same upgrade discipline, invariant testing, and telemetry expectations as the final token-paying contract.
- The helper that decides
-
Reward source provenance should be explicit and machine-checkable
- Helpers that aggregate across several staking contracts or distributors should source those contracts from one canonical registry or immutable set.
- Interface compatibility is not legitimacy.
- The certifier’s authority should stay scoped to one beneficiary / tranche / position namespace so a legitimate-looking source cannot be rebound into a generic mint oracle for whichever account the caller chooses.
-
Accumulator math and user debt must settle from the same state version
- Reward systems that use global indices or per-share accumulators must ensure that user-side reward debt, writeoff, or claimed state is updated from the same post-update view that later claim math treats as authoritative.
- Otherwise a near-zero denominator, fresh pool, or first-touch boundary can amplify small drift into a mint primitive.
Useful positive design shapes
1. Storage-derived or registry-derived claim sets
- The safest batch helpers do not trust the caller to enumerate canonical reward sources or unique claim objects.
- If the caller supplies selectors at all, those selectors should be validated against one canonical registry before any reward computation begins.
2. One consume primitive for claim() and claimMultiple()
- Level Finance is the clean reminder that one local divergence between single and batch cousins can turn a real reward entitlement into replayable payout.
- The reusable lesson is not just
deduplicate arrays; it issingle and batch paths should advance the same consumed-state object the same way.
3. Distinctness checks before any batch accumulator grows
- Systems like Balancer Merkle Orchard show why
we set the claimed bit lateris too late once payout has already been accumulated. - Distinctness must be enforced before the batch total starts treating one entitlement as several.
4. Source authenticity before reward amount trust
- Convergence Finance shows the adjacent failure mode where the batch did not replay one real reward right; it let the user choose the object that defined reward reality.
- The reusable lesson is that reward amount and reward source belong to the same security boundary.
5. Same-state-version reward debt settlement
- Cover Protocol Blacksmith is the accumulator cousin: global reward truth moved to a new state version while user writeoff settled from an older one.
- The reusable lesson is that
updated market indexandupdated user debtmust describe the same accounting frame before any reward becomes mintable or claimable.
Design questions worth asking first
- What exact storage object or proof object says this reward right is still live?
- Which contracts or registries are allowed to certify that right and its amount?
- Can the caller choose the reward-certifying contract or claim source?
- If a certifier is real, what exact beneficiary / position namespace is it allowed to certify, and can the caller rebind that output onto some other account anyway?
- If the caller supplies epochs, indices, or proofs, where are duplicates rejected or canonicalized before payout aggregation begins?
- Do
claim()andclaimMultiple()advance the same consumed-state object through the same internal primitive? - If a global reward index or accumulator updates, does user reward debt settle from that same post-update state version?
- If the same logical reward claim is exercised twice, what guarantees the second attempt returns zero additional value?
Red flags that usually mean the reward helper is relying on folklore
This batch function is just a convenience wrapperUsers won't repeat the same epoch or proofThat contract implements the right interface, so it must be one of oursThe batch helper accepts claimContracts[] or staking-module arrays from the caller, but those are only convenience inputsWe set the claimed bit at the endclaim()is safe, soclaimMultiple()is probably safe too`The pool is tiny only during launch, so weird accumulator jumps are fineWe'll reconcile reward debt on the next interaction
Failure-prevention checklist
- Define one canonical reward-entitlement key and make every claim path, batch helper, proof verifier, and distributor registry resolve down to that same object before any payout is added up.
- Keep reward-source authenticity inside the same boundary as reward amount. A user should not be able to choose the staking contract, distributor, tranche source, or registry branch that certifies reward reality unless that choice is already constrained by canonical protocol state.
- Bind certifier scope and beneficiary scope together. A legitimate certifier for one staking module, user class, or position namespace should not become a generic oracle that can mint or release rewards for arbitrary caller-named recipients.
- Canonicalize distinctness before accumulation. Reject duplicate epochs, duplicate leaves, duplicate indices, and duplicate
(program, account, epoch, asset)selectors before any batch total grows or any transfer side effect starts. - Use one shared internal consume primitive for single and batch claims so
authenticate -> compute live amount -> consume -> accumulate/paystays one invariant instead of several open-coded cousins. - Treat the helper that authenticates sources, canonicalizes selectors, and flips consumed progress as live reward authority. It should inherit the same review, upgrade-gating, and production-monitoring seriousness as any direct payout contract.
- Advance consumed state before sibling iterations, callbacks, or later branches can still see the old right as fresh. Late invalidation is already a replay surface.
- Treat idempotence as a balance-sheet property, not a UX courtesy. Repeating the same logical claim should return zero new value, whether the replay comes from the same batch, a later call, or a sibling public helper.
- Settle global reward-index updates and per-user reward debt from the same state version. If the denominator, accumulator, or user writeoff lags, the protocol is quietly minting from timing drift.
- Test tiny-denominator, launch-state, and fresh-pool edges as first-class cases. Reward systems often look safe only until a small supply or first-touch state makes one stale frame dominate the math.
- Monitor claim surfaces for impossible repetition patterns: the same epoch twice in a batch, the same entitlement exercised through two cousins, or reward growth that exceeds what one certifier path should ever authorize.
Audit prompts
- What exact canonical object represents one spendable reward right, and where is that identity resolved before any batch payout logic runs?
- Which contracts, registries, proofs, or modules are allowed to certify reward existence and size, and can a caller substitute one of them without crossing a protocol-owned allowlist or registry boundary?
- Even if the certifier is canonical, what exact beneficiary / position scope is it allowed to certify, and where is that binding enforced before payout accumulates?
- Which exact helper or function first decides that caller-supplied selectors are authentic, distinct, and payable, and is that surface reviewed like live custody logic rather than convenience code?
- Where are duplicate selectors rejected or canonicalized: before accumulation, during iteration, or only after payout-side effects already started?
- Do
claim()andclaimMultiple()call the same internal consume primitive, or are reviewers relying on several manually synchronized storage-update sequences? - If one logical entitlement is exercised twice through different public surfaces, what exact state transition guarantees the second path observes zero remaining value?
- When global reward state changes, do user reward debt, pool-level accumulator state, and batch-claim math all reference the same post-update frame?
- Can a fresh market, tiny denominator, or first-touch state amplify a small accounting mismatch into a full mint or drain?
- What production signal would reveal that one entitlement was paid twice, one fake certifier got accepted, or one accumulator jump outran the user-debt update that was supposed to cap it?
Lessons that should already be standard
- Batch helpers are not harmless convenience code. They are often the real place where reward identity, uniqueness, provenance, and payout finality get decided.
- Batch convenience is already reward authority. The function that canonicalizes selectors and advances consumed state is part of the money boundary even if the final token transfer happens elsewhere.
Interface-compatibleis not the same asauthorized to certify reward truth. Counterfeit certifiers are a reward-mint bug, not just an integration bug.- A reward certifier is not automatically a generic beneficiary oracle. Even legitimate modules need scope: who or what they may certify for should stay fused to the entitlement boundary.
- Duplicate claims are rarely just a loop bug. They usually mean the protocol never had one canonical answer to
what exact reward right is being spent right now? - Claim idempotence has to survive surface-area growth. A safe single-claim path does not matter much if the batch cousin, migration cousin, or helper cousin can still spend the same right again.
- Reward accounting and user-debt accounting are one security object. If they settle from different state versions, someone will eventually monetize the timing gap.
- Tiny denominators, launch states, and first-touch boundaries are not edge noise. They are where one stale or counterfeit reward frame becomes large enough to matter.
- Healthy reward systems make entitlement identity boring: one canonical key, one authentic certifier story, one consume boundary, and one second-attempt result of zero.
How to use this note with the exploit corpus
- Start here when the sharper question is
what exact reward entitlement object should this claim helper have authenticated, canonicalized, and consumed before paying anything out? - Drop into idempotency failure when the main task is classifying replayable unlock, vesting, or reward claims broadly.
- Pair with Exit right consumed-state binding when the sharper question is how sibling payout branches should all have consumed one shared entitlement object.
- Pair with root-of-trust substitution when the decisive failure is that the caller chose the contract or account graph that certifies reward truth in the first place.
- Pair with denominator-collapse reward amplification when the decisive bug is that accumulator truth and user debt truth settled from different state versions, especially under dust denominators.
- Pair with Reward-index initialization failure when the sharper question is the zero-sentinel / first-touch boundary rather than batch claim identity.
Internal linkages
- Canonical replay umbrella: duplicate-claim-idempotency-failure.
- Consumed-state companion: exit-right-consumed-state-binding.
- Counterfeit-certifier cousin: account-confusion-root-of-trust-substitution.
- Accumulator-state-version cousin: reward-debt-desynchronization-denominator-collapse-reward-amplification.
- Initialization cousin: reward-index-initialization-failure.
- Strong concrete exemplars: Level Finance ReferralControllerV2, Balancer Merkle Orchard, Convergence Finance, Cover Protocol Blacksmith.
Control surface
-
The decisive control surface is the boundary between
this reward right is authentic, distinct, and still liveandthis reward has already been priced, consumed, or never existed at all. -
In practice that means claim-source provenance, entitlement identity, batch canonicalization, helper-local authority over which selectors count as payable, consumed-state timing, and whether global accumulator truth and user debt truth still refer to the same accounting frame.
-
Healthy reward systems treat batch helpers as economic authority surfaces, not as harmless UX sugar around money that is supposedly already accounted for.
-
Sources:
- https://rekt.news/level-finance-rekt
- https://medium.com/balancer-protocol/logic-error-bug-fix-review-da37f0cc9a08
- https://medium.com/immunefi/balancer-logic-error-bugfix-review-74f5edca8b1a
- https://blog.verichains.io/p/convergence-incident-how-a-simple
- https://coverprotocol.medium.com/12-28-post-mortem-34c5f9f718d4
-
Last reviewed: 2026-07-29 UTC