Skip to content

feat: implement stBlend ERC4626 vault, staking gateway#96

Open
chillhacker wants to merge 2 commits into
mainfrom
feat/stblend
Open

feat: implement stBlend ERC4626 vault, staking gateway#96
chillhacker wants to merge 2 commits into
mainfrom
feat/stblend

Conversation

@chillhacker

@chillhacker chillhacker commented May 12, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

Release Notes

  • New Features
    • Cross-chain staking gateway supporting both L1 escrow and L2 canonical vault operations, including share bridging
    • New stBlend streaming ERC-4626 vault with reward-rate/periodFinish mechanics, pausing, TVL caps, and EIP-712 signature deposits/mints
    • New RewardPool to schedule daily rewards into stBlend
    • New bridged mirror token for canonical vault shares across chains
  • Documentation
    • Added omni liquid staking system architecture guide and flow diagrams
  • Tests
    • Added end-to-end gateway, vault, and reward pool test coverage

@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a comprehensive omni liquid staking system supporting L1→L2 cross-chain staking with canonical shares and L1 mirror tokens. The implementation includes stBlend (ERC-4626 vault with streaming rewards), RewardPool (reward distribution helper), StakedTokenMirror (bridged share representation), StakingGateway (dual-mode bridge controller), and extensive tests validating all flows and edge cases.

Changes

Omni Liquid Staking Implementation

Layer / File(s) Summary
Interface Contracts
contracts/interfaces/IstBlend.sol, contracts/interfaces/gateways/IStakingGateway.sol, contracts/interfaces/IRewardPool.sol
Defines IstBlend with streaming-reward views, permit constants, and mutators; IStakingGateway with config views, admin functions, and cross-chain bridging entrypoints; IRewardPool with vault reference, distribution configuration, and fund/distribute operations. All include custom errors and event declarations.
stBlend Vault Implementation
contracts/stBlend/stBlend.sol
Upgradeable ERC-4626 vault with ERC-7201 storage, supporting streaming reward amortization via notifyRewards, TVL cap enforcement via maxTotalAssets, pause/unpause controls, EIP-712 staking permits (depositWithSig/mintWithSig with separate nonce tracking), role-gated access (admin/pauser/distributor), and UUPS upgrade authorization.
RewardPool Reward Distribution
contracts/stBlend/RewardPool.sol
Upgradeable contract managing external reward distribution into stBlend via configurable daily amounts and distribution periods. Enforces minimum elapsed time between distributions, verifies pool balance sufficiency, force-approves the vault, calls notifyRewards, and emits streaming state updates. Includes admin configuration and UUPS upgrade with role restrictions.
StakedTokenMirror Token
contracts/tokens/StakedTokenMirror.sol
Upgradeable ERC20 with custom decimals storage (ERC-7201 slot), owner-gated mint/burn operations, pause/unpause controls, and paused-state transfer blocking via _update override. Represents L1 mirror shares for canonical L2 vault shares.
StakingGateway Bridge
contracts/gateways/StakingGateway.sol
FluentBridge staking gateway with L1 escrow/L2 canonical modes, L1→L2 deposit-and-stake flow (escrowing underlying, bridging mint messages), L2→L1 redemption flow (redeeming shares, bridging withdrawal), native canonical-share bridging in both directions (locking/unlocking on L2, minting/burning mirror shares on L1), and mode-dependent configuration validation.
Deployment & Documentation
scripts/deploy/DeployStBlend.s.sol, scripts/deploy/DeployRewardPool.s.sol, docs/OmniLiquidStaking.md
Foundry scripts deploying stBlend and RewardPool via UUPS proxies with env-driven parameters; architecture doc describing L1-source/L2-canonical design, cross-chain flows via sequence diagrams, and operational constraints (yield source, escrow/inventory dependencies, trust assumptions).
stBlend Comprehensive Tests
test/staking/stBlend.t.sol
Full test coverage: initialization and role grants, ERC-4626 mutator accounting, streaming reward mechanics (rate/periodFinish updates, linear decay, residual carry-forward, dust handling), TVL cap clamping and cap-exceeded reverts, pause/unpause gating, EIP-712 permit signature flows (nonce separation, expiry, replay/signer validation), admin setters, and UUPS upgrade authorization.
RewardPool Tests
test/staking/RewardPool.t.sol
Test coverage: initialization with role/config wiring, distribution streaming into vault with rate/period/undistributed tracking, daily fund intake and distribution cooldown enforcement, balance sufficiency, vault role authorization, admin configuration, token recovery, and UUPS upgrade.
StakingGateway Tests
test/Gateway/StakingGateway.t.sol
Mode/config validation, mode-enforcement reverts (L1 functions on L2 and vice versa), L1→L2 deposit-and-stake escrow/message emission and L2 receipt with vault inventory, L2→L1 redemption and L1 escrow release to recipient, native share bridging in both directions with mirror minting/burning and canonical-share locking/unlocking, and input validation (zero amounts, zero recipients, zero shares).

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant L1Gateway as L1 StakingGateway
  participant FluentBridge
  participant L2Gateway as L2 StakingGateway
  participant stBlend as stBlend Vault

  User->>L1Gateway: depositAndStake(assets, l2Receiver)
  L1Gateway->>L1Gateway: escrow underlying
  L1Gateway->>FluentBridge: bridge message (receiveDepositAndStake)
  FluentBridge->>L2Gateway: relay receiveDepositAndStake
  L2Gateway->>stBlend: deposit(inventory, shares)
  stBlend-->>L2Gateway: shares minted
  L2Gateway-->>L2Gateway: track shares for receiver
Loading
sequenceDiagram
  participant L2User as L2 User (stBlend holder)
  participant L2Gateway as L2 StakingGateway
  participant stBlend as stBlend Vault
  participant FluentBridge
  participant L1Gateway as L1 StakingGateway
  participant L1Recipient

  L2User->>L2Gateway: redeemToL1(shares, l1Receiver)
  L2Gateway->>L2Gateway: transfer in shares
  L2Gateway->>stBlend: redeem(shares, assets)
  stBlend-->>L2Gateway: underlying released
  L2Gateway->>FluentBridge: bridge message (receiveUnderlyingWithdrawal)
  FluentBridge->>L1Gateway: relay receiveUnderlyingWithdrawal
  L1Gateway->>L1Gateway: consume escrow limit
  L1Gateway->>L1Recipient: transfer underlying
Loading
sequenceDiagram
  participant RewardAdmin
  participant RewardPool
  participant stBlend as stBlend Vault

  RewardAdmin->>RewardPool: fund(rewardAmount)
  RewardPool->>RewardPool: accumulate balance
  Note over RewardPool: Wait for distribution cooldown
  RewardAdmin->>RewardPool: distribute()
  RewardPool->>RewardPool: verify balance & timing
  RewardPool->>stBlend: notifyRewards(dailyAmount)
  stBlend->>stBlend: compute rate, set periodFinish
  stBlend-->>RewardPool: RewardsNotified event
  RewardPool->>RewardPool: update lastDistributionTime
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 A vault that streams rewards so sweet,
L1 escrows, L2 shares complete,
RewardPool feeds the fountain fair,
Mirror tokens bridge with care,
Permits signed for gasless treat!

🚥 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 title clearly and accurately summarizes the main changes in the changeset: implementing an ERC4626 vault (stBlend) and a staking gateway, which are the primary additions across multiple new contract files and tests.
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 feat/stblend

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
test/staking/stBlend.t.sol (1)

602-603: ⚡ Quick win

Make replay-path reverts explicit instead of generic.

Using bare vm.expectRevert() here can mask unrelated failures. Assert the exact selector (e.g., IstBlendErrors.InvalidSigner) so replay protection tests only pass for the intended reason.

Suggested diff
-        vm.expectRevert(); // recovered signer will not match alice — InvalidSigner
+        vm.expectRevert(IstBlendErrors.InvalidSigner.selector);
         vault.depositWithSig(1e18, alice, alice, deadline, v, r, s);
...
-        vm.expectRevert();
+        vm.expectRevert(IstBlendErrors.InvalidSigner.selector);
         vault.mintWithSig(1e18, alice, alice, deadline, v, r, s);

Also applies to: 661-662

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/staking/stBlend.t.sol` around lines 602 - 603, The tests use a bare
vm.expectRevert() which can hide unrelated failures; replace the generic
expectRevert with an explicit expectRevert referencing the exact revert selector
(e.g., IstBlendErrors.InvalidSigner) so the replay-path failure is asserted
precisely. Update the calls around vault.depositWithSig(1e18, alice, alice,
deadline, v, r, s) and the similar assertion at the other location to use
vm.expectRevert(IstBlendErrors.InvalidSigner.selector) (or the correct selector
constant) before invoking vault.depositWithSig so the test only passes for the
intended InvalidSigner revert.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contracts/gateways/StakingGateway.sol`:
- Around line 285-294: Revoke the previous vault's allowance on the old
underlying token before updating the staking config: read StakingGatewayStorage
via _getStorage(), capture the current $._vault and $._underlying into locals,
and if the old vault/address is non-zero and differs from the new config
(oldVault != vault or oldUnderlying != underlying), call
IERC20(oldUnderlying).forceApprove(oldVault, 0) to clear the stale approval;
then continue to emit StakingConfigUpdated and assign $._underlying, $._vault,
$._mirrorToken, $._isL2Canonical and finally set the new approval with
IERC20(underlying).forceApprove(vault, type(uint256).max) as before.

In `@scripts/deploy/DeployStBlend.s.sol`:
- Around line 62-63: Before casting STREAM_DURATION to uint64 in the
streamDuration assignment, read the raw value from vm.envUint("STREAM_DURATION")
into a local (e.g., rawStreamDuration) and validate that rawStreamDuration <=
type(uint64).max; if it exceeds the max, revert or vm.expect to fail with a
clear message, then safely cast to uint64 for streamDuration. Update the code
around the streamDuration assignment in DeployStBlend.s.sol to perform this
bounds check (use vm.envUint("STREAM_DURATION") as the source and streamDuration
as the final uint64 value).

---

Nitpick comments:
In `@test/staking/stBlend.t.sol`:
- Around line 602-603: The tests use a bare vm.expectRevert() which can hide
unrelated failures; replace the generic expectRevert with an explicit
expectRevert referencing the exact revert selector (e.g.,
IstBlendErrors.InvalidSigner) so the replay-path failure is asserted precisely.
Update the calls around vault.depositWithSig(1e18, alice, alice, deadline, v, r,
s) and the similar assertion at the other location to use
vm.expectRevert(IstBlendErrors.InvalidSigner.selector) (or the correct selector
constant) before invoking vault.depositWithSig so the test only passes for the
intended InvalidSigner revert.
🪄 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: 2c3ea846-6108-40f9-a1de-0fc921f4a5a6

📥 Commits

Reviewing files that changed from the base of the PR and between 9421d48 and dd1974b.

📒 Files selected for processing (9)
  • contracts/gateways/StakingGateway.sol
  • contracts/interfaces/IstBlend.sol
  • contracts/interfaces/gateways/IStakingGateway.sol
  • contracts/stBlend/stBlend.sol
  • contracts/tokens/StakedTokenMirror.sol
  • docs/OmniLiquidStaking.md
  • scripts/deploy/DeployStBlend.s.sol
  • test/Gateway/StakingGateway.t.sol
  • test/staking/stBlend.t.sol

Comment on lines +285 to +294
StakingGatewayStorage storage $ = _getStorage();
emit StakingConfigUpdated($._underlying, underlying, $._vault, vault, $._mirrorToken, mirrorToken, isL2Canonical_);
$._underlying = underlying;
$._vault = vault;
$._mirrorToken = mirrorToken;
$._isL2Canonical = isL2Canonical_;

if (isL2Canonical_) {
IERC20(underlying).forceApprove(vault, type(uint256).max);
}

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 | ⚡ Quick win

Revoke old vault allowance before setting a new staking config.

When config changes, the previous vault can keep unlimited allowance and still pull underlying from this gateway. Revoke stale approval before applying the new config.

🔒 Proposed fix
     function _setStakingConfig(address underlying, address vault, address mirrorToken, bool isL2Canonical_) internal {
         require(underlying != address(0), ZeroAddressNotAllowed("underlying"));
@@
         StakingGatewayStorage storage $ = _getStorage();
+        address prevUnderlying = $._underlying;
+        address prevVault = $._vault;
+
+        if (prevUnderlying != address(0) && prevVault != address(0)) {
+            // Revoke stale approval whenever config changes away from previous vault mode/path.
+            if (prevUnderlying != underlying || prevVault != vault || !isL2Canonical_) {
+                IERC20(prevUnderlying).forceApprove(prevVault, 0);
+            }
+        }
+
         emit StakingConfigUpdated($._underlying, underlying, $._vault, vault, $._mirrorToken, mirrorToken, isL2Canonical_);
         $._underlying = underlying;
         $._vault = vault;
         $._mirrorToken = mirrorToken;
         $._isL2Canonical = isL2Canonical_;
📝 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
StakingGatewayStorage storage $ = _getStorage();
emit StakingConfigUpdated($._underlying, underlying, $._vault, vault, $._mirrorToken, mirrorToken, isL2Canonical_);
$._underlying = underlying;
$._vault = vault;
$._mirrorToken = mirrorToken;
$._isL2Canonical = isL2Canonical_;
if (isL2Canonical_) {
IERC20(underlying).forceApprove(vault, type(uint256).max);
}
StakingGatewayStorage storage $ = _getStorage();
address prevUnderlying = $._underlying;
address prevVault = $._vault;
if (prevUnderlying != address(0) && prevVault != address(0)) {
// Revoke stale approval whenever config changes away from previous vault mode/path.
if (prevUnderlying != underlying || prevVault != vault || !isL2Canonical_) {
IERC20(prevUnderlying).forceApprove(prevVault, 0);
}
}
emit StakingConfigUpdated($._underlying, underlying, $._vault, vault, $._mirrorToken, mirrorToken, isL2Canonical_);
$._underlying = underlying;
$._vault = vault;
$._mirrorToken = mirrorToken;
$._isL2Canonical = isL2Canonical_;
if (isL2Canonical_) {
IERC20(underlying).forceApprove(vault, type(uint256).max);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/gateways/StakingGateway.sol` around lines 285 - 294, Revoke the
previous vault's allowance on the old underlying token before updating the
staking config: read StakingGatewayStorage via _getStorage(), capture the
current $._vault and $._underlying into locals, and if the old vault/address is
non-zero and differs from the new config (oldVault != vault or oldUnderlying !=
underlying), call IERC20(oldUnderlying).forceApprove(oldVault, 0) to clear the
stale approval; then continue to emit StakingConfigUpdated and assign
$._underlying, $._vault, $._mirrorToken, $._isL2Canonical and finally set the
new approval with IERC20(underlying).forceApprove(vault, type(uint256).max) as
before.

Comment on lines +62 to +63
streamDuration: uint64(vm.envUint("STREAM_DURATION")),
maxTotalAssets: vm.envUint("MAX_TOTAL_ASSETS")

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the narrowing cast exists and there is no uint64 max guard nearby.
rg -n -C2 'uint64\(vm\.envUint\("STREAM_DURATION"\)\)' scripts/deploy/DeployStBlend.s.sol
rg -n -C3 'STREAM_DURATION|type\(uint64\)\.max|overflow' scripts/deploy/DeployStBlend.s.sol

Repository: fluentlabs-xyz/solidity-contracts

Length of output: 1084


🏁 Script executed:

# Find the StBlendParams struct definition
rg -n "struct StBlendParams" --type solidity -A 15

# Check what maxTotalAssets should be (it's also on line 63)
rg -n "maxTotalAssets" scripts/deploy/DeployStBlend.s.sol -B 2 -A 2

# Look for any similar validation patterns in other deploy scripts
fd -e sol scripts/deploy/ | head -10

Repository: fluentlabs-xyz/solidity-contracts

Length of output: 1319


🏁 Script executed:

# Look at other deploy scripts in the directory
fd . 'scripts/deploy/' --type f -name '*.sol'

# Check if there are any validation patterns in this script or related files
rg -n "require.*MAX|require.*STREAM" scripts/deploy/ -A 1

# Check the overall structure of the deploy script
head -80 scripts/deploy/DeployStBlend.s.sol

Repository: fluentlabs-xyz/solidity-contracts

Length of output: 3214


🏁 Script executed:

# Check other deploy scripts to understand codebase patterns
find scripts/deploy -name "*.sol" -type f

# Look for similar downcast patterns in other deploy scripts
rg "uint64.*vm\.env|uint32.*vm\.env|uint16.*vm\.env" scripts/deploy/ -A 1 -B 1

Repository: fluentlabs-xyz/solidity-contracts

Length of output: 1100


Add bounds validation before downcast of STREAM_DURATION.

uint64(vm.envUint("STREAM_DURATION")) silently truncates values exceeding type(uint64).max, risking deployment with incorrect stream duration. Add an explicit bounds check before casting.

Suggested fix
+        uint256 streamDurationRaw = vm.envUint("STREAM_DURATION");
+        require(streamDurationRaw <= type(uint64).max, "STREAM_DURATION overflow");
+
         StBlendParams memory p = StBlendParams({
             asset: vm.envAddress("ASSET_ADDRESS"),
             name: vm.envString("NAME"),
             symbol: vm.envString("SYMBOL"),
             admin: vm.envAddress("ADMIN"),
             pauser: vm.envAddress("PAUSER"),
             rewardsDistributor: vm.envAddress("REWARDS_DISTRIBUTOR"),
-            streamDuration: uint64(vm.envUint("STREAM_DURATION")),
+            streamDuration: uint64(streamDurationRaw),
             maxTotalAssets: vm.envUint("MAX_TOTAL_ASSETS")
         });
📝 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
streamDuration: uint64(vm.envUint("STREAM_DURATION")),
maxTotalAssets: vm.envUint("MAX_TOTAL_ASSETS")
uint256 streamDurationRaw = vm.envUint("STREAM_DURATION");
require(streamDurationRaw <= type(uint64).max, "STREAM_DURATION overflow");
StBlendParams memory p = StBlendParams({
asset: vm.envAddress("ASSET_ADDRESS"),
name: vm.envString("NAME"),
symbol: vm.envString("SYMBOL"),
admin: vm.envAddress("ADMIN"),
pauser: vm.envAddress("PAUSER"),
rewardsDistributor: vm.envAddress("REWARDS_DISTRIBUTOR"),
streamDuration: uint64(streamDurationRaw),
maxTotalAssets: vm.envUint("MAX_TOTAL_ASSETS")
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deploy/DeployStBlend.s.sol` around lines 62 - 63, Before casting
STREAM_DURATION to uint64 in the streamDuration assignment, read the raw value
from vm.envUint("STREAM_DURATION") into a local (e.g., rawStreamDuration) and
validate that rawStreamDuration <= type(uint64).max; if it exceeds the max,
revert or vm.expect to fail with a clear message, then safely cast to uint64 for
streamDuration. Update the code around the streamDuration assignment in
DeployStBlend.s.sol to perform this bounds check (use
vm.envUint("STREAM_DURATION") as the source and streamDuration as the final
uint64 value).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contracts/stBlend/RewardPool.sol`:
- Around line 86-98: The initialize function at line 86-98 and the corresponding
update function at lines 176-182 only validate that dailyRewardAmount_ is
non-zero, but do not enforce that it is sufficiently large relative to the
vault's current stream window. This allows configurations that later fail in
notifyRewards with RewardRateZero, halting distributions. Add validation in both
the initialize function (after the existing require statements checking
dailyRewardAmount_ != 0) and in the update function to ensure that
dailyRewardAmount is sufficient for the vault's stream window, preventing
undersized reward amounts from being configured in the first place.

In `@scripts/deploy/DeployRewardPool.s.sol`:
- Line 46: The environment variable reads for DISTRIBUTION_PERIOD and
STREAM_DURATION are being directly downcast to uint64 without bounds checking,
which silently truncates values exceeding type(uint64).max. Create helper
functions that explicitly check bounds before casting: for DISTRIBUTION_PERIOD,
read the value into a uint256, require that it does not exceed type(uint64).max
with a descriptive error message, then return the safe cast to uint64. Apply the
same pattern to STREAM_DURATION with an appropriate bounds check and error
message. This ensures invalid configuration values fail fast during deployment
rather than silently corrupting state.
🪄 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: dc9ede3d-58d3-4bc0-b83c-3a78594543a3

📥 Commits

Reviewing files that changed from the base of the PR and between dd1974b and 42dd6d6.

📒 Files selected for processing (4)
  • contracts/interfaces/IRewardPool.sol
  • contracts/stBlend/RewardPool.sol
  • scripts/deploy/DeployRewardPool.s.sol
  • test/staking/RewardPool.t.sol
✅ Files skipped from review due to trivial changes (1)
  • contracts/interfaces/IRewardPool.sol

Comment on lines +86 to +98
function initialize(
address admin_,
IstBlend vault_,
uint256 dailyRewardAmount_,
uint64 distributionPeriod_
) external initializer {
require(admin_ != address(0), ZeroAddressNotAllowed("admin"));
require(address(vault_) != address(0), ZeroAddressNotAllowed("vault"));
require(dailyRewardAmount_ != 0, ZeroAmount());
require(
distributionPeriod_ >= MIN_DISTRIBUTION_PERIOD && distributionPeriod_ <= MAX_DISTRIBUTION_PERIOD,
InvalidDistributionPeriod(distributionPeriod_, MIN_DISTRIBUTION_PERIOD, MAX_DISTRIBUTION_PERIOD)
);

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 | ⚡ Quick win

Enforce a minimum reward amount at configuration time to prevent distribution halts.

Line 94 and Line 177 only enforce non-zero values, so a too-small amount can be accepted and later fail at Line 166 (notifyRewards) with RewardRateZero, blocking scheduled distributions until reconfigured. Validate that configured dailyRewardAmount is sufficient for the vault’s current stream window when initializing and when updating the amount.

Also applies to: 152-167, 176-182

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/stBlend/RewardPool.sol` around lines 86 - 98, The initialize
function at line 86-98 and the corresponding update function at lines 176-182
only validate that dailyRewardAmount_ is non-zero, but do not enforce that it is
sufficiently large relative to the vault's current stream window. This allows
configurations that later fail in notifyRewards with RewardRateZero, halting
distributions. Add validation in both the initialize function (after the
existing require statements checking dailyRewardAmount_ != 0) and in the update
function to ensure that dailyRewardAmount is sufficient for the vault's stream
window, preventing undersized reward amounts from being configured in the first
place.

admin: vm.envAddress("ADMIN"),
vault: vm.envAddress("VAULT_ADDRESS"),
dailyRewardAmount: vm.envUint("DAILY_REWARD_AMOUNT"),
distributionPeriod: uint64(vm.envUint("DISTRIBUTION_PERIOD"))

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify similar unsafe env downcasts in deployment scripts.
rg -nP 'uint(8|16|32|64|128)\s*\(\s*vm\.envUint\(' scripts

Repository: fluentlabs-xyz/solidity-contracts

Length of output: 297


🏁 Script executed:

# Read the specific file to verify the exact code at line 46
cat -n scripts/deploy/DeployRewardPool.s.sol | head -60 | tail -20

Repository: fluentlabs-xyz/solidity-contracts

Length of output: 1068


🏁 Script executed:

# Check the Solidity version pragma
head -5 scripts/deploy/DeployRewardPool.s.sol

Repository: fluentlabs-xyz/solidity-contracts

Length of output: 259


🏁 Script executed:

# Check if there are existing guards or validation patterns for environment variables
rg -B2 -A2 'vm\.envUint' scripts/deploy/

Repository: fluentlabs-xyz/solidity-contracts

Length of output: 1720


🏁 Script executed:

# Check the second file mentioned in the grep results
cat -n scripts/deploy/DeployStBlend.s.sol | sed -n '58,66p'

Repository: fluentlabs-xyz/solidity-contracts

Length of output: 553


🏁 Script executed:

# Check the RewardPoolParams struct definition to see if there's validation
rg -B3 -A10 'struct RewardPoolParams'

Repository: fluentlabs-xyz/solidity-contracts

Length of output: 1038


🏁 Script executed:

# Check if there are any validation or overflow guards used elsewhere in deployment scripts
rg -B2 -A2 'require.*type\(uint' scripts/

Repository: fluentlabs-xyz/solidity-contracts

Length of output: 59


🏁 Script executed:

# Look at the actual initialization to see if there's any validation of the distributionPeriod after creation
cat -n scripts/deploy/DeployRewardPool.s.sol | sed -n '40,70p'

Repository: fluentlabs-xyz/solidity-contracts

Length of output: 1522


Guard the uint64 cast for DISTRIBUTION_PERIOD to prevent silent truncation.

At line 46, uint64(vm.envUint("DISTRIBUTION_PERIOD")) silently truncates if the environment variable exceeds type(uint64).max, leading to incorrect deployment configuration. In Solidity 0.8.30, explicit downcasts are unchecked. Add an explicit bounds check before casting.

This pattern also appears in scripts/deploy/DeployStBlend.s.sol:62 with STREAM_DURATION, so consider applying the same fix there.

Proposed fix
-            distributionPeriod: uint64(vm.envUint("DISTRIBUTION_PERIOD"))
+            distributionPeriod: _readDistributionPeriod()
         });
function _readDistributionPeriod() internal view returns (uint64) {
    uint256 raw = vm.envUint("DISTRIBUTION_PERIOD");
    require(raw <= type(uint64).max, "DISTRIBUTION_PERIOD overflows uint64");
    return uint64(raw);
}
📝 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
distributionPeriod: uint64(vm.envUint("DISTRIBUTION_PERIOD"))
distributionPeriod: _readDistributionPeriod()
});
// ... rest of deploy logic ...
}
function _readDistributionPeriod() internal view returns (uint64) {
uint256 raw = vm.envUint("DISTRIBUTION_PERIOD");
require(raw <= type(uint64).max, "DISTRIBUTION_PERIOD overflows uint64");
return uint64(raw);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deploy/DeployRewardPool.s.sol` at line 46, The environment variable
reads for DISTRIBUTION_PERIOD and STREAM_DURATION are being directly downcast to
uint64 without bounds checking, which silently truncates values exceeding
type(uint64).max. Create helper functions that explicitly check bounds before
casting: for DISTRIBUTION_PERIOD, read the value into a uint256, require that it
does not exceed type(uint64).max with a descriptive error message, then return
the safe cast to uint64. Apply the same pattern to STREAM_DURATION with an
appropriate bounds check and error message. This ensures invalid configuration
values fail fast during deployment rather than silently corrupting state.

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