Skip to content

refactor(staker): unify reward lastCollectTime invariants#1263

Open
notJoon wants to merge 4 commits intomainfrom
refactor/staker-unify-last-collect-time
Open

refactor(staker): unify reward lastCollectTime invariants#1263
notJoon wants to merge 4 commits intomainfrom
refactor/staker-unify-last-collect-time

Conversation

@notJoon
Copy link
Copy Markdown
Member

@notJoon notJoon commented Apr 21, 2026

Description

This PR makes the staker reward cursor invariant explicit and symmetric across internal GNS rewards and external incentive rewards.

Both reward streams maintains a lastCollectTime cursor. That cursor is expected to move monotonically forward from the effective last collection time, where an unset to zero cursor falls back to the deposit's StakeTime. Before this change, the internal and external reward paths enforced that rule through different read paths, which made the invariant weaker for external rewards in defensive cases.

Problem

CollectReward settles two independent reward streams:

  • internal GNS emissions
  • external incentive token rewards

Both streams update a lastCollectTime cursor to prevent collecting rewards over an invalid earlier interval. However the update paths were asymmetric:

  • Internal reward updates used DepositResolver.InternalRewardLastCollectTime(), which falls back to StakeTime() when the stored cursor is zero.
  • External reward updates read Deposit.GetExternalRewardLastCollectTime(...) directly, which bypassed the wrapped fallback behavior.

As a result, the external path did not enforce the same lower bound in two edge cases:

  • when the external reward cursor did not exist yet
  • when an external reward cursor entry existed but stored 0

In those cases, the external path could skip or weaken the monotonic validation that the internal path already performed. The normal production flow is not expected to create those states intentionally, but the implementation still encoded two different strengths of the same invariant. That made the reward cursor logic harder to reason about and easier to regress later.

Changes

  • Add a shared monotonic collect-time assertion on DepositResolver.
  • Route both internal and external reward cursor updates through the shared assertion.
  • Validate external reward cursor updates through ExternalRewardLastCollectTime(incentiveID), so missing or zero cursor state falls back to StakeTime() like the internal path.
  • Remove the redundant external reward cursor map guard because Deposit.SetExternalRewardLastCollectTime already lazy-initializes the map.
  • Align the public getter read path so the internal last collect timestamp uses the same wrapped resolver behavior as the external getter path.
  • Consolidate the internal CollectReward update flow so reward accounting, cursor updates, transfers, and event emission live under one positive hasInternalReward branch.
  • Narrow the scope of internal reward accounting variables and return explicit zero values from the no-internal-reward path.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved internal reward collection handling with fallback timestamp logic when the last-collect timestamp is unavailable, now defaults to the deposit's stake time.
  • Tests

    • Added test coverage for internal reward collection fallback behavior.

notJoon added 4 commits April 21, 2026 14:20
Collapse the repeated `!skipInternalUpdate` checks in `CollectReward`  into a single internal reward path.
Rename the internal reward guard from `skipInternalUpdate` to `hasInternalReward` to avoid the double negative branch

Also narrow internal reward accounting variables to the branch where they are used and return explicit zero values on the no internal reward path.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

Walkthrough

Changes refactor the staker contract's reward collection logic by introducing a deposit resolver pattern for timestamp resolution, reorganizing CollectReward to conditionally handle internal rewards with scoped variables and early returns, and adding a centralized monotonicity validation helper for collect-time timestamps.

Changes

Cohort / File(s) Summary
Getter Updates
contract/r/gnoswap/staker/v1/getter.gno, contract/r/gnoswap/staker/v1/getter_test.gno
Modified GetDepositInternalRewardLastCollectTimestamp to resolve timestamps via deposit resolver instead of direct deposit loading; added fallback test verifying timestamp returns to stake time when internal reward collect time is zero.
Reward Collection Refactor
contract/r/gnoswap/staker/v1/staker.gno
Reorganized CollectReward to replace skipInternalUpdate flag with hasInternalReward condition, scoping internal reward variables to their processing branch; added conditional early-return path for internal rewards and separate handling for unclaimable internal rewards when no internal reward exists.
Validation Helper
contract/r/gnoswap/staker/v1/type.gno
Added assertMonotonicCollectTime helper method to centralize timestamp monotonicity validation; refactored updateInternalRewardLastCollectTime and updateExternalRewardLastCollectTime to delegate validation to this helper.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main refactoring objective: unifying the reward lastCollectTime invariants across internal and external reward paths in the staker module.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/staker-unify-last-collect-time

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.

@sonarqubecloud
Copy link
Copy Markdown

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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 22b4fc2e-f72e-44b5-81c2-e599bb43525d

📥 Commits

Reviewing files that changed from the base of the PR and between 7931e1a and 3f949d9.

📒 Files selected for processing (4)
  • contract/r/gnoswap/staker/v1/getter.gno
  • contract/r/gnoswap/staker/v1/getter_test.gno
  • contract/r/gnoswap/staker/v1/staker.gno
  • contract/r/gnoswap/staker/v1/type.gno

Comment on lines +725 to +744
// Unclaimable must still be processed when regular internal rewards are
// skipped so accrued pool state is cleared for the collect window.
unClaimableInternal := s.processUnClaimableReward(depositResolver.TargetPoolPath(), currentTime)
if unClaimableInternal > 0 {
totalEmissionSent = safeAddInt64(totalEmissionSent, unClaimableInternal)
}

err := s.store.SetTotalEmissionSent(totalEmissionSent)
if err != nil {
panic(err)
}

deposits := s.getDeposits()
deposits.set(positionId, deposit)

if unClaimableInternal > 0 {
gns.Transfer(cross, communityPoolAddr, unClaimableInternal)
}

return rewardToUser, rewardPenalty, toUserExternalReward, toUserExternalPenalty
return "0", "0", toUserExternalReward, toUserExternalPenalty
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Emit an event when the no-internal branch processes unclaimable rewards.

This branch can reset unclaimable reward state, update totalEmissionSent, persist the deposit, and transfer GNS to the community pool, but returns without emitting any event. Add a CollectReward-style event when unClaimableInternal > 0 so indexers can observe the state change and transfer. As per coding guidelines, “All state changes MUST emit events.”

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