Skip to content

Fix owneronly for settings#453

Merged
ianhe8x merged 4 commits into
developfrom
fix-owneronly
May 1, 2026
Merged

Fix owneronly for settings#453
ianhe8x merged 4 commits into
developfrom
fix-owneronly

Conversation

@ianhe8x
Copy link
Copy Markdown
Contributor

@ianhe8x ianhe8x commented Apr 23, 2026

This pull request introduces comprehensive blacklist checks across multiple smart contracts to enhance security and prevent blacklisted accounts from interacting with protocol functions. The main change is the systematic addition of _requireNotBlacklisted checks for all user-facing and sensitive methods in contracts related to consumers, rewards, and staking. This ensures that blacklisted addresses cannot perform actions such as deposits, withdrawals, claiming rewards, or managing controllers.

Blacklist Enforcement Across Contracts

  • Added _requireNotBlacklisted checks to all relevant functions in ConsumerHost, including controller management, deposits, withdrawals, and state channel interactions. This prevents both the caller and affected addresses from being blacklisted. [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]
  • Enforced blacklist checks in RewardsBooster for all user-facing methods, including booster management, reward collection, and authorization modifiers. Also added a new adminRemoveBoosterDeployment function for owner-controlled booster removal. [1] [2] [3] [4] [5] [6]
  • Integrated blacklist verification in RewardsDistributor for all reward distribution, claim, and agreement-related actions, ensuring neither consumers nor indexers can be blacklisted. [1] [2] [3] [4] [5] [6]
  • Added blacklist checks to all runner-related actions in RewardsPool, including labor accounting, reward collection, and batch operations. [1] [2] [3] [4] [5]
  • Updated RewardsStaking to inherit from SQParameter and enforce blacklist restrictions on staking changes and indexer registry events. [1] [2] [3] [4]

Summary by CodeRabbit

  • New Features

    • Wallet blacklist management: owners can mark wallets as blacklisted and queries emit updates.
    • Owner admin action to remove booster deployments for a specified account.
  • Updates

    • Blacklist enforcement applied across staking, rewards, state channels, and allocation flows to block blacklisted wallets from key actions.
    • Reward spending interface extended to include an explicit runner parameter.

ianhe8x added 2 commits April 14, 2026 16:05
Add a centralized wallet blacklist in Settings and gate the core reward, staking, booster, and channel paths through the shared Settings lookup. RewardsBooster also gains an owner-only accounting removal path so polluted booster balances can be corrected without moving tokens.

Constraint: RewardsBooster is close to the EIP-170 size limit, so blacklist reverts are intentionally reasonless and runner checks are not duplicated in spendQueryRewards.

Rejected: Per-contract blacklist state | would fragment emergency operations and make removals easy to miss.

Rejected: Token-transfer based booster repair | cleanup must correct polluted accounting without touching custody.

Confidence: high

Scope-risk: moderate

Directive: Keep blacklist authority centralized in Settings unless deployment governance changes.

Tested: yarn hardhat compile

Tested: yarn build:abi

Tested: yarn test test/RewardsBooster.test.ts test/Staking.test.ts test/RewardsDistributer.test.ts

Not-tested: full yarn test suite
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 73ae7ede-f1e6-479a-8b3d-348cd2124f24

📥 Commits

Reviewing files that changed from the base of the PR and between 32d271e and f48f5ea.

📒 Files selected for processing (1)
  • publish/mainnet.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • publish/mainnet.json

📝 Walkthrough

Walkthrough

Adds owner-managed wallet blacklist in Settings and a reusable SQParameter guard; enforces blacklist checks across channel, rewards, and staking contracts. Updates IRewardsBooster ABI/signature (adds _runner) and introduces an owner admin removal for booster deployments.

Changes

Cohort / File(s) Summary
Settings & Guard
contracts/Settings.sol, contracts/interfaces/ISettings.sol, contracts/utils/SQParameter.sol
Adds walletBlacklist storage, setWalletBlacklisted, setWalletBlacklistedBatch, isWalletBlacklisted, WalletBlacklistUpdated event, and _requireNotBlacklisted(settings, addr) guard.
State Channels & Consumer
contracts/StateChannel.sol, contracts/ConsumerHost.sol
Apply blacklist checks to channel lifecycle and consumer flows (open/extend/fund/checkpoint/terminate/respond/claim, approvals, deposits/withdrawals). Update calls to spendQueryRewards to pass channel indexer as new _runner arg.
Rewards: Booster, Distributor, Pool
contracts/RewardsBooster.sol, contracts/RewardsDistributor.sol, contracts/RewardsPool.sol, contracts/interfaces/IRewardsBooster.sol
Enforce blacklist guards on booster auth/migration, reward collection, era collection, and claiming flows. Add adminRemoveBoosterDeployment (owner) and change spendQueryRewards signature to include address _runner.
Staking & Allocation
contracts/Staking.sol, contracts/StakingManager.sol, contracts/StakingAllocation.sol, contracts/RewardsStaking.sol
Introduce SQParameter inheritance where applicable and add blacklist checks on runner, staker, and caller entrypoints (registration, delegation, redelegation, unbonding, allocation changes). Add unbond lock timestamp validation and minor slash accounting tweak.
ABIs & Publish Metadata
publish/ABI/RewardsBooster.json, publish/ABI/RewardsStaking.json, publish/ABI/Settings.json, publish/ABI/StakingAllocation.json, publish/ABI/StakingManager.json, publish/mainnet.json
Update ABIs for new functions/events and updated spendQueryRewards signature; add Parameter and WalletBlacklistUpdated event entries; refresh mainnet metadata hashes/timestamps.
Tests
test/RewardsBooster.test.ts, test/RewardsBooster_migration.test.ts, test/RewardsDistributer.test.ts, test/Staking.test.ts
Wire Settings into fixtures; add tests for blacklist management and enforcement; update tests to pass runner arg to spendQueryRewards and adapt migration/refund flows.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Contract as StateChannel / Staking / Rewards*
  participant Settings as Settings
  participant Booster as RewardsBooster
  participant Distributor as RewardsDistributor

  Client->>Contract: call external action (e.g., claim / fund / stake)
  Contract->>Settings: isWalletBlacklisted(msg.sender or target)?
  Settings-->>Contract: bool (not blacklisted)
  alt allowed
    Contract->>Distributor: perform reward/claim flows (may call)
    Distributor->>Settings: isWalletBlacklisted(runner / sender)?
    Distributor-->>Contract: proceed
    Contract->>Booster: spendQueryRewards(..., _runner)
    Booster->>Settings: isWalletBlacklisted(_runner)?
    Booster-->>Contract: reward spend result
    Contract-->>Client: success / token transfers
  else blocked
    Settings-->>Contract: revert (blocked)
    Contract-->>Client: revert
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I nibble checks with careful hops,

I guard the gates, I mend the locks,
Blacklisted fleas stay out the yard,
Contracts safe, the network's starred! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title 'Fix owneronly for settings' is vague and does not accurately represent the main change. The PR is primarily about adding comprehensive blacklist enforcement across multiple smart contracts, not about fixing owner-only modifiers for settings. Revise the title to reflect the primary objective, such as 'Add blacklist enforcement across protocol contracts' or 'Implement wallet blacklist restrictions across contract suite'.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 fix-owneronly

Comment @coderabbitai help to get the list of available commands and usage tips.

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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
contracts/ConsumerHost.sol (1)

382-387: ⚠️ Potential issue | 🔴 Critical

Don't blacklist-gate the claimed() settlement callback.

claimed() is invoked from StateChannel._finalize(), so this turns a later blacklist on the consumer into a permanent finalize failure for any already-open channel. That strands locked channel funds instead of just blocking new interactions.

Suggested fix
 function claimed(uint256 channelId, uint256 amount) external {
     require(msg.sender == settings.getContractAddress(SQContracts.StateChannel), 'G011');

     address consumer = channels[channelId];
-    _requireNotBlacklisted(settings, consumer);
     Consumer storage info = consumers[consumer];
     info.balance += amount;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/ConsumerHost.sol` around lines 382 - 387, The claimed() function
currently calls _requireNotBlacklisted(settings, consumer), which prevents
StateChannel._finalize() from completing for already-open channels; remove that
blacklist check so claimed() can always be executed by the StateChannel caller.
Update the claimed(uint256 channelId, uint256 amount) implementation to keep the
require(msg.sender == settings.getContractAddress(SQContracts.StateChannel),
'G011') guard but delete the _requireNotBlacklisted(settings, consumer) line (or
wrap blacklist logic only for new-interaction entrypoints), ensuring channel
finalization and Consumer storage updates still occur as before.
contracts/RewardsBooster.sol (2)

996-1007: ⚠️ Potential issue | 🟠 Major

Controller callers are still missing a blacklist check here.

collectAllocationReward() blocks a blacklisted _runner, but not a blacklisted controller. If the runner is clean and its controller is blacklisted, the controller can still collect rewards because only the subject address is checked before the RB005 authorization branch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/RewardsBooster.sol` around lines 996 - 1007, In
collectAllocationReward add a blacklist check for the controller before the
authorization require: call _requireNotBlacklisted(settings, controller)
(similar to the existing _requireNotBlacklisted(settings, _runner)) prior to the
require that compares msg.sender to _runner/controller/stakingAllocation so a
blacklisted controller cannot collect rewards; locate this in the
collectAllocationReward function where controller is assigned from
indexerRegistry.getController(_runner).

124-131: ⚠️ Potential issue | 🟠 Major

Blacklisted controllers can still act through consumerAuthorised.

This modifier only checks consumer. When msg.sender is a controller, a blacklisted controller can still call boostDeploymentFor() and swapBoosterDeployment() on behalf of an unblacklisted consumer.

Suggested fix
 modifier consumerAuthorised(address consumer) {
         _requireNotBlacklisted(settings, consumer);
+        _requireNotBlacklisted(settings, msg.sender);

         if (msg.sender != consumer) {
             bool isController = IConsumerRegistry(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/RewardsBooster.sol` around lines 124 - 131, The consumerAuthorised
modifier currently only checks that the consumer is not blacklisted via
_requireNotBlacklisted(settings, consumer) but allows a blacklisted controller
(msg.sender) to act for the consumer; update the modifier so that when
msg.sender != consumer and isController(...) returns true you also call
_requireNotBlacklisted(settings, msg.sender) (or otherwise reject blacklisted
controllers) before allowing the call—this will protect functions like
boostDeploymentFor and swapBoosterDeployment from being invoked by blacklisted
controllers while preserving valid controller behavior via
settings.getContractAddress(...) and IConsumerRegistry.isController.
🧹 Nitpick comments (2)
test/RewardsDistributer.test.ts (1)

642-646: Good regression check; consider adding unblock recovery assertion.

Optional: after blacklisting revert, set blacklist back to false and assert claim path succeeds when rewards exist. That guards against sticky-state regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/RewardsDistributer.test.ts` around lines 642 - 646, After asserting that
a blacklisted wallet cannot claim, un-blacklist the wallet by calling
settings.setWalletBlacklisted(delegator.address, false) and then perform the
positive claim path with
rewardsDistributor.connect(delegator).claim(runner.address), asserting it
succeeds (e.g., does not revert and optionally emits the expected event or
updates token/balance state); if the test requires rewards to exist first,
ensure you create/distribute rewards (via the existing distribution helper or
rewardsDistributor methods) before asserting the successful claim to guard
against sticky blacklist state regressions.
test/Staking.test.ts (1)

150-154: Test name overstates current coverage.

The case currently verifies only delegate. Either rename it to match scope or add at least one more staking action assertion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/Staking.test.ts` around lines 150 - 154, Test title is too broad for
what it asserts; either narrow the name or add another assertion for a different
staking action. Option A: rename the it(...) description from 'blacklisted
wallet can not use staking actions' to 'blacklisted wallet cannot delegate' to
match the single assertion that calls settings.setWalletBlacklisted(...) and
stakingManager.connect(delegator).delegate(...). Option B: keep the broader name
and add at least one more expect that a blacklisted account is reverted when
calling a different staking method (for example
stakingManager.connect(delegator).undelegate(...) or
stakingManager.connect(delegator).withdraw(...) depending on available API),
using the same pattern as the delegate assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@contracts/RewardsPool.sol`:
- Line 153: The blacklist checks currently call _requireNotBlacklisted(settings,
runner) but do not block a blacklisted msg.sender from delegating through a
non-blacklisted runner; update each occurrence (including the instances at the
same pattern around lines ~153, 215, 227, 241, 255) to enforce the blacklist for
both parties by calling _requireNotBlacklisted(settings, msg.sender) in addition
to _requireNotBlacklisted(settings, runner) (or replace with a new helper that
validates both addresses) so that both the caller (msg.sender) and the runner
are checked before proceeding.

In `@contracts/RewardsStaking.sol`:
- Around line 254-256: The applyStakeChange function currently only calls
_requireNotBlacklisted for staker but proceeds to modify runner state
(getRewardInfo, claimFrom, _updateTotalStakingAmount, onStakeUpdate); add a
blacklist gate for runner by calling _requireNotBlacklisted(settings, runner) at
the start of applyStakeChange (before any calls to getRewardInfo, claimFrom,
_updateTotalStakingAmount, or onStakeUpdate) so blacklisted runners cannot have
their reward/stake state progressed by this path.

In `@contracts/Staking.sol`:
- Around line 122-123: The expectedBalance invariant currently starts at 0 and
is only updated in transferDelegationTokens(), causing drift and possible
underflow in withdraw/slash; seed expectedBalance to the actual token balance
during the upgrade/migration and update it wherever tokens enter the contract
(not just transferDelegationTokens()), specifically add updates in the reward
restake path (handle inflows from StakingManager.claimForDelegate() and
subsequent addDelegation(...) calls) so every inflow increments expectedBalance
and every outflow (withdraw/slash) decrements it to keep the invariant
consistent.

In `@contracts/StakingManager.sol`:
- Around line 122-124: The added _requireNotBlacklisted(settings, _runner) at
the top of unstake() prevents IndexerRegistry from force-unregistering
blacklisted runners; change the guard so blacklisted checks only apply to the
self-service path: remove the top-level _requireNotBlacklisted(settings,
_runner) and instead call _requireNotBlacklisted(settings, _runner) only inside
the branch where msg.sender == _runner (the self-initiated unstake path),
leaving the msg.sender == IndexerRegistry branch untouched so protocol-driven
unregisters succeed.

---

Outside diff comments:
In `@contracts/ConsumerHost.sol`:
- Around line 382-387: The claimed() function currently calls
_requireNotBlacklisted(settings, consumer), which prevents
StateChannel._finalize() from completing for already-open channels; remove that
blacklist check so claimed() can always be executed by the StateChannel caller.
Update the claimed(uint256 channelId, uint256 amount) implementation to keep the
require(msg.sender == settings.getContractAddress(SQContracts.StateChannel),
'G011') guard but delete the _requireNotBlacklisted(settings, consumer) line (or
wrap blacklist logic only for new-interaction entrypoints), ensuring channel
finalization and Consumer storage updates still occur as before.

In `@contracts/RewardsBooster.sol`:
- Around line 996-1007: In collectAllocationReward add a blacklist check for the
controller before the authorization require: call
_requireNotBlacklisted(settings, controller) (similar to the existing
_requireNotBlacklisted(settings, _runner)) prior to the require that compares
msg.sender to _runner/controller/stakingAllocation so a blacklisted controller
cannot collect rewards; locate this in the collectAllocationReward function
where controller is assigned from indexerRegistry.getController(_runner).
- Around line 124-131: The consumerAuthorised modifier currently only checks
that the consumer is not blacklisted via _requireNotBlacklisted(settings,
consumer) but allows a blacklisted controller (msg.sender) to act for the
consumer; update the modifier so that when msg.sender != consumer and
isController(...) returns true you also call _requireNotBlacklisted(settings,
msg.sender) (or otherwise reject blacklisted controllers) before allowing the
call—this will protect functions like boostDeploymentFor and
swapBoosterDeployment from being invoked by blacklisted controllers while
preserving valid controller behavior via settings.getContractAddress(...) and
IConsumerRegistry.isController.

---

Nitpick comments:
In `@test/RewardsDistributer.test.ts`:
- Around line 642-646: After asserting that a blacklisted wallet cannot claim,
un-blacklist the wallet by calling
settings.setWalletBlacklisted(delegator.address, false) and then perform the
positive claim path with
rewardsDistributor.connect(delegator).claim(runner.address), asserting it
succeeds (e.g., does not revert and optionally emits the expected event or
updates token/balance state); if the test requires rewards to exist first,
ensure you create/distribute rewards (via the existing distribution helper or
rewardsDistributor methods) before asserting the successful claim to guard
against sticky blacklist state regressions.

In `@test/Staking.test.ts`:
- Around line 150-154: Test title is too broad for what it asserts; either
narrow the name or add another assertion for a different staking action. Option
A: rename the it(...) description from 'blacklisted wallet can not use staking
actions' to 'blacklisted wallet cannot delegate' to match the single assertion
that calls settings.setWalletBlacklisted(...) and
stakingManager.connect(delegator).delegate(...). Option B: keep the broader name
and add at least one more expect that a blacklisted account is reverted when
calling a different staking method (for example
stakingManager.connect(delegator).undelegate(...) or
stakingManager.connect(delegator).withdraw(...) depending on available API),
using the same pattern as the delegate assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8b43976e-aafe-4cd1-afa6-9d58ef025762

📥 Commits

Reviewing files that changed from the base of the PR and between 5b7ee64 and 124a0fa.

📒 Files selected for processing (24)
  • contracts/ConsumerHost.sol
  • contracts/RewardsBooster.sol
  • contracts/RewardsDistributor.sol
  • contracts/RewardsPool.sol
  • contracts/RewardsStaking.sol
  • contracts/Settings.sol
  • contracts/Staking.sol
  • contracts/StakingAllocation.sol
  • contracts/StakingManager.sol
  • contracts/StateChannel.sol
  • contracts/interfaces/IRewardsBooster.sol
  • contracts/interfaces/ISettings.sol
  • contracts/utils/SQParameter.sol
  • publish/ABI/RewardsBooster.json
  • publish/ABI/RewardsStaking.json
  • publish/ABI/Settings.json
  • publish/ABI/Staking.json
  • publish/ABI/StakingAllocation.json
  • publish/ABI/StakingManager.json
  • publish/mainnet.json
  • test/RewardsBooster.test.ts
  • test/RewardsBooster_migration.test.ts
  • test/RewardsDistributer.test.ts
  • test/Staking.test.ts

Comment on lines 337 to +339
function collectAndDistributeRewards(address runner) public {
_requireNotBlacklisted(settings, runner);

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

Avoid freezing distributor accounting for blacklisted runners.

These two functions are bookkeeping entrypoints, not runner-authenticated actions. Rejecting them on runner means accSQTPerStake never advances for that runner again, so non-blacklisted delegators can get stuck with already-earned rewards and unsettled stake changes.

Suggested fix
 function collectAndDistributeRewards(address runner) public {
-    _requireNotBlacklisted(settings, runner);
-
     // check current era is after lastClaimEra
     uint256 currentEra = _getCurrentEra();
     require(info[runner].lastClaimEra < currentEra - 1, 'RD003');
     collectAndDistributeEraRewards(currentEra, runner);
 }
@@
 function collectAndDistributeEraRewards(
     uint256 currentEra,
     address runner
 ) public returns (uint256) {
-    _requireNotBlacklisted(settings, runner);
-
     RewardInfo storage rewardInfo = info[runner];
     require(rewardInfo.lastClaimEra > 0, 'RD004');

Also applies to: 351-356

Comment thread contracts/RewardsPool.sol
* @param amount the labor of services
*/
function labor(bytes32 deploymentId, address runner, uint256 amount) external {
_requireNotBlacklisted(settings, runner);
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

Blacklist enforcement is incomplete: caller bypass remains possible.

These paths only gate runner; they do not gate msg.sender. A blacklisted account can still interact by calling with a non-blacklisted runner.

Suggested patch
 function labor(bytes32 deploymentId, address runner, uint256 amount) external {
+    _requireNotBlacklisted(settings, msg.sender);
     _requireNotBlacklisted(settings, runner);
     ...
 }

 function collect(bytes32 deploymentId, address runner) external {
+    _requireNotBlacklisted(settings, msg.sender);
     _requireNotBlacklisted(settings, runner);
     ...
 }

 function batchCollect(address runner) external {
+    _requireNotBlacklisted(settings, msg.sender);
     _requireNotBlacklisted(settings, runner);
     ...
 }

 function collectEra(uint256 era, bytes32 deploymentId, address runner) external {
+    _requireNotBlacklisted(settings, msg.sender);
     _requireNotBlacklisted(settings, runner);
     ...
 }

 function batchCollectEra(uint256 era, address runner) external {
+    _requireNotBlacklisted(settings, msg.sender);
     _requireNotBlacklisted(settings, runner);
     ...
 }

Also applies to: 215-215, 227-227, 241-241, 255-255

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/RewardsPool.sol` at line 153, The blacklist checks currently call
_requireNotBlacklisted(settings, runner) but do not block a blacklisted
msg.sender from delegating through a non-blacklisted runner; update each
occurrence (including the instances at the same pattern around lines ~153, 215,
227, 241, 255) to enforce the blacklist for both parties by calling
_requireNotBlacklisted(settings, msg.sender) in addition to
_requireNotBlacklisted(settings, runner) (or replace with a new helper that
validates both addresses) so that both the caller (msg.sender) and the runner
are checked before proceeding.

Comment on lines 254 to +256
function applyStakeChange(address runner, address staker) external {
_requireNotBlacklisted(settings, staker);

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

Also blacklist-gate runner in applyStakeChange().

This path still settles runner state (getRewardInfo(runner), claimFrom(runner, staker), _updateTotalStakingAmount(..., runner, ...), onStakeUpdate(runner)), but only staker is checked. A runner blacklisted after queuing a stake change can still have rewards/stake state progressed by any caller.

Suggested fix
 function applyStakeChange(address runner, address staker) external {
+        _requireNotBlacklisted(settings, runner);
         _requireNotBlacklisted(settings, staker);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/RewardsStaking.sol` around lines 254 - 256, The applyStakeChange
function currently only calls _requireNotBlacklisted for staker but proceeds to
modify runner state (getRewardInfo, claimFrom, _updateTotalStakingAmount,
onStakeUpdate); add a blacklist gate for runner by calling
_requireNotBlacklisted(settings, runner) at the start of applyStakeChange
(before any calls to getRewardInfo, claimFrom, _updateTotalStakingAmount, or
onStakeUpdate) so blacklisted runners cannot have their reward/stake state
progressed by this path.

Comment thread contracts/Staking.sol Outdated
Comment on lines +122 to +123
// Expected token balance: tracks actual transfers in/out for balance invariant check
uint256 public expectedBalance;
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 | 🔴 Critical

expectedBalance will drift from the real token balance.

This new invariant starts at 0, so an on-chain upgrade leaves all pre-existing stake unaccounted for. It also only increments on transferDelegationTokens(), but Staking receives additional inflows via reward restaking in contracts/StakingManager.sol Lines 205-211 (claimForDelegate() followed by addDelegation(...)). After either case, the next expectedBalance -= amount in withdraw/slash can underflow or hit S023 even though the tokens are actually there.

Seed expectedBalance during the upgrade and update it on every inflow path, not just direct deposits.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/Staking.sol` around lines 122 - 123, The expectedBalance invariant
currently starts at 0 and is only updated in transferDelegationTokens(), causing
drift and possible underflow in withdraw/slash; seed expectedBalance to the
actual token balance during the upgrade/migration and update it wherever tokens
enter the contract (not just transferDelegationTokens()), specifically add
updates in the reward restake path (handle inflows from
StakingManager.claimForDelegate() and subsequent addDelegation(...) calls) so
every inflow increments expectedBalance and every outflow (withdraw/slash)
decrements it to keep the invariant consistent.

Comment on lines 122 to +124
function unstake(address _runner, uint256 _amount) external {
_requireNotBlacklisted(settings, _runner);

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

Don't block the IndexerRegistry unregister path here.

Per the existing function contract, IndexerRegistry calls unstake() to remove a runner completely. With the new guard at the top, a blacklisted runner can no longer be force-unregistered because the registry call reverts before the msg.sender == IndexerRegistry branch is reached.

A safer pattern is to keep the blacklist check only on the self-service branch, so blacklisted runners cannot call unstake() themselves while the protocol can still clean them up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/StakingManager.sol` around lines 122 - 124, The added
_requireNotBlacklisted(settings, _runner) at the top of unstake() prevents
IndexerRegistry from force-unregistering blacklisted runners; change the guard
so blacklisted checks only apply to the self-service path: remove the top-level
_requireNotBlacklisted(settings, _runner) and instead call
_requireNotBlacklisted(settings, _runner) only inside the branch where
msg.sender == _runner (the self-initiated unstake path), leaving the msg.sender
== IndexerRegistry branch untouched so protocol-driven unregisters succeed.

Comment on lines +720 to +723
function _requireChannelWalletsNotBlacklisted(uint256 channelId) private view {
_requireNotBlacklisted(settings, channels[channelId].indexer);
_requireNotBlacklisted(settings, channels[channelId].consumer);
}
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 | 🔴 Critical

This helper is too coarse for channel settlement paths.

Because claim, respond, terminate, and checkpoint all rely on this, blacklisting either stored channel wallet can leave an already-open channel impossible to advance or finalize. Also, for contract consumers this checks only channels[channelId].consumer (the wrapper contract), not the resolved wallet from channelConsumer(channelId).

@ianhe8x ianhe8x merged commit 2bb43d5 into develop May 1, 2026
2 checks passed
@ianhe8x ianhe8x deleted the fix-owneronly branch May 1, 2026 01:58
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