-
Notifications
You must be signed in to change notification settings - Fork 21
Add OperatorStaking
, operator/stakers splitter and StakersRewardsRecipient
#216
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
base: feat/staking-1
Are you sure you want to change the base?
Conversation
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughIntroduces OperatorStaking (ERC4626 vault) integrating with ProtocolStaking and a new StakersRewardsRecipient for per-staker rewards accounting, cooldown-based unstaking, reward withdrawal, and restaking. Adds OpenZeppelin v4 PaymentSplitter dependency alias and import. Minor NatSpec comment added in ProtocolStaking. Tests added for staking, rewards, cooldown release, and restaking; removes describe.only in two suites. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant U as User
participant OS as OperatorStaking (ERC4626)
participant PS as ProtocolStaking
participant SRR as StakersRewardsRecipient
participant GRR as Global Rewards Recipient
participant T as ERC20 Token
rect rgb(235, 245, 255)
note over U,OS: Deposit flow
U->>OS: deposit(assets)
OS->>SRR: increaseReleasedRewards(U, shares, totalShares)
OS->>T: transferFrom(U, OS, assets)
OS->>T: approve(PS, assets)
OS->>PS: stake(assets)
OS-->>U: mint shares
end
rect rgb(240, 255, 240)
note over OS,SRR: Rewards pull/distribution
OS->>PS: globalRewardsRecipient()
PS-->>OS: GRR address
OS->>SRR: withdrawRewards(account, shares, totalShares)
SRR->>GRR: pull rewards (if any)
GRR-->>SRR: transfer rewards
SRR-->>account: transfer allocated rewards
end
rect rgb(255, 250, 235)
note over U,PS: Unstake with cooldown
U->>OS: withdraw(assets)
OS->>SRR: decreaseReleasedRewards(U, burnShares, totalShares)
OS-->>U: burn shares
OS->>PS: unstake(assets)
PS-->>OS: cooldown receipt
OS-->>U: record cooldown checkpoint
end
rect rgb(255, 240, 245)
note over U,PS: Release after cooldown
U->>OS: release(U)
OS->>PS: withdraw after cooldown(U)
PS-->>OS: transfer assets
OS-->>U: transfer assets
end
rect rgb(235, 255, 250)
note over U,OS: Restake rewards
U->>OS: restakeRewards()
OS->>SRR: withdrawRewards(U, shares, totalShares)
SRR-->>U: transfer rewards
U->>OS: deposit(rewardAssets)
OS->>PS: stake(rewardAssets)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~55 minutes Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
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. Comment |
OperatorStaking
and OperatorStakingRewarder
OperatorStaking
and OperatorStakingRewarder
08aebb9
to
37178c6
Compare
OperatorStaking
and OperatorStakingRewarder
OperatorStaking
, operator/stakers splitter and StakersRewardsRecipient
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.json
is excluded by!**/package-lock.json
📒 Files selected for processing (8)
contracts/finance/OperatorStaking.sol
(1 hunks)contracts/finance/ProtocolStaking.sol
(1 hunks)contracts/finance/StakersRewardsRecipient.sol
(1 hunks)contracts/mocks/import.sol
(1 hunks)package.json
(1 hunks)test/finance/OperatorStaking.test.ts
(1 hunks)test/finance/ProtocolOperatorRegistry.test.ts
(1 hunks)test/finance/ProtocolStaking.t.ts
(1 hunks)
function withdrawRewards(address account) public virtual { | ||
_protocolStaking.claimRewards(address(this)); // Transfer all rewards to global rewards recipient | ||
if (account == owner()) { | ||
// Withdraw operator rewards | ||
IPaymentSplitter globalRewardsRecipient_ = IPaymentSplitter(globalRewardsRecipient()); | ||
IERC20 stakingToken_ = IERC20(asset()); | ||
if (globalRewardsRecipient_.releasable(stakingToken_, owner()) > 0) { | ||
globalRewardsRecipient_.release(stakingToken_, owner()); | ||
} | ||
} else { | ||
// Withdraw staker rewards | ||
_stakersRewardsRecipient.withdrawRewards(account, balanceOf(account), totalSupply()); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Claim protocol rewards for stakers before distributing
withdrawRewards
only calls _protocolStaking.claimRewards
when account == owner()
. If a staker calls withdrawRewards
while fresh rewards are still held by ProtocolStaking
, we never pull those funds into the splitter, so _stakersRewardsRecipient.withdrawRewards
has nothing to release. In practice this means stakers are dependent on the operator (the owner) to withdraw first, otherwise their rewards remain stuck. Please add a claimRewards
before servicing staker withdrawals so the latest rewards are always available.
A minimal fix is to invoke _protocolStaking.claimRewards(address(this));
unconditionally at the top of withdrawRewards
.
🤖 Prompt for AI Agents
In contracts/finance/OperatorStaking.sol around lines 80 to 93, withdrawRewards
only calls _protocolStaking.claimRewards when account == owner(), leaving staker
withdrawals unable to include fresh protocol rewards; move or add an
unconditional call to _protocolStaking.claimRewards(address(this)); at the start
of withdrawRewards so rewards are always claimed into the contract before
branching to owner or staker distribution, keeping existing owner/staker logic
unchanged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When account is a staker, here is the flow: _stakersRewardsRecipient.withdrawRewards(..)
-> modifier pullRewards()
-> protocolStaking.claimRewards(..)
-> globalRewardsRecipient.release(..)
so staking tokens are back to StakersRewardsRecipient
, then transferred to staker with stakingToken().transfer(account, releasable)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify the pullRewards modifier and _pullRewards implementation in StakersRewardsRecipient
cat contracts/finance/StakersRewardsRecipient.sol
Length of output: 5205
@james-toussaint, you're absolutely right! I apologize for the incorrect analysis.
Looking at the StakersRewardsRecipient
contract, I can now confirm that the pullRewards
modifier (line 45) calls _pullRewards()
internally (line 113), which in turn invokes protocolStaking.claimRewards(address(_operatorStaking))
at line 115 before transferring the staker's share from the global rewards recipient.
The implementation correctly handles both paths:
- Operator path: Explicit
claimRewards
call inOperatorStaking.withdrawRewards
- Staker path:
claimRewards
triggered via thepullRewards
modifier inStakersRewardsRecipient.withdrawRewards
This is a clean architectural pattern where the rewards recipient contract encapsulates its own preconditions. Thank you for the clarification!
ฅ^•ﻌ•^ฅ
✏️ Learnings added
Learnt from: james-toussaint
PR: OpenZeppelin/openzeppelin-confidential-contracts#216
File: contracts/finance/OperatorStaking.sol:80-93
Timestamp: 2025-10-06T13:03:33.545Z
Learning: In contracts/finance/OperatorStaking.sol and StakersRewardsRecipient.sol, the withdrawRewards flow for stakers automatically claims protocol rewards via the pullRewards modifier in StakersRewardsRecipient, which internally calls _pullRewards() that invokes protocolStaking.claimRewards. This is a valid pattern where the called contract handles preconditions via modifiers, so there's no need for an explicit claimRewards call in OperatorStaking.withdrawRewards for stakers.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
Summary by CodeRabbit
New Features
Tests
Chores