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.
MochiYieldHook
ParityDriftDetected
MochiReactiveKeeper
react() · Callback
ArbitrageRouter
restoreParity() · ParityRestored
Full PT/YT swap execution on callback is next — MVP records drift and emits events.
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)
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:
| Role | Description |
|---|---|
| Alice · LP | Provides liquidity to PT/WETH or YT/WETH pools. Earns maturity-scaled swap fees — higher far from expiry, lower near redemption. |
| Bob · Yield trader | Deposits wstETH, sells PT, keeps YT. Gains leveraged yield exposure without selling the underlying. |
| Charlie · PT holder | Buys and holds PT to maturity. Fixed-income exposure — redeems 1:1 for underlying at expiry. |
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%
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
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:
beforeSwap()
Rate bounds + fee override
swap
v4 core executes
afterSwap()
APY update + parity check
emit: FeeAdjustedForMaturity · ImpliedRateUpdated · ParityDriftDetected
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)
MochiYieldHook
ParityDriftDetected
MochiReactiveKeeper
react() · Callback
ArbitrageRouter
restoreParity() · ParityRestored
Full PT/YT swap execution on callback is next — MVP records drift and emits events.
The cross-chain loop moves parity from passive monitoring to autonomous correction — with honest status labels on what ships today.
8. Contract Architecture
| Contract | Role |
|---|---|
| MochiYieldHook.sol | beforeSwap / afterSwap — rate sentinel, fee decay, parity oracle |
| YieldVault.sol | deposit() underlying → mint PT + YT 1:1; redeem() post-maturity |
| PTToken.sol | Principal Token ERC-20 — fixed income leg, redeemable at maturity |
| YTToken.sol | Yield Token ERC-20 — yield speculation leg, converges to 0 at maturity |
| MochiReactiveKeeper.sol | Reactive RSC on Lasna — subscribes to drift, fires callback |
| ArbitrageRouter.sol | Origin-chain callback receiver — restoreParity() |
| MockWstETH.sol | Test 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
| Parameter | Value |
|---|---|
| BASE_FEE_BPS | 30 (0.30% — unused default; dynamic fee overrides) |
| MIN_FEE_BPS / MAX_FEE_BPS | 5 / 100 (0.05% – 1.00%) |
| MIN_FEE_TIME / MAX_FEE_TIME | 7 days / 90 days |
| MIN_IMPLIED_APY / MAX_IMPLIED_APY | 0 / 10000 bps (0% – 100%) |
| PARITY_DRIFT_THRESHOLD | 500 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:
| Environment | Details |
|---|---|
| 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 mainnet | Chain ID 1597 — production Reactive Network (future) |
Current addresses (deployments.json)
| Contract | Address |
|---|---|
| Chain ID | 1301 |
| YieldVault | 0xBd3c…AC01 |
| MochiYieldHook | 0x1f59…80c0 |
| PT Token | 0xb2c2…B5D1 |
| YT Token | 0x2B21…E115 |
| Underlying (wstETH) | 0x2c36…4e9E |
| WETH | 0xD2E4…f2Fb |
| Swap Router | 0x9cD2…8Dba |
| Pool Manager | 0x00B0…62AC |
| PT Pool ID | 0x19477f7b51c43d9111cf5623710c8e3230bbbd96522a68df321a8a4a7d646f7c |
| YT Pool ID | 0x88b76d7a883ccafd9c2a9490212af6bb256d516172d7a39bb7d1e985f267ed21 |
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.