Skip to content

[sdk]: introduce ERC-4626 vault#965

Merged
seunlanlege merged 2 commits into
mainfrom
seun/streaming-yield-vault
Jun 11, 2026
Merged

[sdk]: introduce ERC-4626 vault#965
seunlanlege merged 2 commits into
mainfrom
seun/streaming-yield-vault

Conversation

@seunlanlege

@seunlanlege seunlanlege commented Jun 11, 2026

Copy link
Copy Markdown
Member

Summary

Adds StreamingYieldVault — a standalone ERC-4626 vault to @hyperbridge/core where yield is supplied by the owner via periodic transfers (addYield) and recognized linearly over a fixed window rather than instantly. Built on OpenZeppelin's audited ERC4626, Ownable, and Pausable.

Motivating use case: a stablecoin issuer (e.g. cNGN) that today distributes yield to holders manually. With this vault, distribution becomes a single periodic transfer, holders get a composable yield-bearing share token, and yield is paid in the asset itself.

Design

  • Yield streaming. totalAssets() = balanceOf(this) - lockedYield, where the current tranche unlocks linearly over VEST (23h, tuned for a ~24h add cadence). This defeats yield sniping: because recognition is keyed on block.timestamp, a same-block deposit→withdraw sees an unchanged share price and captures nothing.
  • Owner-supplied yield + no-backlog guard. addYield is onlyOwner and reverts (YieldStillVesting) until the previous tranche has fully vested, so tranches never overlap and no yield is left permanently locked.
  • Pausable. The owner can pause() to freeze all share movement — transfers, deposits, and withdrawals — via a single _update chokepoint, for emergencies. Yield can still be added while paused; it simply can't be withdrawn until unpaused.
  • Inflation-attack hardening. Non-zero _decimalsOffset() (OZ virtual shares/assets); seed-and-burn recommended at deploy.
  • Underflow-safe ordering. addYield pulls funds before marking them locked, so a first tranche larger than current backing can't underflow totalAssets.
  • No reentrancy guard by design. The only external calls are transfer/transferFrom; OZ orders effects before interactions. Assumes a standard, non-rebasing, non-fee-on-transfer, non-hooked ERC-20 (documented as an invariant).

Tests

test/StreamingYieldVault.t.sol — 29 tests (incl. 2 fuzz), covering the known vuln classes:

  • First-depositor inflation/donation attack — victim keeps non-zero shares, attack is unprofitable, victim made whole (with and without seed).
  • Yield sniping — same-block sandwich captures nothing; one-block crossing captures only sub-1% dust; denied yield accrues to the long-term holder.
  • Vesting — no instant jump, linear unlock to full over VEST, monotonic (fuzz).
  • addYield guard — reverts while vesting, allowed at vestedAt(), no backlog across cycles.
  • Pausing — pause blocks transfers / deposits / withdrawals; unpause restores them; pause/unpause are owner-only; addYield still works while paused.
  • _update chokepoint — every path into _update is gated (mint, withdraw-by-assets, transferFrom), the gate sits at _update itself (even a zero-value transfer reverts while paused), paused() tracks state across re-pause cycles, and all paths flow as a no-op when unpaused.
  • Underflow / access control / validation — first-tranche-exceeds-backing, onlyOwner, zero-amount.
  • Rounding & ERC-4626 parity — deposit→redeem rounds in the vault's favour (fuzz), preview* matches realized, decimals offset applied.
Suite result: ok. 29 passed; 0 failed; 0 skipped

Notes

  • New files only: contracts/vaults/StreamingYieldVault.sol, test/StreamingYieldVault.t.sol, plus a forge-std remapping. No existing contracts touched.
  • The vault has no Hyperbridge/ISMP dependency; it lives in @hyperbridge/core as a reusable Solidity primitive.

Docs

Adds a developer guide at docs/content/developers/evm/streaming-yield-vault.mdx (registered in the EVM section nav) covering how streaming/vesting works, deployment with seed-and-burn, deposit/withdraw, addYield, pausing, a reference table, and security considerations. pnpm types:check passes.

@seunlanlege seunlanlege changed the title [sdk]: add StreamingYieldVault ERC-4626 vault with anti-snipe yield streaming [sdk]: introduce StreamingYieldVault ERC-4626 vault Jun 11, 2026
@seunlanlege seunlanlege changed the title [sdk]: introduce StreamingYieldVault ERC-4626 vault [sdk]: introduce ERC-4626 vault Jun 11, 2026
@seunlanlege seunlanlege force-pushed the seun/streaming-yield-vault branch 2 times, most recently from f9d6825 to 4e43ec9 Compare June 11, 2026 10:19
@seunlanlege seunlanlege force-pushed the seun/streaming-yield-vault branch from 4e43ec9 to 9f6df87 Compare June 11, 2026 10:22
@seunlanlege seunlanlege merged commit 85def4c into main Jun 11, 2026
1 check passed
@seunlanlege seunlanlege deleted the seun/streaming-yield-vault branch June 11, 2026 13:18
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.

2 participants