Skip to content

Automatically stake PNK rewards instead of transferring them #2099

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from

Conversation

jaybuidl
Copy link
Member

@jaybuidl jaybuidl commented Aug 20, 2025

PR-Codex overview

This PR introduces a new setStakeReward function to manage PNK rewards for jurors and updates the reward calculation process in multiple contracts. It also corrects token balance assertions in tests.

Detailed summary

  • Added setStakeReward function in ISortitionModule.sol to deposit PNK rewards.
  • Updated reward calculation comments from "Transfer the rewards" to "Compute the rewards" in KlerosCoreBase.sol and KlerosCoreUniversity.sol.
  • Adjusted token balance assertions in KlerosCore.t.sol.
  • Implemented reward handling logic in KlerosCoreBase.sol and KlerosCoreUniversity.sol to transfer rewards conditionally.
  • Added detailed comments for setStakeReward function in SortitionModuleBase.sol and SortitionModuleUniversity.sol.

✨ Ask PR-Codex anything about this PR by commenting with /codex {your question}

Summary by CodeRabbit

  • New Features
    • Juror PNK rewards can now be auto-staked into their court stake, increasing future staking power. If staking isn’t possible, rewards are paid out as tokens as before.
    • Fee rewards (ETH/ERC20) remain unchanged.
  • Tests
    • Updated test expectations to reflect staking-based reward distribution and resulting balance changes.

Copy link
Contributor

coderabbitai bot commented Aug 20, 2025

Walkthrough

Implements conditional auto-staking of PNK rewards via a new SortitionModule.setStakeReward API. KlerosCore reward execution now attempts to stake juror PNK rewards per court, falling back to direct token transfer if staking fails. Interfaces and university/base modules are updated accordingly, and tests adjust expected balances.

Changes

Cohort / File(s) Summary
Reward distribution flow
contracts/src/arbitration/KlerosCoreBase.sol, contracts/src/arbitration/university/KlerosCoreUniversity.sol
_executeRewards now: transfers fee reward; attempts sortitionModule.setStakeReward(account, courtID, pnkReward); on false, falls back to pinakion.safeTransfer. Accounting (sumPnkRewardPaid / sumFeeRewardPaid) unchanged.
Sortition module interface
contracts/src/arbitration/interfaces/ISortitionModule.sol
Adds external function setStakeReward(address, uint96, uint256) returns (bool). No other interface changes.
Sortition module base/university implementations
contracts/src/arbitration/SortitionModuleBase.sol, contracts/src/arbitration/university/SortitionModuleUniversity.sol
Implements setStakeReward: returns true on zero reward; false if current stake is zero; otherwise deposits reward into stake via internal _setStake. University adds external setStake(...) and internal _setStake(...) that update juror/court stakes and emit events.
Tests
contracts/test/foundry/KlerosCore.t.sol
Updates expected PNK balances to reflect staking path effects: core balance +1000 PNK; staker2 -1000 PNK (two assertions updated).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Core as KlerosCore(_executeRewards)
  participant Sortition as SortitionModule
  participant PNK as Pinakion Token

  Core->>PNK: Transfer fee reward (ETH/Token path unchanged)
  Note over Core: Compute pnkReward

  Core->>Sortition: setStakeReward(account, courtID, pnkReward)
  alt setStakeReward succeeds
    Note over Sortition,Core: PNK reward credited to juror stake
  else setStakeReward fails
    Core->>PNK: safeTransfer(account, pnkReward)
    Note over Core: Fallback direct PNK payout
  end

  Note over Core: sumFeeRewardPaid / sumPnkRewardPaid updated
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Suggested labels

Type: Feature🗿, Package: Contracts

Poem

A gavel taps—then hops my heart,
Rewards now plant where trials start.
If soil is set, I stake my prize;
If not, I carry home supplies.
Thump-thump my feet on freshly staked ground,
Justice in PNK—carrots all around! 🥕🐇

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/autostake-pnk-rewards

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai or @coderabbitai title anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@jaybuidl jaybuidl linked an issue Aug 20, 2025 that may be closed by this pull request
Copy link

Copy link
Contributor

@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

🧹 Nitpick comments (9)
contracts/src/arbitration/interfaces/ISortitionModule.sol (2)

32-33: Add NatSpec to document new reward-staking API and its return semantics

The new function is clear, but please document:

  • When it returns false (e.g., juror not staked in that court, or other preconditions).
  • That it must not create a new stake path (only top-up existing court stake).
  • Whether phase constraints apply (see my comment on SortitionModuleBase).

This helps downstream implementers and keeps interface behavior unambiguous.


32-33: Optional: encode phase behavior in the interface comment

If you adopt phase gating in setStakeReward, please reflect that in the interface comments so callers (Core) understand that return false may simply mean “not Staking phase,” and should fallback to transfer.

Would you like me to update the interface NatSpec if we merge the phase gate?

contracts/src/arbitration/SortitionModuleBase.sol (1)

309-318: Consider emitting a dedicated event for reward-based stake increases

Today, reward credits emit StakeSet only, making it hard to distinguish user-initiated staking from reward compounding in off-chain analytics. An optional, additive event (e.g., RewardStaked(address juror, uint96 courtID, uint256 reward)) helps indexers and dashboards.

contracts/src/arbitration/KlerosCoreBase.sol (1)

858-862: Avoid external call when pnkReward is zero, and rely on phase-gated setStakeReward

Two small tweaks:

  • Skip calling sortitionModule when pnkReward == 0 to save gas.
  • If you accept gating setStakeReward to Staking phase (see SortitionModuleBase note), this code remains correct: it will fall back to transfer when false.

Diff within this hunk:

-        // Stake the PNK reward if possible, by-passes delayed stakes and other checks usually done by validateStake()
-        if (!sortitionModule.setStakeReward(account, dispute.courtID, pnkReward)) {
-            pinakion.safeTransfer(account, pnkReward);
-        }
+        // Stake the PNK reward if possible (phase-gated in SortitionModule); otherwise transfer.
+        if (pnkReward != 0) {
+            if (!sortitionModule.setStakeReward(account, dispute.courtID, pnkReward)) {
+                pinakion.safeTransfer(account, pnkReward);
+            }
+        }

If you don’t adopt the phase gate, please confirm you’re comfortable with sum-tree mutations during Generating/Drawing potentially affecting ongoing draws in other disputes.

contracts/src/arbitration/university/KlerosCoreUniversity.sol (2)

850-857: Prefer a safe, unified fee payout instead of raw send()

University version still uses payable(account).send(feeReward) (unchecked boolean). Consider mirroring KlerosCoreBase’s _transferFeeToken pattern (SafeSend) to avoid silent failures and to standardize behavior across variants. If you want to keep University minimal, at least check the boolean and revert on failure.

Example minimal change:

-        if (round.feeToken == NATIVE_CURRENCY) {
-            // The dispute fees were paid in ETH
-            payable(account).send(feeReward);
-        } else {
-            // The dispute fees were paid in ERC20
-            round.feeToken.safeTransfer(account, feeReward);
-        }
+        if (round.feeToken == NATIVE_CURRENCY) {
+            require(payable(account).send(feeReward), "ETH fee payout failed");
+        } else {
+            round.feeToken.safeTransfer(account, feeReward);
+        }

858-862: Skip SortitionModule call for zero reward and rely on phase-gate behavior

Same gas/tidiness nit as base contract: avoid calling setStakeReward when pnkReward == 0; if setStakeReward is gated to Staking phase, fallback to transfer preserves behavior during Drawing/Generating.

-        if (!sortitionModule.setStakeReward(account, dispute.courtID, pnkReward)) {
-            pinakion.safeTransfer(account, pnkReward);
-        }
+        if (pnkReward != 0) {
+            if (!sortitionModule.setStakeReward(account, dispute.courtID, pnkReward)) {
+                pinakion.safeTransfer(account, pnkReward);
+            }
+        }
contracts/test/foundry/KlerosCore.t.sol (1)

2400-2403: Fix comments to reflect the new autostake behavior

The assertions are correct, but the inline comments contradict them:

  • Core balance comment says “Was 21500. 1000 was transferred to staker2” — here 1000 was autostaked (into staker2’s stake via core custody), not transferred to wallet.
  • staker2 comment says “thus -19k from the default” — the wallet is down by 20k (stake), the 1k reward is staked, not in wallet.

Proposed diff (comments only):

-        assertEq(pinakion.balanceOf(address(core)), 21500, "Wrong token balance of the core"); // Was 21500. 1000 was transferred to staker2
+        assertEq(pinakion.balanceOf(address(core)), 21500, "Wrong token balance of the core"); // 1000 PNK reward was autostaked (held by core)
...
-        assertEq(pinakion.balanceOf(staker2), 999999999999980000, "Wrong token balance of staker2"); // 20k stake and 1k added as a reward, thus -19k from the default
+        assertEq(pinakion.balanceOf(staker2), 999999999999980000, "Wrong token balance of staker2"); // 20k deposited as stake; 1k reward autostaked (not in wallet)
contracts/src/arbitration/university/SortitionModuleUniversity.sol (2)

220-226: _setStake entry point looks good; note minor gas/readability tweaks.

No functional issues spotted. Optional micro-tweaks you may consider:

  • Make the deposit/withdraw branches explicit: if (_pnkDeposit > 0) { ... } else if (_pnkWithdrawal > 0) { ... } to skip the no-op subtraction when both are zero.
  • In the cleanup loop that walks courtIDs in reverse, use unchecked decrement to save gas.
  • In the ancestor update, consider applying a single delta per court (±(new - current)) instead of two consecutive updates to reduce SSTOREs.

If helpful, I can draft a focused gas-optimized variant of the loop/update block.


206-218: setStakeReward behavior confirmed; optional O(1) lookup refactor and event emission recommended

  • Verified that in SortitionModuleUniversity.sol (lines 206–218) setStakeReward(address,uint96,uint256) is declared as
    external override onlyByCore returns (bool success) and uses the intended early-return logic:
    ● returns true when _reward == 0
    ● returns false when the juror’s current stake is zero, allowing the caller to fallback to a direct transfer
  • Checked KlerosCoreUniversity.sol (line 859) and KlerosCoreBase.sol (line 859) and confirmed that both core implementations handle a false return by calling pinakion.safeTransfer(account, pnkReward) as a fallback—ensuring no funds are lost when auto-staking isn’t possible
  • The method signature and caller expectations align correctly; no breaking changes detected

Optional improvements to consider:

  • Replace the O(n) scan in _stakeOf(_account,_courtID) with a direct storage lookup (e.g. jurors[_account].stakesByCourtID[_courtID]) to reduce gas in hot reward loops
  • Emit a dedicated RewardAutoStaked(address indexed account, uint96 indexed courtID, uint256 reward, uint256 newStake) event immediately after _setStake for easier off-chain indexing and auditing
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9895c7c and 0cadf21.

📒 Files selected for processing (6)
  • contracts/src/arbitration/KlerosCoreBase.sol (1 hunks)
  • contracts/src/arbitration/SortitionModuleBase.sol (1 hunks)
  • contracts/src/arbitration/interfaces/ISortitionModule.sol (1 hunks)
  • contracts/src/arbitration/university/KlerosCoreUniversity.sol (1 hunks)
  • contracts/src/arbitration/university/SortitionModuleUniversity.sol (1 hunks)
  • contracts/test/foundry/KlerosCore.t.sol (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: contracts-testing
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (4)
contracts/src/arbitration/KlerosCoreBase.sol (2)

849-854: Micro: reuse helper to compute rewards (already done) – looks good

The switch to _applyCoherence improves readability and reduces duplication. No action needed.


855-857: Uniform fee transfer helper: good call

Using _transferFeeToken unifies ETH/ERC20 payout logic and inherits SafeSend behavior. Nice.

contracts/src/arbitration/university/KlerosCoreUniversity.sol (1)

843-848: LGTM on reward computation

The reward math mirrors the base contract and maintains sumPnkRewardPaid/sumFeeRewardPaid accounting. Good.

contracts/src/arbitration/university/SortitionModuleUniversity.sol (1)

193-195: Good extraction: single source of truth for stake mutations via _setStake.

Routing setStake through the internal helper removes duplication and ensures the rewards path shares identical accounting side-effects and events. LGTM.

Comment on lines +319 to +331
address _account,
uint96 _courtID,
uint256 _reward
) external override onlyByCore returns (bool success) {
if (_reward == 0) return true; // No reward to add.

uint256 currentStake = stakeOf(_account, _courtID);
if (currentStake == 0) return false; // Juror has been unstaked, don't increase their stake.

uint256 newStake = currentStake + _reward;
_setStake(_account, _courtID, _reward, 0, newStake);
return true;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Do not mutate sortition trees during Generating/Drawing: gate setStakeReward by phase

setStakeReward calls _setStake directly, bypassing validateStake() and the phase guard that defers writes during non-staking phases. That diverges from the module’s invariant “sum trees are only updated in Staking phase,” and can alter draw probabilities mid-Drawing for unrelated disputes. This can lead to inconsistent sortition outcomes and hard-to-reason temporal coupling between execution and drawing.

Recommend returning false when phase != Phase.staking so KlerosCore falls back to transfer (as designed), preserving invariants and avoiding mid-phase mutations.

Diff within this hunk:

 function setStakeReward(
   address _account,
   uint96 _courtID,
   uint256 _reward
 ) external override onlyByCore returns (bool success) {
   if (_reward == 0) return true; // No reward to add.
 
+  // Preserve invariant: do not modify trees outside Staking phase.
+  if (phase != Phase.staking) {
+    return false;
+  }
+
   uint256 currentStake = stakeOf(_account, _courtID);
   if (currentStake == 0) return false; // Juror has been unstaked, don't increase their stake.
 
   uint256 newStake = currentStake + _reward;
   _setStake(_account, _courtID, _reward, 0, newStake);
   return true;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
address _account,
uint96 _courtID,
uint256 _reward
) external override onlyByCore returns (bool success) {
if (_reward == 0) return true; // No reward to add.
uint256 currentStake = stakeOf(_account, _courtID);
if (currentStake == 0) return false; // Juror has been unstaked, don't increase their stake.
uint256 newStake = currentStake + _reward;
_setStake(_account, _courtID, _reward, 0, newStake);
return true;
}
function setStakeReward(
address _account,
uint96 _courtID,
uint256 _reward
) external override onlyByCore returns (bool success) {
if (_reward == 0) return true; // No reward to add.
// Preserve invariant: do not modify trees outside Staking phase.
if (phase != Phase.staking) {
return false;
}
uint256 currentStake = stakeOf(_account, _courtID);
if (currentStake == 0) return false; // Juror has been unstaked, don't increase their stake.
uint256 newStake = currentStake + _reward;
_setStake(_account, _courtID, _reward, 0, newStake);
return true;
}
🤖 Prompt for AI Agents
In contracts/src/arbitration/SortitionModuleBase.sol around lines 319 to 331,
setStakeReward currently calls _setStake unconditionally which mutates sortition
trees during non-staking phases; change the function to check the current phase
and return false unless Phase.staking (or call validateStake() that enforces the
same guard) so that writes are deferred and KlerosCore will fall back to
transfer, avoiding mid-Drawing mutations and preserving the invariant that sum
trees are only updated in Staking phase.

Copy link

netlify bot commented Aug 20, 2025

Deploy Preview for kleros-v2-testnet-devtools ready!

Name Link
🔨 Latest commit 0cadf21
🔍 Latest deploy log https://app.netlify.com/projects/kleros-v2-testnet-devtools/deploys/68a62d049772670008950bc5
😎 Deploy Preview https://deploy-preview-2099--kleros-v2-testnet-devtools.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link

netlify bot commented Aug 20, 2025

Deploy Preview for kleros-v2-university failed. Why did it fail? →

Name Link
🔨 Latest commit 0cadf21
🔍 Latest deploy log https://app.netlify.com/projects/kleros-v2-university/deploys/68a62d040571910008f88d7f

Copy link

netlify bot commented Aug 20, 2025

Deploy Preview for kleros-v2-neo ready!

Name Link
🔨 Latest commit 0cadf21
🔍 Latest deploy log https://app.netlify.com/projects/kleros-v2-neo/deploys/68a62d04a5c6c50008564f1e
😎 Deploy Preview https://deploy-preview-2099--kleros-v2-neo.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link

netlify bot commented Aug 20, 2025

Deploy Preview for kleros-v2-testnet ready!

Name Link
🔨 Latest commit 0cadf21
🔍 Latest deploy log https://app.netlify.com/projects/kleros-v2-testnet/deploys/68a62d046041bb000891731f
😎 Deploy Preview https://deploy-preview-2099--kleros-v2-testnet.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

PNK Rewards Auto-Staking
1 participant