sNOTE

Documentation snapshot 18 October 2026. Deployment · Audit Status
On this page

sNOTE represents a share of a NOTE staking vault. Rewards change its NOTE-denominated exchange rate; they do not fix the USDG market value of either token.

Summary

fact

fact Summary
PropertyValue
StandardERC-4626 vault over NOTE (18 decimals); shares have 24 decimals because _decimalsOffset() = 6 (virtual shares)
Reward sourceRevenueRouter only, via notifyReward(amount) after transferring NOTE
StreamingLinear over STREAM_DURATION = 7 days, re-based on each notify
WithdrawalrequestWithdraw(shares) then wait cooldown (constructor argument, MAX_COOLDOWN = 30 days) then redeem/withdraw with owner == msg.sender. requests(owner) returns the total pending shares and the latest unlockAt across all entries, which is the time from which every pending request is redeemable (a cooldown reduction can make earlier entries redeemable sooner; maxRedeem reflects that)
Share priceMonotonically non-decreasing

Why Streaming

If rewards were credited instantly, a large NOTE holder could deposit in the block before a buyback fill, capture the reward, and withdraw. Streaming vests each reward linearly over seven days so that a depositor earns in proportion to time staked. Streaming and the withdrawal cooldown constrain short-lived reward capture; they do not remove every trading or contract risk.

Accounting

fact Accounting Calculation

totalAssets() = NOTE.balanceOf(vault) − unvestedRewards() unvestedRewards() = streamAmount × (streamEnd − now) / (streamEnd − streamStart) for streamStart ≤ now < streamEnd = 0 for now ≥ streamEnd sharePrice = (totalAssets + 1) / (totalSupply + 10^6) (ERC-4626 with offset)

fact The cooldown is set at deployment. PROTOCOL_SPEC.md specifies a 3-day default; script/DeployToken.s.sol currently defaults SNOTE_COOLDOWN to 7 days unless overridden by environment. Governance can change it with setCooldown up to 30 days. The launch value is tbd.

On notifyReward(a) the router has already transferred a NOTE. The call reverts with RewardNotReceived if the vault balance does not cover the new stream. The vault sets streamAmount = unvestedRewards() + a, streamStart = now, streamEnd = now + 7 days. totalAssets is unchanged at the moment of the call, so the share price does not jump.

While totalSupply() == 0 a notified reward is not streamed: it is added to deferredRewards, excluded from totalAssets, and RewardDeferred(notifier, amount, deferredTotal) is emitted. The first deposit that restores supply starts a fresh 7 day stream of the deferred amount plus any unvested remainder (RewardNotified with notifier == sNOTE). When the last shares are redeemed mid-stream the unvested part is deferred the same way, so no reward vests to an empty vault.

Worked Example

illustrative Vault holds 1,000,000 NOTE against 1,000,000 shares (price 1.0). The router notifies 7,000 NOTE at t = 0.

illustrative Worked Example Reference
TimeUnvestedtotalAssetsShare price
t = 07,0001,000,0001.000000
t = 1 day6,0001,001,0001.001000
t = 3.5 days3,5001,003,5001.003500
t = 7 days01,007,0001.007000

At t = 3.5 days the router notifies a further 3,500 NOTE. Stream re-bases: streamAmount = 3,500 (unvested) + 3,500 = 7,000, vesting over the next 7 days from t = 3.5 d. totalAssets remains 1,003,500 at that instant and reaches 1,010,500 at t = 10.5 days.

Withdrawals

  1. requestWithdraw(shares): the shares are transferred into the vault and escrowed as a new, independent WithdrawRequest{shares, unlockAt} with unlockAt = now + cooldown (at most MAX_OPEN_REQUESTS = 8 open requests per account). A later request never delays an earlier one; redemptions consume matured requests oldest-first, and requests(owner) reports the total pending shares with the unlockAt of the newest request.
  2. After unlockAt, redeem(shares, receiver, owner) or withdraw(assets, receiver, owner) for up to the pending amount, at the share price at redemption time. Escrowed shares keep accruing.
  3. cancelWithdrawRequest() returns escrowed shares to the caller.

Standard ERC-4626 redeem/withdraw without a matured request revert (NoRequest, CooldownActive(unlockAt), ExceedsRequested); calling on behalf of another owner reverts with OwnerMustBeCaller, so ERC-4626 allowances are not honoured for exits. deposit and mint are unrestricted (when not paused). Pausing applies to entries only: requestWithdraw, cancelWithdrawRequest, withdraw and redeem are never paused, so a holder can always start the cooldown and leave once it has matured.

Inflation Attack Protection

The 10^6 virtual-share offset makes the classic first-depositor donation attack unprofitable: an attacker must donate a million times the victim’s deposit to steal one wei of value.

Governance Surface

fact Governance Surface Reference
FunctionWhoEvent
setRewardNotifier(addr)ownerRewardNotifierUpdated
setCooldown(seconds) (≤ 30 days)ownerCooldownUpdated
pause / unpauseguardian / ownerOZ Paused / Unpaused; stops deposit and mint only, exits stay open

What sNOTE Does Not Do

  • It does not mint NOTE. All rewards are NOTE already bought on the open auction with realised fees.
  • It does not rebase. Balances are constant; share price rises.
  • It does not lock beyond the cooldown. There are no lock tiers or boosted multipliers.
A little more clarity?

Keep exploring, or revisit this topic from the navigation.