Skip to content

sNOTE

fact

PropertyValue
StandardERC-4626 vault over NOTE, 18 decimals, _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
Share priceMonotonically non-decreasing

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. Combined with the withdrawal cooldown, the sandwich is uneconomic.

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.

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

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.

  1. requestWithdraw(shares): the shares are transferred into the vault and escrowed as a new request with its own 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. Requesting again adds shares and restarts the clock for the whole pending amount.
  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).

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.

FunctionWhoEvent
setRewardNotifier(addr)ownerRewardNotifierUpdated
setCooldown(seconds) (≤ 30 days)ownerCooldownUpdated
pause / unpauseguardian / ownerOZ Paused / Unpaused
  • 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.