feat: implement stBlend ERC4626 vault, staking gateway#96
Conversation
📝 WalkthroughWalkthroughThis 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. ChangesOmni Liquid Staking Implementation
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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
test/staking/stBlend.t.sol (1)
602-603: ⚡ Quick winMake 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
📒 Files selected for processing (9)
contracts/gateways/StakingGateway.solcontracts/interfaces/IstBlend.solcontracts/interfaces/gateways/IStakingGateway.solcontracts/stBlend/stBlend.solcontracts/tokens/StakedTokenMirror.soldocs/OmniLiquidStaking.mdscripts/deploy/DeployStBlend.s.soltest/Gateway/StakingGateway.t.soltest/staking/stBlend.t.sol
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| streamDuration: uint64(vm.envUint("STREAM_DURATION")), | ||
| maxTotalAssets: vm.envUint("MAX_TOTAL_ASSETS") |
There was a problem hiding this comment.
🧩 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.solRepository: 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 -10Repository: 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.solRepository: 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 1Repository: 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.
| 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).
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
contracts/interfaces/IRewardPool.solcontracts/stBlend/RewardPool.solscripts/deploy/DeployRewardPool.s.soltest/staking/RewardPool.t.sol
✅ Files skipped from review due to trivial changes (1)
- contracts/interfaces/IRewardPool.sol
| 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) | ||
| ); |
There was a problem hiding this comment.
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")) |
There was a problem hiding this comment.
🧩 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\(' scriptsRepository: 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 -20Repository: fluentlabs-xyz/solidity-contracts
Length of output: 1068
🏁 Script executed:
# Check the Solidity version pragma
head -5 scripts/deploy/DeployRewardPool.s.solRepository: 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.
| 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.
Summary by CodeRabbit
Release Notes
stBlendstreaming ERC-4626 vault with reward-rate/periodFinish mechanics, pausing, TVL caps, and EIP-712 signature deposits/mintsRewardPoolto schedule daily rewards intostBlend