Back to Home
Documentation v1.0

Get Started

mochiyeild Technical Specification

Time-aware fixed income markets on Uniswap v4 — PT/YT tokenization, maturity fee decay, implied rate guards, and cross-pool parity with Reactive Network integration.

Abstract

mochiyeild is a yield trading protocol built on Uniswap v4 that splits yield-bearing assets into Principal Tokens (PT) and Yield Tokens (YT). This separation enables investors to independently trade future yield while preserving principal exposure — a capability impossible with traditional vault mechanics.

The protocol's core innovation is MochiYieldHook, a single Uniswap v4 hook that enforces three layers of fixed-income market logic atomically: implied rate bounds, maturity-aware fee decay, and cross-pool PT/YT parity monitoring.

Beyond the hook, mochiyeild closes the loop with a Reactive Smart Contract that subscribes to ParityDriftDetected on the origin chain and triggers a cross-chain callback to an ArbitrageRouter — moving parity enforcement from passive monitoring toward autonomous correction.

Origin chainlive

MochiYieldHook

ParityDriftDetected

Reactive Lasnatestnet

MochiReactiveKeeper

react() · Callback

Origin chainpartial

ArbitrageRouter

restoreParity() · ParityRestored

Full PT/YT swap execution on callback is next — MVP records drift and emits events.

System overview — hook on origin chain, Reactive keeper on Lasna, callback back to ArbitrageRouter.

1. Tokenization Model

Users deposit yield-bearing assets (e.g., wstETH) into YieldVault. The vault mints two ERC-20 tokens against deposited value at a 1:1 ratio:

YieldVault.sol accepts the underlying via deposit(amount), mints PT + YT 1:1 against deposited value, and holds underlying until maturity. After maturity, redeem(ptAmount) burns PT and returns underlying 1:1. YT accrual stops at maturity and converges to zero — it is not redeemable for underlying through the vault.

  • Principal Token (PT) — Represents the underlying principal, redeemable at maturity. PT holders receive fixed-return exposure with capital protection.
  • Yield Token (YT) — Represents all future yield accrual until maturity. YT holders can trade yield exposure independently, capturing upside from yield rate movements.
  • deposit() — transfers underlying in, mints equal PT and YT to the depositor, emits Deposited(user, amount, ptMinted, ytMinted)
  • redeem() — post-maturity only; burns PT, returns underlying 1:1, emits Redeemed(user, ptBurned, underlyingReturned)
wstETHunderlyingdeposit()YieldVaultmint 1:1PT+YTUniswap v4 poolsPT/WETH · YT/WETHMochiYieldHook
Deposit → mint → trade PT and YT in separate v4 pools.

At maturity, 1 PT redeems for 1 unit of underlying. YT ceases to accrue yield and converges to zero. PT and YT always reconcile to the underlying — that invariant is what the hook monitors across separate pools.

2. Roles & Participants

mochiyeild serves three distinct participants in the same market structure:

RoleDescription
Alice · LPProvides liquidity to PT/WETH or YT/WETH pools. Earns maturity-scaled swap fees — higher far from expiry, lower near redemption.
Bob · Yield traderDeposits wstETH, sells PT, keeps YT. Gains leveraged yield exposure without selling the underlying.
Charlie · PT holderBuys and holds PT to maturity. Fixed-income exposure — redeems 1:1 for underlying at expiry.
Illustrative PT/YT convergence — normalized to underlying = 1.0. Shaded band marks yield earned so far.

LPs are compensated by fees that track time-to-maturity risk, not flat meme-coin pool rates.

3. Implied Rate Sentinel

The hook calculates implied APY from PT market price and time-to-maturity using simple annualized discount (matches MochiYieldHook.sol):

// ptPrice below par → positive yield
discount = 1e18 - ptPrice
impliedAPY(bps) = discount / ptPrice × (365 days / timeToMaturity) × 10000

// ptPrice ≥ 1e18 (PT at/above par) → negative yield branch
premium = ptPrice - 1e18
impliedAPY(bps) = -premium / ptPrice × (365 days / timeToMaturity) × 10000
  • MIN_IMPLIED_APY = 0 bps — negative yield triggers SwapRejectedNegativeYield and MarketStressDetected
  • MAX_IMPLIED_APY = 10000 bps (100%) — extreme rates trigger stress alerts
  • beforeSwap() calls _enforceImpliedRateBounds() on the current PT price and, for PT buys (!zeroForOne), on the limit price — reverting ImpliedRateOutOfBounds before the swap executes
  • afterSwap() recalculates implied APY for observability and emits ImpliedRateUpdated on every PT pool swap

Rate enforcement runs atomically inside the swap callback — no external oracle call required for bounds checking.

4. Maturity Fee Decay

Unlike generic dynamic fee hooks that respond to volume, mochiyeild's fees decay with time-to-maturity because volatility decreases as PT approaches redemption:

ttm ≥ 90 days  → 100 bps (1.00%)
ttm ≤ 7 days   →   5 bps (0.05%)
7 < ttm < 90   → linear: 100 - (95 × (90d - ttm) / 83d)

// Applied via v4 dynamic fee override: feeBps × 100 | 0x800000
  • Far from maturity (>90 days): 1.00% swap fee — premium for LP uncertainty
  • Near maturity (<7 days): 0.05% swap fee — PT is bond-like, risk is low
  • Between: smooth linear interpolation via calculateFeeForMaturity()

Today: 0.00%

Matches calculateFeeForMaturity() in MochiYieldHook.sol.

This aligns LP compensation with actual risk exposure over the yield curve.

5. Cross-Pool Parity Oracle

PT and YT trade in separate Uniswap v4 pools but must maintain parity with the underlying asset. After every swap, the hook updates:

combined = ptPrice + ytPrice
driftBps = |combined - underlying| / underlying × 10000

// emit ParityDriftDetected if driftBps > PARITY_DRIFT_THRESHOLD (500 = 5%)
  • lastPTPrice and lastYTPrice updated from pool slot0 after each swap
  • underlyingPrice is currently a settable assumption (setUnderlyingPrice) — oracle integration is future work
  • ParityDriftDetected(ptPrice, ytPrice, underlyingPrice, driftBps, isOverValued) enables Reactive Network subscription
Underlying1.00
PT + YT (combined)0.94 · 6.0% drift
PT YT
Alert threshold5%→ ParityDriftDetected
Alert fires when PT + YT drifts more than 5% from underlying.

When drift exceeds threshold, the hook emits ParityDriftDetected — the entry point for the cross-chain correction loop.

6. Hook Execution Lifecycle

Every swap through a registered pool passes through MochiYieldHook in two phases:

beforeSwapEnforce implied rate bounds on PT pool → compute maturity fee → emit FeeAdjustedForMaturity → return dynamic fee override
Swap executesUniswap v4 core executes at the overridden fee tier
afterSwapRead slot0 price → recompute implied APY → emit ImpliedRateUpdated / stress events → _updateParityState across PT + YT

beforeSwap()

Rate bounds + fee override

swap

v4 core executes

afterSwap()

APY update + parity check

emit: FeeAdjustedForMaturity · ImpliedRateUpdated · ParityDriftDetected

Sequence for a single swap through MochiYieldHook.

All three hook layers (rate sentinel, fee decay, parity oracle) run atomically within swap callbacks.

7. Reactive Network Pipeline

MochiReactiveKeeper (Reactive Lasna 5318007 / mainnet 1597) subscribes to the ParityDriftDetected topic on the origin chain hook at construction via service.subscribe().

On a qualifying event (driftBps ≥ driftThresholdBps), react() decodes the log, emits ParityCallbackTriggered, and emits a Callback carrying a restoreParity(...) payload to ArbitrageRouter on the origin chain. The router re-verifies drift and emits ParityRestored.

Full PT/YT swap execution is the next step — the router currently records drift and emits events. The contract contains: // TODO: execute PT/YT pool swaps via v4 router when callback is funded.

  • Topic: keccak256("ParityDriftDetected(uint256,uint256,uint256,uint256,bool)")
  • react() — vmOnly entry; decodes (ptPrice, ytPrice, underlyingPrice, driftBps, isOverValued) from log.data
  • Callback payload — restoreParity(address, ptPrice, ytPrice, underlyingPrice, isOverValued) on ArbitrageRouter
  • ArbitrageRouter — authorizedSenderOnly; reverts DriftBelowThreshold if drift < 500 bps
  • Reactive cannot subscribe to local Anvil (31337) — E2E loop requires Unichain Sepolia (1301) + Lasna (5318007)
Origin chainlive

MochiYieldHook

ParityDriftDetected

Reactive Lasnatestnet

MochiReactiveKeeper

react() · Callback

Origin chainpartial

ArbitrageRouter

restoreParity() · ParityRestored

Full PT/YT swap execution on callback is next — MVP records drift and emits events.

Origin chain → Reactive Lasna → callback → ArbitrageRouter on origin.

The cross-chain loop moves parity from passive monitoring to autonomous correction — with honest status labels on what ships today.

8. Contract Architecture

ContractRole
MochiYieldHook.solbeforeSwap / afterSwap — rate sentinel, fee decay, parity oracle
YieldVault.soldeposit() underlying → mint PT + YT 1:1; redeem() post-maturity
PTToken.solPrincipal Token ERC-20 — fixed income leg, redeemable at maturity
YTToken.solYield Token ERC-20 — yield speculation leg, converges to 0 at maturity
MochiReactiveKeeper.solReactive RSC on Lasna — subscribes to drift, fires callback
ArbitrageRouter.solOrigin-chain callback receiver — restoreParity()
MockWstETH.solTest underlying + faucet for local demo

All hook logic executes atomically within swap callbacks — no external oracle calls required for fee adjustment or parity checks.

9. On-Chain Events

Structured events for frontend, analytics, and Reactive Network integration. Full signatures (greppable against ABI):

// MochiYieldHook
event ImpliedRateUpdated(uint256 indexed maturity, int256 impliedAPY, uint256 ptPrice, uint256 timeToMaturity);
event FeeAdjustedForMaturity(bytes32 indexed poolId, uint256 timeToMaturity, uint256 newFeeBps);
event ParityDriftDetected(uint256 ptPrice, uint256 ytPrice, uint256 underlyingPrice, uint256 driftBps, bool isOverValued);
event SwapRejectedNegativeYield(bytes32 indexed poolId, address indexed swapper, int256 impliedAPY);
event MarketStressDetected(uint256 timestamp, string reason);
event PoolRegistered(bytes32 indexed poolId, bool isPTPool, uint256 maturity);

// MochiReactiveKeeper
event ParityCallbackTriggered(uint256 driftBps, bool isOverValued);

// ArbitrageRouter
event ParityRestoreRequested(uint256 ptPrice, uint256 ytPrice, uint256 underlyingPrice, uint256 driftBps, bool isOverValued);
event ParityRestored(uint256 ptPrice, uint256 ytPrice, uint256 underlyingPrice, uint256 driftBps, bool isOverValued);

// YieldVault
event Deposited(address indexed user, uint256 underlyingAmount, uint256 ptMinted, uint256 ytMinted);
event Redeemed(address indexed user, uint256 ptBurned, uint256 underlyingReturned);
  • ParityDriftDetected — subscribed by MochiReactiveKeeper via topic_0 on origin chain
  • FeeAdjustedForMaturity — emitted on every swap through a registered pool
  • ImpliedRateUpdated — PT pool swaps only; drives ImpliedRateGauge in the app
  • ParityRestored — emitted when ArbitrageRouter receives and validates a Reactive callback

10. Parameters & Constants

ParameterValue
BASE_FEE_BPS30 (0.30% — unused default; dynamic fee overrides)
MIN_FEE_BPS / MAX_FEE_BPS5 / 100 (0.05% – 1.00%)
MIN_FEE_TIME / MAX_FEE_TIME7 days / 90 days
MIN_IMPLIED_APY / MAX_IMPLIED_APY0 / 10000 bps (0% – 100%)
PARITY_DRIFT_THRESHOLD500 bps (5%)
DEFAULT_DRIFT_THRESHOLD_BPS (router)500 bps (5%)

All constants are defined in MochiYieldHook.sol and ArbitrageRouter.sol.

11. Security & Limitations

This is an MVP built for the UHI9 Hookathon. Known limitations:

  • underlyingPrice is a settable assumption (setUnderlyingPrice) — not a live Chainlink oracle
  • registerPool and setUnderlyingPrice have no access control — known limitation, intentional for local demo
  • ArbitrageRouter.restoreParity() records drift and emits ParityRestored — does not execute full v4 arb swaps yet
  • Reactive E2E requires testnet — cannot subscribe to Anvil chain 31337
  • Implied rate formula uses simple annualization, not compound discounting (faceValue/ptPrice)^n

Production deployment would add oracle feeds, access control, and funded swap paths on the router.

12. Deployments

Contract addresses are synced from deployments.json after each deploy. Three environments:

EnvironmentDetails
Anvil (local demo)Chain ID 31337 — hook, vault, markets. Reactive cannot subscribe here.
Unichain Sepolia (origin)Chain ID 1301 — Mochi stack + ArbitrageRouter. Event source and callback destination.
Reactive Lasna (keeper)Chain ID 5318007 — MochiReactiveKeeper. RPC: lasna-rpc.rnk.dev
Reactive mainnetChain ID 1597 — production Reactive Network (future)

Current addresses (deployments.json)

ContractAddress
Chain ID1301
YieldVault0xBd3c…AC01
MochiYieldHook0x1f59…80c0
PT Token0xb2c2…B5D1
YT Token0x2B21…E115
Underlying (wstETH)0x2c36…4e9E
WETH0xD2E4…f2Fb
Swap Router0x9cD2…8Dba
Pool Manager0x00B0…62AC
PT Pool ID0x19477f7b51c43d9111cf5623710c8e3230bbbd96522a68df321a8a4a7d646f7c
YT Pool ID0x88b76d7a883ccafd9c2a9490212af6bb256d516172d7a39bb7d1e985f267ed21

Local: run ./scripts/deploy-local.sh with plain anvil on port 8545, connect MetaMask to chain 31337. Testnet E2E: ./scripts/deploy-reactive.sh for Unichain Sepolia + Lasna.